feat: 实现传送魔镜mod完整功能
Some checks failed
Build / build (push) Has been cancelled

- 回城魔镜:传送至出生点(优先床/重生锚,否则世界出生点)

- 传送魔镜:打开玩家选择UI后传送到目标玩家身边

- 4个等级(初级/中级/高级/永久),支持耐久、冷却、副作用配置

- 8个合成配方

- 支持中英文语言

- 修复配置加载顺序问题
This commit is contained in:
LiuEnder
2026-04-25 19:02:51 +08:00
parent 33145ade44
commit ceb59bb625
38 changed files with 904 additions and 261 deletions

View File

@@ -12,28 +12,28 @@ parchment_mappings_version=2024.11.10
# Environment Properties # Environment Properties
# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge # You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge
# The Minecraft version must agree with the Neo version to get a valid artifact # The Minecraft version must agree with the Neo version to get a valid artifact
minecraft_version=1.21 minecraft_version=1.21.1
# The Minecraft version range can use any release version of Minecraft as bounds. # The Minecraft version range can use any release version of Minecraft as bounds.
# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly # Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
# as they do not follow standard versioning conventions. # as they do not follow standard versioning conventions.
minecraft_version_range=[1.21] minecraft_version_range=[1.21.1]
# The Neo version must agree with the Minecraft version to get a valid artifact # The Neo version must agree with the Minecraft version to get a valid artifact
neo_version=21.0.167 neo_version=21.1.227
# The loader version range can only use the major version of FML as bounds # The loader version range can only use the major version of FML as bounds
loader_version_range=[1,) loader_version_range=[2,)
## Mod Properties ## Mod Properties
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
# Must match the String constant located in the main mod class annotated with @Mod. # Must match the String constant located in the main mod class annotated with @Mod.
mod_id=examplemod mod_id=teleportmirror
# The human-readable display name for the mod. # The human-readable display name for the mod.
mod_name=Example Mod mod_name=Teleport Mirror
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved mod_license=MIT
# The mod version. See https://semver.org/ # The mod version. See https://semver.org/
mod_version=1.0.0 mod_version=1.0.0
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources. # This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html # See https://maven.apache.org/guides/mini/guide-naming-conventions.html
mod_group_id=com.example.examplemod mod_group_id=com.le.teleportmirror

View File

@@ -1,42 +0,0 @@
package com.example.examplemod;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.config.ModConfigEvent;
import net.neoforged.neoforge.common.ModConfigSpec;
// An example config class. This is not required, but it's a good idea to have one to keep your config organized.
// Demonstrates how to use Neo's config APIs
public class Config {
private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
public static final ModConfigSpec.BooleanValue LOG_DIRT_BLOCK = BUILDER
.comment("Whether to log the dirt block on common setup")
.define("logDirtBlock", true);
public static final ModConfigSpec.IntValue MAGIC_NUMBER = BUILDER
.comment("A magic number")
.defineInRange("magicNumber", 42, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.ConfigValue<String> MAGIC_NUMBER_INTRODUCTION = BUILDER
.comment("What you want the introduction message to be for the magic number")
.define("magicNumberIntroduction", "The magic number is... ");
// a list of strings that are treated as resource locations for items
public static final ModConfigSpec.ConfigValue<List<? extends String>> ITEM_STRINGS = BUILDER
.comment("A list of items to log on common setup.")
.defineListAllowEmpty("items", List.of("minecraft:iron_ingot"), () -> "", Config::validateItemName);
static final ModConfigSpec SPEC = BUILDER.build();
private static boolean validateItemName(final Object obj) {
return obj instanceof String itemName && BuiltInRegistries.ITEM.containsKey(ResourceLocation.parse(itemName));
}
}

View File

@@ -1,131 +0,0 @@
package com.example.examplemod;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.food.FoodProperties;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraft.client.Minecraft;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
import net.neoforged.neoforge.event.server.ServerStartingEvent;
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
// The value here should match an entry in the META-INF/neoforge.mods.toml file
@Mod(ExampleMod.MODID)
public class ExampleMod {
// Define mod id in a common place for everything to reference
public static final String MODID = "examplemod";
// Directly reference a slf4j logger
public static final Logger LOGGER = LogUtils.getLogger();
// Create a Deferred Register to hold Blocks which will all be registered under the "examplemod" namespace
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MODID);
// Create a Deferred Register to hold Items which will all be registered under the "examplemod" namespace
public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID);
// Create a Deferred Register to hold CreativeModeTabs which will all be registered under the "examplemod" namespace
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS = DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);
// Creates a new Block with the id "examplemod:example_block", combining the namespace and path
public static final DeferredBlock<Block> EXAMPLE_BLOCK = BLOCKS.registerSimpleBlock("example_block", BlockBehaviour.Properties.of().mapColor(MapColor.STONE));
// Creates a new BlockItem with the id "examplemod:example_block", combining the namespace and path
public static final DeferredItem<BlockItem> EXAMPLE_BLOCK_ITEM = ITEMS.registerSimpleBlockItem("example_block", EXAMPLE_BLOCK);
// Creates a new food item with the id "examplemod:example_id", nutrition 1 and saturation 2
public static final DeferredItem<Item> EXAMPLE_ITEM = ITEMS.registerSimpleItem("example_item", new Item.Properties().food(new FoodProperties.Builder()
.alwaysEdible().nutrition(1).saturationModifier(2f).build()));
// Creates a creative tab with the id "examplemod:example_tab" for the example item, that is placed after the combat tab
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.examplemod")) //The language key for the title of your CreativeModeTab
.withTabsBefore(CreativeModeTabs.COMBAT)
.icon(() -> EXAMPLE_ITEM.get().getDefaultInstance())
.displayItems((parameters, output) -> {
output.accept(EXAMPLE_ITEM.get()); // Add the example item to the tab. For your own tabs, this method is preferred over the event
}).build());
// The constructor for the mod class is the first code that is run when your mod is loaded.
// FML will recognize some parameter types like IEventBus or ModContainer and pass them in automatically.
public ExampleMod(IEventBus modEventBus, ModContainer modContainer) {
// Register the commonSetup method for modloading
modEventBus.addListener(this::commonSetup);
// Register the Deferred Register to the mod event bus so blocks get registered
BLOCKS.register(modEventBus);
// Register the Deferred Register to the mod event bus so items get registered
ITEMS.register(modEventBus);
// Register the Deferred Register to the mod event bus so tabs get registered
CREATIVE_MODE_TABS.register(modEventBus);
// Register ourselves for server and other game events we are interested in.
// Note that this is necessary if and only if we want *this* class (ExampleMod) to respond directly to events.
// Do not add this line if there are no @SubscribeEvent-annotated functions in this class, like onServerStarting() below.
NeoForge.EVENT_BUS.register(this);
// Register the item to a creative tab
modEventBus.addListener(this::addCreative);
// Register our mod's ModConfigSpec so that FML can create and load the config file for us
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
}
private void commonSetup(FMLCommonSetupEvent event) {
// Some common setup code
LOGGER.info("HELLO FROM COMMON SETUP");
if (Config.LOG_DIRT_BLOCK.getAsBoolean()) {
LOGGER.info("DIRT BLOCK >> {}", BuiltInRegistries.BLOCK.getKey(Blocks.DIRT));
}
LOGGER.info("{}{}", Config.MAGIC_NUMBER_INTRODUCTION.get(), Config.MAGIC_NUMBER.getAsInt());
Config.ITEM_STRINGS.get().forEach((item) -> LOGGER.info("ITEM >> {}", item));
}
// Add the example block item to the building blocks tab
private void addCreative(BuildCreativeModeTabContentsEvent event) {
if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) {
event.accept(EXAMPLE_BLOCK_ITEM);
}
}
// You can use SubscribeEvent and let the Event Bus discover methods to call
@SubscribeEvent
public void onServerStarting(ServerStartingEvent event) {
// Do something when the server starts
LOGGER.info("HELLO from server starting");
}
// You can use EventBusSubscriber to automatically register all static methods in the class annotated with @SubscribeEvent
@EventBusSubscriber(modid = ExampleMod.MODID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
static class ClientModEvents {
@SubscribeEvent
static void onClientSetup(FMLClientSetupEvent event) {
// Some client setup code
LOGGER.info("HELLO FROM CLIENT SETUP");
LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
}
}
}

View File

@@ -0,0 +1,65 @@
package com.le.teleportmirror;
import net.neoforged.neoforge.common.ModConfigSpec;
public class Config {
private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
public static final ModConfigSpec.IntValue COOLDOWN_SECONDS = BUILDER
.comment("Cooldown in seconds between mirror uses")
.defineInRange("cooldownSeconds", 60, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue CHARGE_TICKS = BUILDER
.comment("Number of ticks required to charge the mirror (20 ticks = 1 second)")
.defineInRange("chargeTicks", 40, 1, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue BASIC_DURABILITY = BUILDER
.comment("Durability for Basic tier mirrors")
.defineInRange("basicDurability", 10, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue INTERMEDIATE_DURABILITY = BUILDER
.comment("Durability for Intermediate tier mirrors")
.defineInRange("intermediateDurability", 50, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue ADVANCED_DURABILITY = BUILDER
.comment("Durability for Advanced tier mirrors")
.defineInRange("advancedDurability", 100, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue BASIC_NAUSEA_SECONDS = BUILDER
.comment("Nausea effect duration in seconds for Basic tier")
.defineInRange("basicNauseaSeconds", 10, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue BASIC_WITHER_SECONDS = BUILDER
.comment("Wither effect duration in seconds for Basic tier")
.defineInRange("basicWitherSeconds", 5, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue INTERMEDIATE_NAUSEA_SECONDS = BUILDER
.comment("Nausea effect duration in seconds for Intermediate tier")
.defineInRange("intermediateNauseaSeconds", 5, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue INTERMEDIATE_WITHER_SECONDS = BUILDER
.comment("Wither effect duration in seconds for Intermediate tier")
.defineInRange("intermediateWitherSeconds", 3, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.IntValue ADVANCED_NAUSEA_SECONDS = BUILDER
.comment("Nausea effect duration in seconds for Advanced tier")
.defineInRange("advancedNauseaSeconds", 3, 0, Integer.MAX_VALUE);
public static final ModConfigSpec.BooleanValue ENABLE_BASIC_RECIPE = BUILDER
.comment("Enable crafting recipe for Basic tier mirrors")
.define("enableBasicRecipe", true);
public static final ModConfigSpec.BooleanValue ENABLE_INTERMEDIATE_RECIPE = BUILDER
.comment("Enable crafting recipe for Intermediate tier mirrors")
.define("enableIntermediateRecipe", true);
public static final ModConfigSpec.BooleanValue ENABLE_ADVANCED_RECIPE = BUILDER
.comment("Enable crafting recipe for Advanced tier mirrors")
.define("enableAdvancedRecipe", true);
public static final ModConfigSpec.BooleanValue ENABLE_PERMANENT_RECIPE = BUILDER
.comment("Enable crafting recipe for Permanent tier mirrors")
.define("enablePermanentRecipe", true);
static final ModConfigSpec SPEC = BUILDER.build();
}

View File

@@ -0,0 +1,208 @@
package com.le.teleportmirror;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.UseAnim;
import net.minecraft.world.level.Level;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.food.FoodData;
public class MirrorItem extends Item {
private final MirrorTier tier;
private final MirrorType type;
public MirrorItem(MirrorTier tier, MirrorType type, Properties properties) {
super(properties);
this.tier = tier;
this.type = type;
}
public MirrorTier getTier() {
return tier;
}
public MirrorType getMirrorType() {
return type;
}
@Override
public UseAnim getUseAnimation(ItemStack stack) {
return UseAnim.BOW;
}
@Override
public int getUseDuration(ItemStack stack, LivingEntity entity) {
return 72000;
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (player.getCooldowns().isOnCooldown(stack.getItem())) {
return InteractionResultHolder.fail(stack);
}
player.startUsingItem(hand);
return InteractionResultHolder.consume(stack);
}
@Override
public void releaseUsing(ItemStack stack, Level level, LivingEntity entity, int timeCharged) {
if (!(entity instanceof Player player)) {
return;
}
int chargeTicks = Config.CHARGE_TICKS.get();
if (timeCharged < chargeTicks) {
return;
}
if (level.isClientSide) {
return;
}
ServerPlayer serverPlayer = (ServerPlayer) player;
if (type == MirrorType.RETURN) {
performReturnTeleport(serverPlayer, stack);
} else {
MirrorNetwork.sendOpenSelectionToClient(serverPlayer, tier);
}
}
private void performReturnTeleport(ServerPlayer player, ItemStack stack) {
ServerLevel serverLevel = player.serverLevel();
BlockPos targetPos;
ServerLevel targetLevel;
ResourceKey<Level> respawnDimension = player.getRespawnDimension();
BlockPos respawnPos = player.getRespawnPosition();
if (respawnPos != null) {
targetLevel = player.server.getLevel(respawnDimension);
if (targetLevel == null) {
targetLevel = serverLevel;
}
targetPos = respawnPos;
} else {
targetLevel = player.server.getLevel(Level.OVERWORLD);
if (targetLevel == null) {
targetLevel = serverLevel;
}
targetPos = targetLevel.getSharedSpawnPos();
}
consumeMirrorUse(player, stack);
player.teleportTo(targetLevel, targetPos.getCenter().x, targetPos.getCenter().y, targetPos.getCenter().z,
player.getYRot(), player.getXRot());
targetLevel.playSound(null, targetPos, SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0f, 1.0f);
}
public void performTeleportToPlayer(ServerPlayer player, ServerPlayer target, ItemStack stack) {
ServerLevel targetLevel = target.serverLevel();
BlockPos targetPos = target.blockPosition();
consumeMirrorUse(player, stack);
player.teleportTo(targetLevel, targetPos.getCenter().x, targetPos.getCenter().y, targetPos.getCenter().z,
player.getYRot(), player.getXRot());
targetLevel.playSound(null, targetPos, SoundEvents.ENDERMAN_TELEPORT, SoundSource.PLAYERS, 1.0f, 1.0f);
}
private int getConfigMaxDamage() {
return switch (tier) {
case BASIC -> Config.BASIC_DURABILITY.get();
case INTERMEDIATE -> Config.INTERMEDIATE_DURABILITY.get();
case ADVANCED -> Config.ADVANCED_DURABILITY.get();
case PERMANENT -> Integer.MAX_VALUE;
};
}
private void consumeMirrorUse(ServerPlayer player, ItemStack stack) {
applySideEffects(player);
applyCooldown(player);
if (!tier.isPermanent()) {
int maxDamage = getConfigMaxDamage();
if (stack.getDamageValue() + 1 >= maxDamage) {
stack.shrink(1);
player.level().playSound(null, player.blockPosition(),
SoundEvents.ITEM_BREAK, SoundSource.PLAYERS, 1.0f, 1.0f);
} else {
stack.setDamageValue(stack.getDamageValue() + 1);
}
}
}
private void applyCooldown(Player player) {
int cooldownTicks = Config.COOLDOWN_SECONDS.get() * 20;
player.getCooldowns().addCooldown(this, cooldownTicks);
}
private void applySideEffects(Player player) {
int nauseaDuration;
int witherDuration;
switch (tier) {
case BASIC:
nauseaDuration = Config.BASIC_NAUSEA_SECONDS.get() * 20;
witherDuration = Config.BASIC_WITHER_SECONDS.get() * 20;
if (nauseaDuration > 0) {
player.addEffect(new MobEffectInstance(MobEffects.CONFUSION, nauseaDuration, 0));
}
if (witherDuration > 0) {
player.addEffect(new MobEffectInstance(MobEffects.WITHER, witherDuration, 0));
}
halveFood(player);
break;
case INTERMEDIATE:
nauseaDuration = Config.INTERMEDIATE_NAUSEA_SECONDS.get() * 20;
witherDuration = Config.INTERMEDIATE_WITHER_SECONDS.get() * 20;
if (nauseaDuration > 0) {
player.addEffect(new MobEffectInstance(MobEffects.CONFUSION, nauseaDuration, 0));
}
if (witherDuration > 0) {
player.addEffect(new MobEffectInstance(MobEffects.WITHER, witherDuration, 0));
}
halveFood(player);
break;
case ADVANCED:
nauseaDuration = Config.ADVANCED_NAUSEA_SECONDS.get() * 20;
if (nauseaDuration > 0) {
player.addEffect(new MobEffectInstance(MobEffects.CONFUSION, nauseaDuration, 0));
}
halveFood(player);
break;
case PERMANENT:
break;
}
}
private void halveFood(Player player) {
FoodData foodData = player.getFoodData();
foodData.setFoodLevel(Math.max(1, foodData.getFoodLevel() / 2));
foodData.setSaturation(0);
}
@Override
public boolean isEnchantable(ItemStack stack) {
return false;
}
@Override
public boolean isFoil(ItemStack stack) {
return tier == MirrorTier.PERMANENT;
}
}

View File

@@ -0,0 +1,153 @@
package com.le.teleportmirror;
import com.le.teleportmirror.screen.PlayerSelectionScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.registration.PayloadRegistrar;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class MirrorNetwork {
public static void registerPayloads(final RegisterPayloadHandlersEvent event) {
final PayloadRegistrar registrar = event.registrar(TeleportMirrorMod.MODID).versioned("1.0.0");
registrar.playToClient(
OpenPlayerSelectionPayload.TYPE,
OpenPlayerSelectionPayload.STREAM_CODEC,
MirrorNetwork::handleOpenSelection
);
registrar.playToServer(
RequestTeleportPayload.TYPE,
RequestTeleportPayload.STREAM_CODEC,
MirrorNetwork::handleRequestTeleport
);
}
public static void sendOpenSelectionToClient(ServerPlayer player, MirrorTier tier) {
List<String> names = new ArrayList<>();
List<UUID> uuids = new ArrayList<>();
for (ServerPlayer onlinePlayer : player.server.getPlayerList().getPlayers()) {
if (!onlinePlayer.getUUID().equals(player.getUUID())) {
names.add(onlinePlayer.getName().getString());
uuids.add(onlinePlayer.getUUID());
}
}
PacketDistributor.sendToPlayer(player, new OpenPlayerSelectionPayload(names, uuids, tier.getName()));
}
private static void handleOpenSelection(OpenPlayerSelectionPayload payload, IPayloadContext context) {
context.enqueueWork(() -> {
Minecraft client = Minecraft.getInstance();
MirrorTier tier = MirrorTier.valueOf(payload.tierName.toUpperCase());
client.setScreen(new PlayerSelectionScreen(payload.playerNames, payload.playerUuids, tier));
});
}
private static void handleRequestTeleport(RequestTeleportPayload payload, IPayloadContext context) {
context.enqueueWork(() -> {
ServerPlayer sender = (ServerPlayer) context.player();
ServerPlayer target = sender.server.getPlayerList().getPlayer(payload.targetUuid);
if (target == null) {
return;
}
MirrorTier tier = MirrorTier.valueOf(payload.tierName.toUpperCase());
ItemStack mainHand = sender.getMainHandItem();
ItemStack offHand = sender.getOffhandItem();
ItemStack mirrorStack = null;
if (mainHand.getItem() instanceof MirrorItem mirrorItem && mirrorItem.getMirrorType() == MirrorType.TELEPORT) {
mirrorStack = mainHand;
} else if (offHand.getItem() instanceof MirrorItem mirrorItem && mirrorItem.getMirrorType() == MirrorType.TELEPORT) {
mirrorStack = offHand;
}
if (mirrorStack == null || mirrorStack.isEmpty()) {
return;
}
MirrorItem mirrorItem = (MirrorItem) mirrorStack.getItem();
if (mirrorItem.getTier() != tier) {
return;
}
mirrorItem.performTeleportToPlayer(sender, target, mirrorStack);
});
}
public record OpenPlayerSelectionPayload(List<String> playerNames, List<UUID> playerUuids,
String tierName) implements CustomPacketPayload {
public static final Type<OpenPlayerSelectionPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(TeleportMirrorMod.MODID, "open_player_selection"));
public static final StreamCodec<FriendlyByteBuf, OpenPlayerSelectionPayload> STREAM_CODEC = new StreamCodec<>() {
@Override
public OpenPlayerSelectionPayload decode(FriendlyByteBuf buf) {
int size = buf.readVarInt();
List<String> names = new ArrayList<>();
List<UUID> uuids = new ArrayList<>();
for (int i = 0; i < size; i++) {
names.add(buf.readUtf());
uuids.add(buf.readUUID());
}
String tierName = buf.readUtf();
return new OpenPlayerSelectionPayload(names, uuids, tierName);
}
@Override
public void encode(FriendlyByteBuf buf, OpenPlayerSelectionPayload payload) {
buf.writeVarInt(payload.playerNames.size());
for (int i = 0; i < payload.playerNames.size(); i++) {
buf.writeUtf(payload.playerNames.get(i));
buf.writeUUID(payload.playerUuids.get(i));
}
buf.writeUtf(payload.tierName);
}
};
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
public record RequestTeleportPayload(UUID targetUuid, String tierName) implements CustomPacketPayload {
public static final Type<RequestTeleportPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(TeleportMirrorMod.MODID, "request_teleport"));
public static final StreamCodec<FriendlyByteBuf, RequestTeleportPayload> STREAM_CODEC = new StreamCodec<>() {
@Override
public RequestTeleportPayload decode(FriendlyByteBuf buf) {
UUID targetUuid = buf.readUUID();
String tierName = buf.readUtf();
return new RequestTeleportPayload(targetUuid, tierName);
}
@Override
public void encode(FriendlyByteBuf buf, RequestTeleportPayload payload) {
buf.writeUUID(payload.targetUuid);
buf.writeUtf(payload.tierName);
}
};
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
}

View File

@@ -0,0 +1,28 @@
package com.le.teleportmirror;
public enum MirrorTier {
BASIC(10, "basic"),
INTERMEDIATE(50, "intermediate"),
ADVANCED(100, "advanced"),
PERMANENT(-1, "permanent");
private final int defaultDurability;
private final String name;
MirrorTier(int defaultDurability, String name) {
this.defaultDurability = defaultDurability;
this.name = name;
}
public int getDefaultDurability() {
return defaultDurability;
}
public String getName() {
return name;
}
public boolean isPermanent() {
return this == PERMANENT;
}
}

View File

@@ -0,0 +1,16 @@
package com.le.teleportmirror;
public enum MirrorType {
RETURN("return"),
TELEPORT("teleport");
private final String name;
MirrorType(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

View File

@@ -0,0 +1,85 @@
package com.le.teleportmirror;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab;
import net.minecraft.world.item.CreativeModeTabs;
import net.minecraft.world.item.Item;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
@Mod(TeleportMirrorMod.MODID)
public class TeleportMirrorMod {
public static final String MODID = "teleportmirror";
public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID);
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);
public static final DeferredItem<MirrorItem> BASIC_RETURN_MIRROR = ITEMS.register("basic_return_mirror",
() -> new MirrorItem(MirrorTier.BASIC, MirrorType.RETURN,
new Item.Properties().stacksTo(1).durability(MirrorTier.BASIC.getDefaultDurability())));
public static final DeferredItem<MirrorItem> INTERMEDIATE_RETURN_MIRROR = ITEMS.register("intermediate_return_mirror",
() -> new MirrorItem(MirrorTier.INTERMEDIATE, MirrorType.RETURN,
new Item.Properties().stacksTo(1).durability(MirrorTier.INTERMEDIATE.getDefaultDurability())));
public static final DeferredItem<MirrorItem> ADVANCED_RETURN_MIRROR = ITEMS.register("advanced_return_mirror",
() -> new MirrorItem(MirrorTier.ADVANCED, MirrorType.RETURN,
new Item.Properties().stacksTo(1).durability(MirrorTier.ADVANCED.getDefaultDurability())));
public static final DeferredItem<MirrorItem> PERMANENT_RETURN_MIRROR = ITEMS.register("permanent_return_mirror",
() -> new MirrorItem(MirrorTier.PERMANENT, MirrorType.RETURN,
new Item.Properties().stacksTo(1)));
public static final DeferredItem<MirrorItem> BASIC_TELEPORT_MIRROR = ITEMS.register("basic_teleport_mirror",
() -> new MirrorItem(MirrorTier.BASIC, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1).durability(MirrorTier.BASIC.getDefaultDurability())));
public static final DeferredItem<MirrorItem> INTERMEDIATE_TELEPORT_MIRROR = ITEMS.register("intermediate_teleport_mirror",
() -> new MirrorItem(MirrorTier.INTERMEDIATE, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1).durability(MirrorTier.INTERMEDIATE.getDefaultDurability())));
public static final DeferredItem<MirrorItem> ADVANCED_TELEPORT_MIRROR = ITEMS.register("advanced_teleport_mirror",
() -> new MirrorItem(MirrorTier.ADVANCED, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1).durability(MirrorTier.ADVANCED.getDefaultDurability())));
public static final DeferredItem<MirrorItem> PERMANENT_TELEPORT_MIRROR = ITEMS.register("permanent_teleport_mirror",
() -> new MirrorItem(MirrorTier.PERMANENT, MirrorType.TELEPORT,
new Item.Properties().stacksTo(1)));
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> MIRROR_TAB =
CREATIVE_MODE_TABS.register("mirror_tab", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.teleportmirror"))
.withTabsBefore(CreativeModeTabs.COMBAT)
.icon(() -> BASIC_RETURN_MIRROR.get().getDefaultInstance())
.displayItems((parameters, output) -> {
output.accept(BASIC_RETURN_MIRROR.get());
output.accept(INTERMEDIATE_RETURN_MIRROR.get());
output.accept(ADVANCED_RETURN_MIRROR.get());
output.accept(PERMANENT_RETURN_MIRROR.get());
output.accept(BASIC_TELEPORT_MIRROR.get());
output.accept(INTERMEDIATE_TELEPORT_MIRROR.get());
output.accept(ADVANCED_TELEPORT_MIRROR.get());
output.accept(PERMANENT_TELEPORT_MIRROR.get());
}).build());
public TeleportMirrorMod(IEventBus modEventBus, ModContainer modContainer) {
ITEMS.register(modEventBus);
CREATIVE_MODE_TABS.register(modEventBus);
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
modEventBus.addListener(this::onRegisterPayloads);
}
private void onRegisterPayloads(final RegisterPayloadHandlersEvent event) {
MirrorNetwork.registerPayloads(event);
}
}

View File

@@ -0,0 +1,100 @@
package com.le.teleportmirror.screen;
import com.le.teleportmirror.MirrorNetwork;
import com.le.teleportmirror.MirrorTier;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.neoforged.neoforge.network.PacketDistributor;
import java.util.List;
import java.util.UUID;
public class PlayerSelectionScreen extends Screen {
private final List<String> playerNames;
private final List<UUID> playerUuids;
private final MirrorTier tier;
private int scrollOffset = 0;
private static final int BUTTONS_PER_PAGE = 8;
public PlayerSelectionScreen(List<String> playerNames, List<UUID> playerUuids, MirrorTier tier) {
super(Component.translatable("screen.teleportmirror.select_player"));
this.playerNames = playerNames;
this.playerUuids = playerUuids;
this.tier = tier;
}
@Override
protected void init() {
super.init();
rebuildButtons();
}
private void rebuildButtons() {
this.clearWidgets();
int centerX = this.width / 2;
int startY = 40;
int buttonWidth = 200;
int buttonHeight = 20;
int spacing = 4;
int maxOffset = Math.max(0, playerNames.size() - BUTTONS_PER_PAGE);
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
int endIndex = Math.min(scrollOffset + BUTTONS_PER_PAGE, playerNames.size());
for (int i = scrollOffset; i < endIndex; i++) {
final int idx = i;
int y = startY + (i - scrollOffset) * (buttonHeight + spacing);
this.addRenderableWidget(Button.builder(
Component.literal(playerNames.get(i)),
btn -> selectPlayer(idx))
.pos(centerX - buttonWidth / 2, y)
.size(buttonWidth, buttonHeight)
.build());
}
if (playerNames.size() > BUTTONS_PER_PAGE) {
this.addRenderableWidget(Button.builder(
Component.literal("\u25B2"),
btn -> { scrollOffset = Math.max(0, scrollOffset - 1); rebuildButtons(); })
.pos(centerX + buttonWidth / 2 + 5, startY)
.size(20, 20)
.build());
this.addRenderableWidget(Button.builder(
Component.literal("\u25BC"),
btn -> { scrollOffset = Math.min(maxOffset, scrollOffset + 1); rebuildButtons(); })
.pos(centerX + buttonWidth / 2 + 5, startY + BUTTONS_PER_PAGE * (buttonHeight + spacing) - spacing)
.size(20, 20)
.build());
}
this.addRenderableWidget(Button.builder(
Component.translatable("gui.cancel"),
btn -> this.onClose())
.pos(centerX - 50, this.height - 30)
.size(100, 20)
.build());
}
private void selectPlayer(int index) {
if (index >= 0 && index < playerUuids.size()) {
UUID targetUuid = playerUuids.get(index);
PacketDistributor.sendToServer(new MirrorNetwork.RequestTeleportPayload(targetUuid, tier.getName()));
}
this.onClose();
}
@Override
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) {
this.renderBackground(guiGraphics, mouseX, mouseY, partialTick);
guiGraphics.drawCenteredString(this.font, this.title, this.width / 2, 15, 0xFFFFFF);
super.render(guiGraphics, mouseX, mouseY, partialTick);
}
@Override
public boolean isPauseScreen() {
return false;
}
}

View File

@@ -1,5 +0,0 @@
{
"itemGroup.examplemod": "Example Mod Tab",
"block.examplemod.example_block": "Example Block",
"item.examplemod.example_item": "Example Item"
}

View File

@@ -0,0 +1,13 @@
{
"itemGroup.teleportmirror": "Teleport Mirror",
"item.teleportmirror.basic_return_mirror": "Basic Return Mirror",
"item.teleportmirror.intermediate_return_mirror": "Intermediate Return Mirror",
"item.teleportmirror.advanced_return_mirror": "Advanced Return Mirror",
"item.teleportmirror.permanent_return_mirror": "Permanent Return Mirror",
"item.teleportmirror.basic_teleport_mirror": "Basic Teleport Mirror",
"item.teleportmirror.intermediate_teleport_mirror": "Intermediate Teleport Mirror",
"item.teleportmirror.advanced_teleport_mirror": "Advanced Teleport Mirror",
"item.teleportmirror.permanent_teleport_mirror": "Permanent Teleport Mirror",
"screen.teleportmirror.select_player": "Select Target Player",
"teleportmirror.cooldown": "Mirror is on cooldown"
}

View File

@@ -0,0 +1,13 @@
{
"itemGroup.teleportmirror": "传送魔镜",
"item.teleportmirror.basic_return_mirror": "初级回城魔镜",
"item.teleportmirror.intermediate_return_mirror": "中级回城魔镜",
"item.teleportmirror.advanced_return_mirror": "高级回城魔镜",
"item.teleportmirror.permanent_return_mirror": "永久回城魔镜",
"item.teleportmirror.basic_teleport_mirror": "初级传送魔镜",
"item.teleportmirror.intermediate_teleport_mirror": "中级传送魔镜",
"item.teleportmirror.advanced_teleport_mirror": "高级传送魔镜",
"item.teleportmirror.permanent_teleport_mirror": "永久传送魔镜",
"screen.teleportmirror.select_player": "选择目标玩家",
"teleportmirror.cooldown": "魔镜冷却中"
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/advanced_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/advanced_teleport_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/basic_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/basic_teleport_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/intermediate_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/intermediate_teleport_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/permanent_return_mirror"
}
}

View File

@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "teleportmirror:item/permanent_teleport_mirror"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"MRM",
"DBD"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_eye" },
"M": { "item": "minecraft:emerald" },
"R": { "item": "teleportmirror:intermediate_return_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:advanced_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"MRM",
"DBD"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_eye" },
"M": { "item": "minecraft:emerald" },
"R": { "item": "teleportmirror:intermediate_teleport_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:advanced_teleport_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"GCG",
"IBI",
"PIP"
],
"key": {
"G": { "item": "minecraft:gold_ingot" },
"C": { "item": "minecraft:compass" },
"B": { "item": "minecraft:blue_stained_glass_pane" },
"I": { "item": "minecraft:iron_ingot" },
"P": { "item": "minecraft:copper_ingot" }
},
"result": {
"id": "teleportmirror:basic_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"GLG",
"IOI",
"PIP"
],
"key": {
"G": { "item": "minecraft:gold_ingot" },
"L": { "item": "minecraft:lead" },
"O": { "item": "minecraft:orange_stained_glass_pane" },
"I": { "item": "minecraft:iron_ingot" },
"P": { "item": "minecraft:copper_ingot" }
},
"result": {
"id": "teleportmirror:basic_teleport_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,20 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"LRL",
"TAT"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_pearl" },
"L": { "item": "minecraft:lapis_lazuli" },
"R": { "item": "teleportmirror:basic_return_mirror" },
"T": { "item": "minecraft:redstone" },
"A": { "item": "minecraft:amethyst_shard" }
},
"result": {
"id": "teleportmirror:intermediate_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,20 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"DED",
"LRL",
"TAT"
],
"key": {
"D": { "item": "minecraft:diamond" },
"E": { "item": "minecraft:ender_pearl" },
"L": { "item": "minecraft:lapis_lazuli" },
"R": { "item": "teleportmirror:basic_teleport_mirror" },
"T": { "item": "minecraft:redstone" },
"A": { "item": "minecraft:amethyst_shard" }
},
"result": {
"id": "teleportmirror:intermediate_teleport_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"SCS",
"ERE",
"SBS"
],
"key": {
"S": { "item": "minecraft:shulker_shell" },
"C": { "item": "minecraft:chorus_fruit" },
"E": { "item": "minecraft:ender_pearl" },
"R": { "item": "teleportmirror:advanced_return_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:permanent_return_mirror",
"count": 1
}
}

View File

@@ -0,0 +1,19 @@
{
"type": "minecraft:crafting_shaped",
"pattern": [
"SCS",
"ERE",
"SBS"
],
"key": {
"S": { "item": "minecraft:shulker_shell" },
"C": { "item": "minecraft:chorus_fruit" },
"E": { "item": "minecraft:ender_pearl" },
"R": { "item": "teleportmirror:advanced_teleport_mirror" },
"B": { "item": "minecraft:blaze_rod" }
},
"result": {
"id": "teleportmirror:permanent_teleport_mirror",
"count": 1
}
}

View File

@@ -1,95 +1,33 @@
# This is an example neoforge.mods.toml file. It contains the data relating to the loading mods. modLoader="javafml"
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here: https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml loaderVersion="${loader_version_range}"
modLoader="javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the FML version. This is currently 2.
loaderVersion="${loader_version_range}" #mandatory
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="${mod_license}" license="${mod_license}"
# A URL to refer people to when problems occur with this mod [[mods]]
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
# A list of mods - how many allowed here is determined by the individual mod loader modId="${mod_id}"
[[mods]] #mandatory
# The modid of the mod version="${mod_version}"
modId="${mod_id}" #mandatory
# The version number of the mod displayName="${mod_name}"
version="${mod_version}" #mandatory
# A display name for the mod
displayName="${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforged.net/docs/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
#logoFile="examplemod.png" #optional
# A text field displayed in the mod UI
#credits="" #optional
# The authors of the mod, displayed in the mod UI (optional)
#authors=""
# The description text for the mod (multi line!) (#mandatory)
description=''' description='''
Example mod description. Teleport Mirror mod adds magical mirrors that allow players to teleport.
Two types: Return Mirror (teleports to spawn) and Teleport Mirror (teleports to another player).
Four tiers: Basic, Intermediate, Advanced, and Permanent.
''' '''
# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. [[dependencies.${mod_id}]]
#[[mixins]] modId="neoforge"
#config="${mod_id}.mixins.json" type="required"
versionRange="[${neo_version},)"
# The [[accessTransformers]] block allows you to declare where your AT file is.
# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
#[[accessTransformers]]
#file="META-INF/accesstransformer.cfg"
# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies.${mod_id}]] #optional
# the modid of the dependency
modId="neoforge" #mandatory
# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive).
# 'required' requires the mod to exist, 'optional' does not
# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning
type="required" #mandatory
# Optional field describing why the dependency is required or why it is incompatible
# reason="..."
# The version range of the dependency
versionRange="[${neo_version},)" #mandatory
# An ordering relationship for the dependency.
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
ordering="NONE" ordering="NONE"
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side="BOTH" side="BOTH"
# Here's another dependency
[[dependencies.${mod_id}]] [[dependencies.${mod_id}]]
modId="minecraft" modId="minecraft"
type="required" type="required"
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange="${minecraft_version_range}" versionRange="${minecraft_version_range}"
ordering="NONE" ordering="NONE"
side="BOTH" side="BOTH"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.
#[features.${mod_id}]
#openGLVersion="[3.2,)"