Tutorial ¡Crea tu propio mod de Minecraft! [Forge 1.19.4+]

OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España
¡Buenas!

Les voy a enseñar las cosas más básicas para empezar a realizar mods en Minecraft a través del modloader de Forge. No existen tutoriales en español al respecto y puede variar entre versiones.

¿Por qué la para la versión 1.19.4 y no la 1.20.X?
Debido a que es la versión que la comunidad considera "LTS" o la versión que va a ser más longeva en servidores (Algo así como lo que pasó con la 1.16.5), y me parece interesante enseñar una versión que pueda durar más en el tiempo, aunque realmente con experiencia puedes adaptar este tutorial a versiones superiores (1.20.X) sin mucho problema.

¿Qué necesito para empezar a crear mods de Minecraft?

Lo primero que necesitas es conocimiento de Java 8 como mínimo, a ser posible conocer las nuevas características implementadas a partir de Java 11 puede ser beneficioso. También necesitaras Intellij IDEA Community e instalar el plugin Minecraft Development.

Si tienes dudas, puedes postearlas en este mismo tema y las iré resolviendo en cuanto las vaya viendo.

Notas:

  • El tutorial es bastante extenso así que lo subiré poco a poco para subir también los commits de github y que puedan ver el proyecto completo.
  • Las imágenes las pondré dentro de spoilers para hacer el texto mas legible.
  • Al final de cada post, tendréis una versión del repositorio completo con los cambios realizados

Índice del Temario:
  1. Iniciando
    1. Configurando el entorno
    2. Creando el proyecto
    3. Finalizando la burocracia
  2. Ítems y bloques
    1. Creando los primeros ítems
    2. Creando el primer bloque
    3. Creando una pestaña del creativo
  3. Providers: Generación automática de archivos JSON
    1. Aplicando texturas
    2. Creando nuevas recetas
    3. Configurando drops
    4. Usando traducciones
  4. Comandos
  5. Generación de mundo
  6. Networking
    1. Asignar datos al jugador
    2. Creando y manejando paquetes de red
  7. Eventos
    1. Eventos de servidor
    2. Eventos de cliente
  8. Generar archivos de configuración
  9. ¿Cómo sigo aprendiendo?
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

1. Iniciando


1.1 Configurando el entorno

Para configurar nuestro entorno debemos tener previamente descargado e instalado Intellij IDEA Community y la JDK 17 de Java.

Lo primero que veremos al iniciar nuestro entorno de desarrollo (A partir de ahora IDE) es lo siguiente:
sPwEuzu.png

Esto solamente aparecerá la primera vez, ya que desde ahora siempre nos abrirá el proyecto directamente. Lo que nos interesa es instalar el plugin específico que se menciona en la introducción. Para eso vamos a Plugins en el panel izquierdo, y en la barra de búsqueda escribimos Minecraft.
2wrL8is.png

Una vez escrito nos aparecerán todos los plugins, en este caso solo nos interesa el plugin Minecraft Development, y opcionalmente Minecraft Resources Support. Para instalarlos solo le debemos dar al boton Install, y esperamos a que termine para reiniciar el IDE y estar listos para crear el proyecto.



1.2 Creando el proyecto

Para crear el proyecto solamente necesitamos ir a la pestaña Projects y crear el nuevo proyecto, se nos abrirá una ventana donde deberemos rellenar una serie de datos básicos para inicializar nuestro proyecto. Los parametros más importantes son:
- Platform type y Platform: El tipo de proyecto que estamos creado, en este caso un mod de Forge.
- Minecraft/Forge Versions: Las versiones de Minecraft y el Modloader, en este caso voy a usar la 1.19.4 y 45.2.0 respectivamente.
- Mod name: El nombre que le queremos poner a nuestro mod.

El resto lo pueden configurar o les pueden poner lo que quieran. Lo mejor es dejar por defecto todo lo que no sepas que es, excepto los campos mencionados arriba.
eHBATXB.png

Una vez le damos al botón crear, va a tardar un rato (paciencia, puede tardar media hora o más) en terminar de configurarlo todo, podemos ver el progreso abajo a la izquierda. Veremos que ha finalizado cuando nos aparezca BUILD SUCCESSFUL in XXm XXs en la pestaña build.
T30KYgL.png

Con esto tendremos el IDE configurado para trabajar con Gradle, Minecraft y Java. Deberían ver algo similar a esto una vez terminado el proceso.
SpgyeTb.png



1.3 Finalizando la burocracia

Para terminar el proceso vamos a modificar y borrar algunas cosas para dejarlo todo listo, cuando terminemos ya podremos empezar a programar nuestro mod.

El archivo más importante y donde vamos a realizar más cambios es src/main/java/net/rudahee/EmdTest.java. Debemos eliminar varias cosas que no vamos a usar todavía, para empezar a limpiar la clase eliminaremos unos bloques e ítems de ejemplo que trae el mod para aprender a hacerlo nosotros desde cero, y vamos a refactorizar algunas cosas. Vamos a explicar detalladamente todos los cambios:

- Voy a eliminar todos los comentarios de código que no aportan nada, para hacer más legible (y algo más escueto) el código, para comentarlo en EMD. Podéis ver mis comentarios en el repositorio de github de cada post.

- Voy a cambiar la constante MODID a MOD_ID. Además el valor de este MOD_ID debe ser el nombre de nuestro mod en minúscula, y separado con guiones (una buena refencia es como se escriben variables en Python). En mi caso el mod se llama "EmdTest" así que el MOD_ID será "emd_test". Recuerdo que hay que cambiar todas las referencias de MODID a MOD_ID también.

- Voy a eliminar los objetos DeferredRegister<Block>, DeferredRegister<Item>, RegistryObject<Block> y RegistryObject<Item> debido a que son ítems y bloques de ejemplo que no nos servirán para nada. Recuerden eliminar todos los sitios donde aparezcan referencias a estos objetos.

- Voy a eliminar el método EmdTest#AddCreative ya que agrega esos bloques e ítems a una pestaña del creativo y quiero hacerlo más adelante en el tutorial. Recuerden eliminar todas las referencias al método.

Mi clase antes de los cambios se ve así:
Java:
package net.rudahee;

import com.mojang.logging.LogUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.world.item.BlockItem;
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.Material;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.CreativeModeTabEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.DeferredRegister;
import net.minecraftforge.registries.ForgeRegistries;
import net.minecraftforge.registries.RegistryObject;
import org.slf4j.Logger;

// The value here should match an entry in the META-INF/mods.toml file
@Mod(EmdTest.MODID)
public class EmdTest {

    // Define mod id in a common place for everything to reference
    public static final String MODID = "rudahee";
    // Directly reference a slf4j logger
    private static final Logger LOGGER = LogUtils.getLogger();
    // Create a Deferred Register to hold Blocks which will all be registered under the "rudahee" namespace
    public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID);
    // Create a Deferred Register to hold Items which will all be registered under the "rudahee" namespace
    public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID);

    // Creates a new Block with the id "rudahee:example_block", combining the namespace and path
    public static final RegistryObject<Block> EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.of(Material.STONE)));
    // Creates a new BlockItem with the id "rudahee:example_block", combining the namespace and path
    public static final RegistryObject<Item> EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties()));

    public EmdTest() {
        IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();

        // 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 ourselves for server and other game events we are interested in
        MinecraftForge.EVENT_BUS.register(this);

        // Register the item to a creative tab
        modEventBus.addListener(this::addCreative);
    }

    private void commonSetup(final FMLCommonSetupEvent event) {
        // Some common setup code
        LOGGER.info("HELLO FROM COMMON SETUP");
        LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT));
    }

    private void addCreative(CreativeModeTabEvent.BuildContents event) {
        if (event.getTab() == 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
    @Mod.EventBusSubscriber(modid = MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
    public static class ClientModEvents {

        @SubscribeEvent
        public static void onClientSetup(FMLClientSetupEvent event) {
            // Some client setup code
            LOGGER.info("HELLO FROM CLIENT SETUP");
            LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
        }
    }
}

Después de realizar todos los cambios mi código ha quedado así:
Java:
package net.rudahee;

import com.mojang.logging.LogUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.world.level.block.Blocks;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.server.ServerStartingEvent;
import net.minecraftforge.eventbus.api.IEventBus;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
import net.minecraftforge.registries.ForgeRegistries;
import org.slf4j.Logger;

// The value here should match an entry in the META-INF/mods.toml file
@Mod(EmdTest.MOD_ID)
public class EmdTest {

    public static final String MOD_ID = "emd_test";
    private static final Logger LOGGER = LogUtils.getLogger();

    public EmdTest() {
        IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();

        // Register the commonSetup method for modloading
        modEventBus.addListener(this::commonSetup);

        // Register ourselves for server and other game events we are interested in
        MinecraftForge.EVENT_BUS.register(this);
    }

    private void commonSetup(final FMLCommonSetupEvent event) {
        // Some common setup code
        LOGGER.info("HELLO FROM COMMON SETUP");
        LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT));
    }

    // 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
    @Mod.EventBusSubscriber(modid = MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
    public static class ClientModEvents {

        @SubscribeEvent
        public static void onClientSetup(FMLClientSetupEvent event) {
            // Some client setup code
            LOGGER.info("HELLO FROM CLIENT SETUP");
            LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
        }
    }
}

- En el archivo build.gradle debemos cambiar dos valores principalmente. y despues todas las referencias a nuestro mod_id
Nota: El código de gradle es Groovy, pero no existe esa opcion
Java:
plugins {
    id 'net.minecraftforge.gradle' version '5.1.+'
}


group = 'net'                          // El grupo debe incluir el archiveBaseName de la línea 10.
version = '0.0.1-1.19.4'

java {
    archivesBaseName = 'rudahee'     // Este debe ser nuestro mod_id
    toolchain.languageVersion = JavaLanguageVersion.of(17)
}

El resultado final sería:
Nota: El código de Gradle es Groovy, pero no existe esa opción
Java:
plugins {
    id 'net.minecraftforge.gradle' version '5.1.+'
}


group = 'net.rudahee'
version = '0.0.1-1.19.4'

java {
    archivesBaseName = 'emd_test'
    toolchain.languageVersion = JavaLanguageVersion.of(17)
}

- En el archivo src/main/resources/META-INF/mods.toml vamos a cambiar la línea 18: modId = "NOMBRE", para poner el MOD_ID que hemos especificado antes. En mi caso quedaría modId = "emd_test". Debemos cambiar las referencias en las líneas 48 y 60 de este mismo archivo, antes serían dependencies.NOMBRE; ahora en mi caso deben ser dependencies.emd_test.

- En el archivo settings.gradle debemos localizar la línea 8, donde pone: rootProject.name = NOMBRE y sustituir ese nombre por vuestro mod_id (recuerdo que debe ser el nombre del mod en minúsculas y con guiones). En mi caso la línea va a quedar así: rootProject.name = 'emd_test'



Repositorio de Github en este punto: EMDModTutorial-1.19.4 - Commit: 59917c1f30ad

Archivos modificados:
- EmdTest.java
- mods.toml
- build.gradle
- settings.gradle
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

2. Ítems y bloques​

En esta sección vamos a crear unos nuevos ítems y bloques. Empezaremos por lo más sencillo, un par de minerales nuevos similares al hierro o al diamante. Crearemos los bloques de estos ítems, los agregaremos a las pestañas del creativo e incluso crearemos nuestra propia pestaña.

Vamos a empezar poco a poco, y para ello lo principal es empezar desde lo más básico, un ítem que no hace nada.



2.1 Creando los primeros ítems

Vamos a empezar creando los ítems para unos metales nuevos, imitando su funcionamiento en Minecraft: Aluminio (del que crearemos los materiales en bruto, pepitas y lingotes) y el Ettmetal (del que crearemos pepitas y la propia gema, como un diamante con pepitas).

Para eso vamos a realizar un "registro" a través de un objeto DeferredRegister<>, desde el cual lo inyectaremos al juego. Antes de comenzar a registrar ítems, vamos a crear un par de packages y classes que por ahora dejaremos vacías, para tenerlo todo bien organizado.

El código no es Java, pero es para ilustrar mejor la estructura.
Java:
- EmdTest.java  //nuestra única clase ahora.


Vamos a crear la siguiente estructura partiendo la carpeta src/.
El código no esJava, pero es para ilustrar mejor la estructura.
Java:
- data/        // Este package debe quedar vacío ahora mismo. Lo usaremos para la generación y gestión de la información.
- modules/         // Este package lo usaremos para la gestión de la lógica de negocio de nuestro mod.
     - events/        // Este package lo usaremos para la gestión de los distintos eventos de nuestro mod.
           - ModCreativeTabEvents.java
- setup/         // Este package lo usaremos para la gestión de todo lo que podemos inyectar en el juego mientras arranca.
    - Registration.java
    - registries/        // Este package lo usaremos para la gestión de los distintos registros que crearemos en el juego.
          - ModItemsRegister.java
          - ModBlockRegister.java
          - ModCreativeTabRegister.java
-EmdTest.java

Para entender mejor esta estructura, podéis mirar el propio repositorio de github a este punto del tutorial al final del post.

Con todo listo vamos a empezar a crear un ítem. Para ello, lo primero que vamos a crear es un objeto DeferredRegister<Item> que nos permitirá inyectar nuestros ítems en Minecraft. Esto lo haremos en la clase Registration.java. También agregaremos un pequeño método para inyectar esta constante al bus de Minecraft.
Java:
package net.rudahee.setup;

// Imports...

/*
*  CLASE: Registration.java
*/

public class Registration {
/*
*  Lo definimos como público y estático debido a que necesitamos acceder a esta constante cada vez que necesitemos agregar un ítem.
*  En el método ForgeRegistries#create le pasamos que queremos registrar ítems y que nuestro MOD_ID para que sepa qué mod los va a estar registrando.
*/
    public static final DeferredRegister<Item> ITEMS_DEFERRED_REGISTER = DeferredRegister.create(ForgeRegistries.ITEMS, EmdTest.MOD_ID);

/*
* Con este método que recibe un parámetro IEventBus le decimos en que bus debe registrar los ítems que vamos a crear a continuación.
*/
    public static void register(IEventBus modEventBus) {
        ITEMS_DEFERRED_REGISTER.register(modEventBus);
    }
}

Para terminar con esto solamente necesitamos una última cosa: Llamar a nuestro método desde nuestra clase principal EmdTest.java en el método EmdTest#EmdTest, pasándole el bus por parámetro al método. El método quedaría tal que así:
Java:
public EmdTest() {
    IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();

    // Register the commonSetup method for modloading
    modEventBus.addListener(this::commonSetup);

    //Debemos realizar esta llamada antes de la línea "MinecraftForge.EVENT_BUS.register(this)".
    Registration.register(modEventBus);

    // Register ourselves for server and other game events we are interested in
    MinecraftForge.EVENT_BUS.register(this);
}

Esto que acabamos de realizar es la inicialización de un registro, y es muy común al programar con Forge. Se usa para bloques, efectos de pociones, entidades y un sinfín de cosas más que iremos viendo en el curso.

Con esto realizado, solamente tenemos que crear nuestros ítems en la clase ModItemsRegister. Aunque para crearlos de forma más sencilla, primero vamos a crear un método en esa clase:
Java:
public static <T extends Item> RegistryObject<T> registerItem(String name, Supplier<T> itemSupplier) {
// Aquí llamamos a la instancia del ITEMS_DEFERED_REGISTER para registrar nuestros ítems con los valores que pasamos por parámetro:
// El nombre del ítem y un supplier que nos devuelva el item.
    return Registration.ITEMS_DEFERRED_REGISTER.register(name, itemSupplier);
}

Y ahora vamos a proceder a crear nuestros ítems en la misma clase, usando el método que acabamos de crear; la clase completa quedaría así:
Java:
package net.rudahee.setup.registries;

import net.minecraft.world.item.Item;
import net.minecraftforge.registries.RegistryObject;
import net.rudahee.setup.Registration;

import java.util.function.Supplier;

public class ModItemsRegister {

     // Creo unas propiedades comunes para todos los ítems, en este caso que se stackeen de 64 en 64.
    private static Item.Properties COMMON_ITEM_PROPERTIES = new Item.Properties().stacksTo(64);

    // Creo los ítems en unos objetos RegistryObject<Item> del que los obtendremos cuando lo necesitemos.
    // Usando nuestro método, usando lambdas vamos a crear nuevos ítems con propiedades comunes.
    public static RegistryObject<Item> ALUMINUM_INGOT;
    public static RegistryObject<Item> ALUMINUM_NUGGET;
    public static RegistryObject<Item> ALUMINUM_RAW;

    // En los ítems de ettmetal extiendo las propiedades comunes para agregarle una rareza (Esto cambia como el color del texto, como pasa con el huevo del dragón)
    public static RegistryObject<Item> ETTMETAL_NUGGET;
    public static RegistryObject<Item> ETTMETAL;


    public static void register() {
        ALUMINUM_INGOT = registerItem("aluminum_ingot", () -> new Item(COMMON_ITEM_PROPERTIES));
        ALUMINUM_NUGGET = registerItem("aluminum_nugget", () -> new Item(COMMON_ITEM_PROPERTIES));
        ALUMINUM_RAW = registerItem("aluminum_raw", () -> new Item(COMMON_ITEM_PROPERTIES));
        ETTMETAL_NUGGET = registerItem("ettmetal_nugget", () -> new Item(COMMON_ITEM_PROPERTIES.rarity(Rarity.RARE)));
        ETTMETAL = registerItem("ettmetal", () -> new Item(COMMON_ITEM_PROPERTIES.rarity(Rarity.RARE)));

    }

public static <T extends Item> RegistryObject<T> registerItem(String name, Supplier<T> itemSupplier) {
        // Aquí llamamos a la instancia del ITEMS_DEFERED_REGISTER para registrar nuestros ítems con los valores que pasamos por parámetro:
        // El nombre del ítem y un supplier que nos devuelva el item.
        return Registration.ITEMS_DEFERRED_REGISTER.register(name, itemSupplier);
    }
}

Con esto, tendremos nuestros ítems registrados, ahora vamos a por los bloques.



2.2 Creando los primeros bloques

Si algo bueno tiene la chapa que os he soltado arriba, es que hacer bloques es un proceso muy parecido a hacer ítems y ya tenemos la estructura de carpetas y clases montada y funcionando.

Empezamos con la clase Registration.java creamos otro DeferredRegister<> para bloques en este caso:
Java:
public class Registration {
    public static final DeferredRegister<Item> ITEMS_DEFERRED_REGISTER = DeferredRegister.create(ForgeRegistries.ITEMS, EmdTest.MOD_ID);
    public static final DeferredRegister<Block> BLOCKS_DEFERRED_REGISTER = DeferredRegister.create(ForgeRegistries.BLOCKS, EmdTest.MOD_ID);

    public static void register(IEventBus modEventBus) {
        ITEMS_DEFERRED_REGISTER.register(modEventBus);
        BLOCKS_DEFERRED_REGISTER.register(modEventBus);

        ModItemsRegister.register();
    }
}

Ahora vamos con nuestro ModBlockRegister.java. En este caso vamos a crear dos métodos auxiliares, uno para registrar un ítem y otro para registrar nuestros bloques, esto es debido a que los bloques son ítems cuando están en nuestro inventario, no pasan a ser un bloque hasta que no están colocados en el mundo. Por tanto, necesitamos registrar tanto un ítem como un bloque.
Java:
package net.rudahee.setup.registries;

// imports...

import java.util.function.Supplier;

public class ModBlockRegister {
 
    // Como antes, creamos un Item.Properties común para los ítems.
    private static final Item.Properties COMMON_BLOCK_ITEM_PROPERTIES = new Item.Properties().stacksTo(64);

    // Aquí estamos creando dos propiedades distintas para los bloques, uno para las menas de donde vamos a minar nuestros ítems
    // y otro para los bloques que podremos [I]craftear[/I] al unir 9 lingotes en la mesa de [I]crafteo[/I]. La diferencia entre ellos es que uno define en qué material nos basamos y el otro, en el sonido al picarlos.
    // Debemos definir también la resistencia contra explosiones, si se pueden picar con cualquier herramienta y el tiempo que tardan en picarse.

    private static final BlockBehaviour.Properties COMMON_BLOCK_ORE_PROPERTIES
            = BlockBehaviour.Properties.of(Material.STONE).explosionResistance(50).destroyTime(3).requiresCorrectToolForDrops().sound(SoundType.STONE);
    private static final BlockBehaviour.Properties COMMON_BLOCK_METAL_PROPERTIES
            = BlockBehaviour.Properties.of(Material.HEAVY_METAL).explosionResistance(50).destroyTime(3).requiresCorrectToolForDrops().sound(SoundType.METAL);

    // Hacemos nuestros registros como hacíamos con los ítems, a través de los métodos auxiliares que encontramos abajo.
    public static RegistryObject<Block> ALUMINUM_ORE;
    public static RegistryObject<Block> ETTMETAL_ORE;

    public static RegistryObject<Block> ALUMINUM_BLOCK;
    public static RegistryObject<Block> ETTMETAL_BLOCK;

    public static void register() {
        ALUMINUM_ORE = registerBlock("aluminum_ore", () -> new Block(COMMON_BLOCK_ORE_PROPERTIES), COMMON_BLOCK_ITEM_PROPERTIES);
        ETTMETAL_ORE = registerBlock("ettmetal_ore", () -> new Block(COMMON_BLOCK_ORE_PROPERTIES), COMMON_BLOCK_ITEM_PROPERTIES);
        ALUMINUM_BLOCK = registerBlock("aluminum_block", () -> new Block(COMMON_BLOCK_METAL_PROPERTIES), COMMON_BLOCK_ITEM_PROPERTIES);
        ETTMETAL_BLOCK = registerBlock("ettmetal_block", () -> new Block(COMMON_BLOCK_METAL_PROPERTIES), COMMON_BLOCK_ITEM_PROPERTIES);

    }


    public static <T extends Block> RegistryObject<T> registerBlockNoItem(String name, Supplier<T> blockSupplier) {
 
          // En esta línea inyectamos el código al bus a través del DEFERRED_REGISTER de bloques que hemos creado previamente.
          return Registration.BLOCKS_DEFERRED_REGISTER.register(name, blockSupplier);
    }

     // Con este método auxiliar, recibimos 3 parámetros, el nombre del bloque y del ítem, el supplier del bloque y las propiedades del ítem
    public static <T extends Block> RegistryObject<T> registerBlock(String name, Supplier<T> blockSupplier, Item.Properties properties) {
 
         // Llamamos al registro del bloque en sí.
        RegistryObject<T> blockRegistered = registerBlockNoItem(name, blockSupplier);

        // Registramos el ítem tal y como vimos en el punto 2.1
        Registration.ITEMS_DEFERRED_REGISTER.register(name, () -> (new BlockItem(blockRegistered.get(), properties)));
 
        return blockRegistered;

    }
}

Con esto ya tendremos bloques que se pueden colocar, que necesitan picarse con una herramienta concreta, que resisten explosiones y todas las propiedades base de los bloques, además de los ítems vinculados al bloque se stackean de 64 en 64.



2.3 Creando una pestaña en el Creativo

Para crear una pestaña de Creativo no se usa un DeferredRegister, sino que se usa un evento, pero suelo incluir este evento en el paquete registries ya que así es mucho mas sencillo de localizar, debido a que lo que hacemos es más parecido a un registro que a crear una lógica de negocio de un evento (y no voy a explicar los eventos en este punto). Debido a esto, voy a referirme a esto como "Registrar una pestaña de creativo" aunque formalmente no sea un registro, sino un evento.

Para registrar la pestaña vamos a usar solamente la clase ModCreativeTabRegister, donde vamos a crear un único método en el que registraremos la pestaña del creativo y una única constante donde la almacenaremos:

Java:
package net.rudahee.setup.registries;

// imports...

// Al ser un evento, tenemos que poner esta línea; lo veremos próximamente en el tutorial.
@Mod.EventBusSubscriber(modid = EmdTest.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class ModCreativeTabRegister {

    // Creamos un objeto del tipo CreativeModeTab, que contendrá la información de nuestra pestaña.
    public static CreativeModeTab EMD_TEST_TAB;

    // El objeto ResourceLocation es algo bastante común cuando programamos en Forge,
    // sería el equivalente a poner un String como este "emd_test:creative_tab",
    // que es el nombre interno de nuestra tab para Forge. Para simplificar esto, normalmente se crea un
    // ResourceLocation que vamos a usar cada vez que necesitemos crear una pestaña para nuestro mod.
    private static final ResourceLocation LOCATION = new ResourceLocation(EmdTest.MOD_ID, "creative_tab");

    // Al ser un evento, tenemos que poner @SuscribeEvent, lo veremos próximamente en el tutorial.
    @SubscribeEvent
    public static void registerTabs(CreativeModeTabEvent.Register event) {
        //Asignamos a nuestra variable el resultado del evento de registro, necesitamos pasar nuestro ResourceLocation y una funcion anónima (lambda)
        // que tomará el valor de un CreativeModeTab.Builder, en este caso solamente vamos a incluir un título a través de un Component
        // (que es el equivalente a un System.out.println de Minecraft) y le decimos que nos construya la pestaña a través del método build()
        EMD_TEST_TAB = event.registerCreativeModeTab(LOCATION, builder -> builder.title(Component.literal("Emd Test Mod")).build());
    }

}

Una vez registrada la pestaña con esta clase, vamos a dirigirnos a ModCreativeTabEvents, donde vamos a incluir nuestros objetos a nuestra pestaña a través de un evento.

Para esto vamos a crear un metodo estatico que recibirá el evento CreativeModeTabEvent.BuildContents por parámetro. Este evento se va a disparar con cada pestaña, y nosotros solamente necesitamos detectar que es nuestra pestaña e incluir nuestros ítems a través de los métodos del propio objeto del evento que tenemos como parámetro del método.
Java:
package net.rudahee.modules.events;

import net.minecraftforge.event.CreativeModeTabEvent;
import net.rudahee.setup.registries.ModBlockRegister;
import net.rudahee.setup.registries.ModCreativeTabRegister;
import net.rudahee.setup.registries.ModItemsRegister;

public class ModCreativeTabEvents {

    // Como se explica arriba, recibimos un evento del tipo CreativeModeTabEvent.BuildContents
    public static void addContentsToModTab(CreativeModeTabEvent.BuildContents event) {
 
        // Si la pestaña es nuestra EMD_TEST_TAB, aquí podríais usar las pestañas de Minecraft
        // comparando con las constantes de la clase CreativeModeTabs.
        if (event.getTab().equals(ModCreativeTabRegister.EMD_TEST_TAB)) {

            // Con el método accept le decimos que acepte el ítem para esta pestaña y que por tanto
            // lo muestre. El orden en el que se definan aquí será el orden en las pestañas.
            event.accept(ModItemsRegister.ALUMINUM_INGOT);
            event.accept(ModItemsRegister.ALUMINUM_NUGGET);
            event.accept(ModItemsRegister.ALUMINUM_RAW);
            event.accept(ModItemsRegister.ETTMETAL);
            event.accept(ModItemsRegister.ETTMETAL_NUGGET);
            event.accept(ModBlockRegister.ALUMINUM_ORE);
            event.accept(ModBlockRegister.ALUMINUM_BLOCK);
            event.accept(ModBlockRegister.ETTMETAL_ORE);
            event.accept(ModBlockRegister.ETTMETAL_BLOCK);
        }
    }
}

Y para finalizar, este evento necesita ser llamado desde nuestra clase principal, agregando un Listener al bus (repito, se explicará en el apartado dedicado a eventos). Para agregarlo es agregar una línea en el método principal de nuestra clase EmdTest.java.

Java:
 public EmdTest() {
        IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus();

        modEventBus.addListener(this::commonSetup);

        // Agregamos el listener ANTES de los register(), justo debajo del resto de listeners.
        modEventBus.addListener(ModCreativeTabEvents::addContentsToModTab);

        Registration.register(modEventBus);

        MinecraftForge.EVENT_BUS.register(this);
    }

Con esto tenemos tendremos nuestros ítems en nuestra propia pestaña del creativo. En el siguiente punto les asignaremos texturas, drops, etc... y podremos verlos dentro del juego, finalmente.



Repositorio de Github en este punto: EMDModTutorial-1.19.4 - Commit: 092cd3997d43e6e

Archivos modificados:
- EmdTest.java
- setup/Registration.java
- setup/registries/ModBlockRegister.java
- setup/registries/ModItemsRegister.java
- setup/registries/ModCreativeTabRegister.java
- modules/events/ModCreativeTabEvents.java
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

3. Providers: Generación automática de archivos JSON​


En Minecraft, para definir ciertos comportamientos como el drop de un bloque o un enemigo, la textura a usar por un ítem o implementar un nuevo logro se necesita un JSON donde este contenida esa información.

Esto se hace a través de los resource packs, en estos resource packs normalmente se incluyen archivos JSON y las imágenes de las texturas que queramos sustituir, para casi cualquier mod es necesario un resource pack que incluya estos archivos para las entidades, ítems, bloques, etc. que hayamos desarrollado.

Forge nos da una solución para no tener que crear nuestros resource packs a mano, y estos son los providers, que nos permite generar esos archivos JSON de forma automática. Para esto vamos a extender ciertas clases de Forge para generar los archivos individuales y usaremos Gradle para generar el resource pack e incluirlo en el mod automáticamente.



3.1 Aplicando texturas

Para aplicar texturas necesitáis crearlas con algún editor de imágenes (a ser posible que maneje fondos transparentes) y esto es algo con lo que no os puedo ayudar. En este caso usaré unas texturas de uno de los mods de nuestro equipo de desarrollo. El autor de las texturas es Farck y podéis verlas en detalle en el repositorio de github.

Para empezar vamos a crear un nuevo paquete dentro de data y lo vamos a llamar providers y vamos a crear dos clases ModItemModelProvider.java y ModBlockStateProvider.java

Vamos a empezar aplicando texturas a los ítems desde la clase ModItemModelProvider.java y para eso necesitamos extender la clase ItemModelProvider e implementar el constructor y el método ItemModelProvider#registerModels.

Después de eso usaremos métodos que heredamos de la clase padre para decirle el formato del JSON a generar para cada ítem, aunque vamos a usar un pequeño método propio para facilitarnos la vida a la hora de crear varios ítems.

Veamos el resultado final de esta clase:
Java:
package net.rudahee.data.providers;

//imports...

public class ModItemModelProvider extends ItemModelProvider {

    // Constante para decirle el tipo de item que vamos a generar, en este caso es el item "por defecto".
    private final ModelFile MODEL_FILE = getExistingFile(mcLoc("item/generated"));

    // Constructor por defecto.
    public ModItemModelProvider(DataGenerator generator, ExistingFileHelper existingFileHelper) {
        super(generator.getPackOutput(), EmdTest.MOD_ID, existingFileHelper);
    }

    // Metodo que debemos implementar para que nos genere los distintos JSONs.
    @Override
    protected void registerModels() {
 
        // El primer parametro es el tipo de item, el segundo es donde se generará el JSON
        // y el tercero es donde hemos colocado la textura.
        // LLamadas a nuestro metodo que debe devolver un objeto de tipo ItemModelBuilder.
        builder(MODEL_FILE,"item/aluminum_nugget", "item/aluminum_nugget");
        builder(MODEL_FILE,"item/aluminum_ingot", "item/aluminum_ingot");
        builder(MODEL_FILE,"item/aluminum_raw", "item/aluminum_raw");


        builder(MODEL_FILE,"item/ettmetal_nugget", "item/ettmetal_nugget");
        builder(MODEL_FILE,"item/ettmetal", "item/ettmetal_ingot");

    }

    // Metodo auxiliar para obtener el builder, y construir el json con los datos dados por parametros.
    // el metodo super#getBuilder nos da el un builder que estamos usando para decirle el tipo de item
    // que vamos a generar y donde se va a encontrar la textura.
    private ItemModelBuilder builder(ModelFile itemGenerated, String outPath, String texturePath) {
        return getBuilder(outPath).parent(itemGenerated).texture("layer0", texturePath);
    }
}

Esto que hemos realizado en los ítems es básicamente lo mismo con los bloques, solo cambian el nombre de los métodos y la clase de la que extendemos. Así que en este caso vamos a extender de la clase BlockStateProvider. El constructor de nuestra clase será exactamente el mismo y vamos a implementar el método BlockStateProvider#registerStatesAndModel, aunque en este caso no vamos a crear ningún método propio ya que en la ultima versión se implementaron mejoras que hace el proceso mucho mas simple y limpio.

Así queda la clase tras implementar todos estos cambios:
Java:
package net.rudahee.data.providers;

//imports...

public class ModBlockStateProvider extends BlockStateProvider {

    // Nuestro constructor, muy parecido al de los items.
    public ModBlockStateProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
        super(output, EmdTest.MOD_ID, existingFileHelper);
    }

    // Metodo que nos generará los JSON.
    @Override
    protected void registerStatesAndModels() {

        // En este caso tenemos dos metodos en la clase padre, SimpleBlock y SimpleBlockItem.
        // SimpleBlock genera el json para el bloque automaticamente solo con pasarle el bloque.
        // SimpleBlockItem genera el json para el item relacionado al bloque y en este caso debemos pasarle
        // la localizacion de la textura a traves de un objeto ModelFile, en este caso lo haré unchecked para
        // ahorrarnos algunas configuraciones extra, si no lo encuentra, simplemente no se mostrara en el juego
        // sin mostrar ningun tipo de error.
        simpleBlock(ModBlockRegister.ALUMINUM_BLOCK.get());
        simpleBlockItem(ModBlockRegister.ALUMINUM_BLOCK.get(), new ModelFile.UncheckedModelFile(modLoc("block/aluminum_block")));
 
        simpleBlock(ModBlockRegister.ALUMINUM_ORE.get());
        simpleBlockItem(ModBlockRegister.ALUMINUM_ORE.get(), new ModelFile.UncheckedModelFile(modLoc("block/aluminum_ore")));

        simpleBlock(ModBlockRegister.ETTMETAL_BLOCK.get());
        simpleBlockItem(ModBlockRegister.ETTMETAL_BLOCK.get(), new ModelFile.UncheckedModelFile(modLoc("block/ettmetal_block")));
 
        simpleBlock(ModBlockRegister.ETTMETAL_ORE.get());
        simpleBlockItem(ModBlockRegister.ETTMETAL_ORE.get(), new ModelFile.UncheckedModelFile(modLoc("block/ettmetal_ore")));
    }
}

Ahora debemos registrar los providers[ICODE] en nuestra paquete [ICODE]setup. En este caso debemos usar un evento también (que como he comentado anteriormente se verán en el punto 5 del tutorial. Para hacer esto vamos a crear la clase [IDATA]DataGeneration.java[/IDATA] dentro del paquete [IDATA]setup[/IDATA] y suscribirnos a un evento como hicimos en el punto anterior, y pasarle al evento los generadores que hemos creado.

La clase finalmente nos debería quedar así:
Java:
package net.rudahee.setup;

//imports...

// Esta etiqueta es así debido a que es un evento, lo explicaré en el punto 5.
@Mod.EventBusSubscriber(modid = EmdTest.MOD_ID, bus = Mod.EventBusSubscriber.Bus.MOD)
public class DataGeneration {

     // Esta etiqueta debe estar ahí debido a que es un evento, lo explicaré en el punto 5.
    @SubscribeEvent
    public static void gatherData(GatherDataEvent event) {
        // Obtenemos todo lo necesario del evento.
        DataGenerator gen = event.getGenerator();
        PackOutput packOutput = gen.getPackOutput();
        ExistingFileHelper existingFileHelper = event.getExistingFileHelper();
 
        // Le pasamos los generadores al evento con el método DataGenerator#addProvider creando una nueva instancia del generador.
        gen.addProvider(event.includeServer(), new ModBlockStateProvider(packOutput, existingFileHelper));
        gen.addProvider(event.includeServer(), new ModItemModelProvider(gen, existingFileHelper));

    }
}

Y para finalizar solo debemos dejar las texturas en la ruta especifica. Es muy importante respetar la estructura de carpetas y los nombres de los archivos segun lo hayamos especificado en los generadores.
Para ello vamos a crear esta estructura de carpetas:
- resources/assets/MOD_ID/textures/

Tened en cuenta que en mi caso mi id de mod es emd_test, dentro de esta estructura debemos crear dos carpetas item y block y dentro de cada una de estas carpetas dejar las texturas en formato PNG.

En mi caso, la estructura ha quedado así:
laRrjo1.png



3.2 Creando las recetas

Para crear las recetas, de forma muy parecida a como realizamos las texturas, vamos a crear una clase en nuestro paquete providers y extendemos una clase de Forge. Así que no nos vamos a extender mucho explicando como es que funciona y vamos a pasar directamente a las clases.

En este caso vamos a extender la clase RecipeProvider y vamos a implementar el constructor y el método RecipeProvider#buildRecipes. Vamos a ver la clase directamente:
Java:
package net.rudahee.data.providers;

//imports....

public class ModRecipeProvider extends RecipeProvider {

    // Nuestro constructor por defecto.
    public ModRecipeProvider(DataGenerator generator) {
        super(generator.getPackOutput());
    }

    // El metodo que estamos obligados a implementar. Aqui vamos a crear nuestras recetas.
    protected void buildRecipes(@NotNull Consumer<FinishedRecipe> recipesConsumer) {

        // Vamos a crear primero las recetas de un bloque a 9 items. Para ello necesitamos una receta "sin forma",
        // ya que no nos importa el slot donde se coloque el bloque, el resultado siempre será el mismo, 9 lingotes.
        // Para hacer esto vamos a usar ShapelessRecipeBuilder#shapeless.
        // Los parametros de este metodo son: Tipo de receta, item resultante, cantidad del item resultante.
        ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ModItemsRegister.ALUMINUM_INGOT.get(), 9)
                // Tras eso, ponemos los items y bloques necesarios para la receta, en este caso solo el bloque.
                .requires(ModBlockRegister.ALUMINUM_BLOCK.get())
                // Ahora decimos cuando vamos a aprender esta receta en el libro de recetas de Minecraft,
                // en este caso, cuando tengas en el inventario un lingote de aluminio.
                .unlockedBy("has_item", has(ModItemsRegister.ALUMINUM_INGOT.get()))
                // Y para finalizar la guardamos con un nombre descriptivo. En mi caso siempre hago este tipo de nombres
                // Para evitar que se puedan generar errores (al coincidir con recetas de otros mods, o con otras recetas propias).
                .save(recipesConsumer, new ResourceLocation("aluminum_block_to_aluminum_ingot_recipe"));

        // Hacemos lo mismo que arriba, pero este caso para el ettmetal.
        ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ModItemsRegister.ETTMETAL.get(), 9)
                .requires(ModBlockRegister.ETTMETAL_BLOCK.get())
                .unlockedBy("has_item", has(ModItemsRegister.ETTMETAL.get()))
                .save(recipesConsumer, new ResourceLocation("ettmetal_block_to_ettmetal_ingot_recipe"));

        // Ahora vamos a plantear una receta donde hay un orden los items y los slots, una receta con una "forma" especifica.
        // Para eso usamos un #ShapedRecipeBuilder#shaped, para convertir 9 ingots en un bloque.
        // Los parametros de este metodo son: Tipo de receta, item resultante, cantidad del item resultante.
        ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, ModBlockRegister.ETTMETAL_BLOCK.get())
                // Con el metodo ShapedRecipeBuilder#define, definimos un caracter para representar el item en la receta.
                // Podemos llamar al define hasta 9 veces, una por slot de la mesa de crafteo. El caracter puede ser cualquier caracter imprimible.
                .define('#', ModItemsRegister.ETTMETAL.get())
                // El conjunto de tres llamadas a ShapedRecipeBuilder#pattern definen una cuadricula 3x3 que representa
                // la mesa de crafteo, y con ello podemos definir la receta, en nuestro caso son 3x3 ingots de ettmetal.
                .pattern("###")
                .pattern("###")
                .pattern("###")
                // Ahora decimos cuando vamos a aprender esta receta en el libro de recetas de Minecraft,
                // en este caso, cuando tengas en el inventario un lingote de ettmetal.
                .unlockedBy("has_block", has(ModItemsRegister.ETTMETAL.get()))
                // Y para finalizar la guardamos con un nombre descriptivo. En mi caso siempre hago este tipo de nombres
                // Para evitar que se puedan generar errores (al coincidir con recetas de otros mods, o con otras recetas propias).
                .save(recipesConsumer, new ResourceLocation("ettmetal_ingot_to_ettmetal_block"));

        // Esta receta funciona exactamente igual de la de arriba.
        ShapedRecipeBuilder.shaped(RecipeCategory.BUILDING_BLOCKS, ModBlockRegister.ALUMINUM_BLOCK.get())
                .define('#', ModItemsRegister.ALUMINUM_INGOT.get())
                .pattern("###")
                .pattern("###")
                .pattern("###")
                .unlockedBy("has_block", has(ModItemsRegister.ALUMINUM_INGOT.get()))
                .save(recipesConsumer, new ResourceLocation("aluminum_ingot_to_aluminum_block"));

        // Las dos siguientes recetas son sin forma, exactamente igual que las dos primeras.
        // Son para transformar lingotes a pepitas.
        ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ModItemsRegister.ALUMINUM_NUGGET.get(), 9)
                .requires(ModItemsRegister.ALUMINUM_INGOT.get())
                .unlockedBy("has_item", has(ModItemsRegister.ALUMINUM_INGOT.get()))
                .save(recipesConsumer, new ResourceLocation("aluminum_ingot_to_aluminum_nugget_recipe"));

        ShapelessRecipeBuilder.shapeless(RecipeCategory.MISC, ModItemsRegister.ALUMINUM_INGOT.get(), 9)
                .requires(ModItemsRegister.ETTMETAL.get())
                .unlockedBy("has_item", has(ModItemsRegister.ETTMETAL.get()))
                .save(recipesConsumer, new ResourceLocation("ettmetal_ingot_to_ettmetal_nugget_recipe"));

        // Las dos siguientes recetas son con forma especifica.
        // Son para transformar pepitas a lingotes.
        ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItemsRegister.ALUMINUM_INGOT.get())
                .define('#', ModItemsRegister.ALUMINUM_NUGGET.get())
                .pattern("###")
                .pattern("###")
                .pattern("###")
                .unlockedBy("has_item", has(ModItemsRegister.ALUMINUM_NUGGET.get()))
                .save(recipesConsumer, new ResourceLocation("aluminum_nugget_to_aluminum_ingot"));

        ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItemsRegister.ETTMETAL.get())
                .define('#', ModItemsRegister.ETTMETAL_NUGGET.get())
                .pattern("###")
                .pattern("###")
                .pattern("###")
                .unlockedBy("has_item", has(ModItemsRegister.ETTMETAL_NUGGET.get()))
                .save(recipesConsumer, new ResourceLocation("ettmetal_nugget_to_ettmetal_ingot"));

        // Estas recetas son para los distintos hornos y funcionan de forma muy similar.
        // Reciben los mismos parametros: Ingrediente a quemar (se obtiene con Ingredient#of y pasandole el item), tipo de receta,
        // el resultado de la receta, tiempo de cocinado, y experiencia recibida por realizar la receta.
        SimpleCookingRecipeBuilder.blasting(Ingredient.of(ModItemsRegister.ALUMINUM_RAW.get()), RecipeCategory.MISC, ModItemsRegister.ALUMINUM_INGOT.get(), 1f, 100)
                // Como antes, decidimos cuando desbloqueamos la receta, y le ponemos un nombre al JSON.
                .unlockedBy("has_item", has(ModItemsRegister.ALUMINUM_RAW.get()))
                .save(recipesConsumer, new ResourceLocation("cooking_aluminum_raw_to_ingot"));

        SimpleCookingRecipeBuilder.smelting(Ingredient.of(ModItemsRegister.ALUMINUM_RAW.get()), RecipeCategory.MISC, ModItemsRegister.ALUMINUM_INGOT.get(), 0.5f, 200)
                .unlockedBy("has_item", has(ModItemsRegister.ALUMINUM_RAW.get()))
                .save(recipesConsumer, new ResourceLocation("blasting_aluminum_raw_to_ingot"));
    }

}

Y para finalizar, hacemos una llamada al constructor de nuestro nuevo provider en nuestra clase DataGeneration.java. El método queda tal que así:
Java:
    @SubscribeEvent
    public static void gatherData(GatherDataEvent event) {
        DataGenerator gen = event.getGenerator();
        PackOutput packOutput = gen.getPackOutput();
        ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

        gen.addProvider(event.includeServer(), new ModBlockStateProvider(packOutput, existingFileHelper));
        gen.addProvider(event.includeServer(), new ModItemModelProvider(gen, existingFileHelper));
 
        // Nuevo provider.
        gen.addProvider(event.includeServer(), new ModRecipeProvider(gen));

    }

Con esto tendríamos nuestras recetas listas para generar.



3.3 Configurando los tablas de loot

Como en los puntos anteriores, vamos a realizar el mismo procedimiento, crear una clase en el paquete providers llamada ModLootTableProvier.java que debe heredar de LootTableProvider[/ICODE].

Como antes vamos a implementar el método que nos pide la clase, y el constructor. Después vamos a crear un par de métodos auxiliares para cuando lo picamos con toque de seda o cuando lo picamos con un pico normal y corriente tenga o no el encantamiento fortuna.

La clase final en mi caso queda así:
Java:
package net.rudahee.data.providers;

//imports...

public class ModLootTableSubProvider implements LootTableSubProvider {


    @Override
    public void generate(BiConsumer<ResourceLocation, LootTable.Builder> writer) {


        // Agregamos los bloques sencillos.
        addSimpleBlock(writer, ForgeRegistries.BLOCKS.getKey(ModBlockRegister.ALUMINUM_BLOCK.get()).getPath(), ModBlockRegister.ALUMINUM_BLOCK.get());
        addSimpleBlock(writer, ForgeRegistries.BLOCKS.getKey(ModBlockRegister.ETTMETAL_BLOCK.get()).getPath(), ModBlockRegister.ETTMETAL_BLOCK.get());

        // Agregamos las menas de metal.
        addComplexBlock(writer, ForgeRegistries.BLOCKS.getKey(ModBlockRegister.ALUMINUM_ORE.get()).getPath(),
                ModBlockRegister.ALUMINUM_ORE.get(), ModItemsRegister.ALUMINUM_RAW.get(), 1, 3, 2);
        addComplexBlock(writer, ForgeRegistries.BLOCKS.getKey(ModBlockRegister.ETTMETAL_ORE.get()).getPath(),
                ModBlockRegister.ETTMETAL_ORE.get(), ModItemsRegister.ETTMETAL.get(), 1, 1, 3);
    }


    /*
     * Esto lo vamos a usar para implementar bloques muy sencillos, como un bloque de aluminio, que da igual como lo piques, siempre te dará 1 unidad.
     */
    protected static void addSimpleBlock(BiConsumer<ResourceLocation, LootTable.Builder> writer, String name, Block block) {
        // En los parametros a LootPool#name le pasamos el nombre de la loot table, en LootPool#setRolls le decimos la cantidad, en este caso una constante de una unidad.
        LootPool.Builder builder = LootPool.lootPool().name(name).setRolls(ConstantValue.exactly(1)).add(LootItem.lootTableItem(block));

        // Despues solo indicamos donde se va a escribir la loot table, en este caso dentro de la carpeta loot_tables/blocks
        writer.accept(new ResourceLocation(EmdTest.MOD_ID, "blocks/" + name), LootTable.lootTable().withPool(builder));
    }

    protected static void addComplexBlock(BiConsumer<ResourceLocation, LootTable.Builder> writer, String name, Block block, Item lootItem, float min, float max, int bonus) {
        LootPool.Builder builder = LootPool
                .lootPool()
                // En el nombre, como arriba, será el nombre de la propia lootTable
                .name(name)
                // Por defecto debe dropear de X a Y items dependiendo del valor pasado por parametro.
                .setRolls(UniformGenerator.between(min, max))
                // Alternativamente creamos otro loot table si el jugador tiene el encantamiento de toque de seda.
                .add(AlternativesEntry.alternatives(LootItem
                        .lootTableItem(block)
                        .when(MatchTool.toolMatches(ItemPredicate.Builder
                                .item()
                                // Si tiene toque de seda, devolvemos una sola unidad.
                                .hasEnchantment(new EnchantmentPredicate(Enchantments.SILK_TOUCH,
                                        MinMaxBounds.Ints.atLeast(1))))), LootItem
                        .lootTableItem(lootItem)
                        // Aqui volvemos a comentar entre que valores se encuentra el drop
                        .apply(SetItemCountFunction.setCount(UniformGenerator.between(min, max)))
                        // Y en cuantas unidades mas se puede incrementar con fortuna.
                        .apply(ApplyBonusCount.addUniformBonusCount(Enchantments.BLOCK_FORTUNE, bonus))
                        // Y que el item se dropee si hay una explosion.
                        .apply(ApplyExplosionDecay.explosionDecay())));


        writer.accept(new ResourceLocation(EmdTest.MOD_ID, "blocks/" + name), LootTable.lootTable().withPool(builder));
    }
}
Y para finalizar, hacemos una llamada al constructor de nuestro nuevo provider en nuestra clase DataGeneration.java. El método queda tal que así:
Java:
@SubscribeEvent
public static void gatherData(GatherDataEvent event) {
    DataGenerator gen = event.getGenerator();
    PackOutput packOutput = gen.getPackOutput();
    ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

    gen.addProvider(event.includeServer(), new ModBlockStateProvider(packOutput, existingFileHelper));
    gen.addProvider(event.includeServer(), new ModItemModelProvider(gen, existingFileHelper));
    gen.addProvider(event.includeServer(), new ModRecipeProvider(gen));
    gen.addProvider(event.includeServer(), new LootTableProvider(packOutput, Collections.emptySet(),
    List.of(new LootTableProvider.SubProviderEntry(ModLootTableSubProvider::new, LootContextParamSets.BLOCK))));

}

Ahora debemos especificar que nuestros bloques se deben picar con herramientas de hierro, y que debe ser picados con un pico. Vamos a crear un nuevo provider en nuestro paquete, en este caso vamos a heredar de BlockTagsProvider. Como de costumbre implementaremos un constructor y un metodo auxiliar y para finalizar simplemente lo llamaremos en DataGeneration.java. Os dejo la clase directamente:
Java:
package net.rudahee.data.providers;

// imports....

import java.util.concurrent.CompletableFuture;

public class ModBlockTagProvider extends BlockTagsProvider {

    // Como de costumbre  creamos el constructor.
    public ModBlockTagProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> lookupProvider, @Nullable ExistingFileHelper existingFileHelper) {
        super(output, lookupProvider, EmdTest.MOD_ID, existingFileHelper);
    }
 
     // Método principal desde el que llamamos a nuestro método auxiliar con la lista de bloques que contendrán los tags.
    @Override
    protected void addTags(HolderLookup.Provider provider) {
        makePickaxeMineable(ModBlockRegister.ALUMINUM_BLOCK.get(), ModBlockRegister.ETTMETAL_BLOCK.get(),
                ModBlockRegister.ALUMINUM_ORE.get(), ModBlockRegister.ETTMETAL_ORE.get());
    }

    private void makePickaxeMineable(Block... items) {
        // En este caso, el ResourceLocation es de minecraft, ya que son tags de Minecraft vanilla. y despues simplemente pasamos la lista de items.
        tag(BlockTags.create(new ResourceLocation("minecraft", "mineable/pickaxe"))).replace(false).add(items);
        tag(BlockTags.create(new ResourceLocation("minecraft", "needs_iron_tool"))).replace(false).add(items);
    }
}

Con esto tendríamos nuestros drops de bloques listos para generar.



3.4 Usando traducciones

Para usar traducciones para nuestros ítems y bloques, debemos realizar una clase por idioma, en nuestro caso vamos a hacer una para todas las variantes inglesas y una para todas las variantes españolas. El procedimiento va a ser idéntico a los demás: Crear un provider y extender una clase de la implementaremos el constructor y un método, para no extenderme en exceso os dejo directamente las clases.

Clase para traducir al español:
Java:
package net.rudahee.data.providers;

import net.minecraft.data.PackOutput;
import net.minecraftforge.common.data.LanguageProvider;
import net.rudahee.EmdTest;

public class ModSpanishLanguageProvider extends LanguageProvider {

    public ModSpanishLanguageProvider(PackOutput output, String locale) {
        super(output, EmdTest.MOD_ID, locale);
    }

    @Override
    protected void addTranslations() {
 
        // El primer parámetro suele tener el formato de "tipo_de_objeto"."mod_id"."nombre_de_registro". Si no ponemos una traducción,
        // se verá el nombre separado por puntos en el juego y podemos copiarlos y pegarlos, el segundo parámetro es el nombre que le queremos poner.
        add("block.emd_test.aluminum_block", "Bloque de aluminio");
        add("block.emd_test.ettmetal_block", "Bloque de ettmetal");

        add("block.emd_test.ettmetal_ore", "Mena de ettmetal");
        add("block.emd_test.aluminum_ore", "Mena de aluminio");

        add("item.emd_test.aluminum_ingot", "Lingote de aluminio");
        add("item.emd_test.ettmetal", "Ettmetal");

        add("item.emd_test.ettmetal_nugget", "Pepita de ettmetal");
        add("item.emd_test.aluminum_nugget", "Pepita de aluminio");

        add("item.emd_test.aluminum_raw", "Aluminio en crudo");
    }
}

Y para traducir al ingles, la clase es prácticamente la misma pero con el segundo parámetro de cada método LanguageProvide#add en otro idioma.

La clase queda así:
Java:
package net.rudahee.data.providers;

import net.minecraft.data.PackOutput;
import net.minecraftforge.common.data.LanguageProvider;
import net.rudahee.EmdTest;

public class ModEnglishLanguageProvider extends LanguageProvider {

    public ModEnglishLanguageProvider(PackOutput output, String locale) {
        super(output, EmdTest.MOD_ID, locale);
    }

    @Override
    protected void addTranslations() {
        add("block.emd_test.aluminum_block", "Block of Aluminum ");
        add("block.emd_test.ettmetal_block", "Block of Ettmetal");
 
        add("block.emd_test.ettmetal_ore", "Ettmetal ore");
        add("block.emd_test.aluminum_ore", "Aluminum ore");

        add("item.emd_test.aluminum_ingot", "Aluminum ingot");
        add("item.emd_test.ettmetal", "Ettmetal");

        add("item.emd_test.ettmetal_nugget", "Ettmetal nugget");
        add("item.emd_test.aluminum_nugget", "Aluminum nugget");

        add("item.emd_test.aluminum_raw", "Raw of Aluminum");
    }
}

Y finalizamos agregando estas clases a nuestro DataGeneration.java como de costumbre:

Java:
@SubscribeEvent
public static void gatherData(GatherDataEvent event) {
    DataGenerator gen = event.getGenerator();
    PackOutput packOutput = gen.getPackOutput();
    ExistingFileHelper existingFileHelper = event.getExistingFileHelper();

    gen.addProvider(event.includeServer(), new ModBlockStateProvider(packOutput, existingFileHelper));
    gen.addProvider(event.includeServer(), new ModItemModelProvider(gen, existingFileHelper));
    gen.addProvider(event.includeServer(), new ModRecipeProvider(gen));
    gen.addProvider(event.includeServer(), new LootTableProvider(packOutput, Collections.emptySet(),
    List.of(new LootTableProvider.SubProviderEntry(ModLootTableSubProvider::new, LootContextParamSets.BLOCK))));

    // Agregamos un provider llamando a la misma clase para todas las variantes de español:
    // España, Argentina, Mexico, Uruguay, Venezuela y Chile.
    gen.addProvider(event.includeServer(), new ModSpanishLanguageProvider(packOutput, "es_es"));
    gen.addProvider(event.includeServer(), new ModSpanishLanguageProvider(packOutput, "es_ar"));
    gen.addProvider(event.includeServer(), new ModSpanishLanguageProvider(packOutput, "es_mx"));
    gen.addProvider(event.includeServer(), new ModSpanishLanguageProvider(packOutput, "es_uy"));
    gen.addProvider(event.includeServer(), new ModSpanishLanguageProvider(packOutput, "es_ve"));
    gen.addProvider(event.includeServer(), new ModSpanishLanguageProvider(packOutput, "es_cl"));
 
    // Agregamos un provider llamando a la misma clase para todas las variantes de ingles:
    // Estados unidos, Australia, Canadá y Reino unido.
    gen.addProvider(event.includeServer(), new ModEnglishLanguageProvider(packOutput, "en_us"));
    gen.addProvider(event.includeServer(), new ModEnglishLanguageProvider(packOutput, "en_au"));
    gen.addProvider(event.includeServer(), new ModEnglishLanguageProvider(packOutput, "en_ca"));
    gen.addProvider(event.includeServer(), new ModEnglishLanguageProvider(packOutput, "en_gb"));
}

Ya tenemos todos los providers listos. Pasemos al siguiente punto.


3.5 Generando los datos

Ahora, con todos los providers listos, podemos generar los JSONs de forma automática, y además ejecutar el juego para ver nuestros bloques e ítems por primera vez dentro de Minecraft. Para ello vamos a usar el Gradle preconfigurado que montamos en el punto 1.

Para esto desde la interfaz grafica de nuestro IDE tenemos que localizar el menú de la derecha llamado Gradle, y lanzar la tarea "Run Data" como se ve en la siguiente imagen.

77jOh1r.png

Una vez le hagais click a Run data, empezara un proceso en la consola, debeis esperar a que termine y ya encontrareis una nueva carpeta generated. Siempre que cambieis algo en los providers, necesitais realizar un nuevo Run data.

Para arrancar el juego con vuestro mod desde el entorno de desarrollo, debéis hacer clic justo arriba, en Run client. Esto os abrirá el juego de forma normal, con el mod instalado.


Video demostración de este punto del tutorial.
Está en 360 porque YouTube está procesándolo aun. El volumen de mi voz es muy bajo, me di cuenta después de grabar, suban el audio.


Repositorio de Github en este punto: EMDModTutorial-1.19.4 - Commit: 5a13dd5020c88702

Archivos modificados:
- EmdTest.java
- data/providers/ModBlockStateProvider.java
- data/providers/ModItemModelProvider.java
- data/providers/ModLootTableSubProvider.java
- data/providers/ModRecipeProvider.java
- data/providers/ModSpanishLanguageProvider.java
- data/providers/ModEnglishLanguageProvider.java
- setup/DataGeneration.java
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

4. Comandos


Para realizar los comandos necesitamos registrarlos desde un evento. (Esto se explicará en el apartado de Network en un futuro). Y tambien necesitamos crear una clase donde vamos a crear el comando en si mismo.

Para crear el comando de forma mas sencilla, vamos a crear unos metodos auxiliares en vez de usar funciones anonimas donde vamos a pasar por parametro el contexto del comando y la lista de players.

Vamos con la clase ModCommandsRegister:
Java:
package net.rudahee.setup.registries;

// imports...

public class ModCommandsRegister {

    // Este será el metodo donde creemos los distintos comandos. El parametro dispatcher noso viene dado del evento que crearemos luego.
    public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {

        // Desde el dispatcher vamos a registrar un comando literal (Esto significa que se deben poner ese texto expecifico). Despues indicamos que no necesitamos permisos de OP
        // Para el nivel de permisos, a partir de nivel 2, se necesita ser OP, a partir de nivel 4 no se puede usar en bloques de comando.
        dispatcher.register(Commands.literal("emd").requires(commandSourceStack -> commandSourceStack.hasPermission(0))
                // Cuando pongamos "emd" el siguiente literal será obligatoriamente un "give", podriamos anexar tanto metodos ArgumentBuilder#then con comandos literales como quisieramos.
                .then(Commands.literal("give")
                    // Ahora anexamos el ultimo comando literal de esta accion que tomará el comando.
                    .then(Commands.literal("all items")
                        // Si se anexa un all items y nada mas, se ejecutará el metodo ModCommandsRegister#getAllItems, donde pasamos el contexto, y el usuario que ejecuta el comando.
                        .executes(context -> getAllItems(context, List.of(context.getSource().getPlayerOrException())))
                        // Si por el contrario pasamos un target o varios targets como argumento (y especificamos que los targets son players con EntityArgument#players) ejecutaremos la siguiente linea.
                        .then(Commands.argument("target", EntityArgument.players())
                                // En este caso llamamos al metodo ModCommandsRegister#getAllItems, pero vamos a pasar la lista de targets que hemos recogido del Commands#argument.
                                .executes(context -> getAllItems(context, EntityArgument.getPlayers(context, "target").stream().toList()))))
                    // Esto está al nivel de "all items", y por tanto es una alternativa de comando. Podemos usar el comando "/emd give all items" o el comando "/emd give effects", ya que son
                    // las dos alternativas que estamos dando con los metodos ArgumentBuilder#then
                    .then(Commands.literal("effects")
                            // Si ejecutamos este comando pasará como arriba pero llamando al metodo ModCommandsRegister#applyEffects
                            .executes(context -> applyEffects(context, List.of(context.getSource().getPlayerOrException())))
                            .then(Commands.argument("target", EntityArgument.players())
                                    .executes(context -> applyEffects(context, EntityArgument.getPlayers(context, "target").stream().toList()))))
                ));
    }

    // Método auxiliar encargado de dar los items a la lista de usuarios.
    private static int getAllItems(CommandContext<CommandSourceStack> context, List<ServerPlayer> targetPlayers) {

        // StringBuilder para acumular el nombre de todos los jugadores para imprimirlos en el chat posteriormente
        StringBuilder playersName = new StringBuilder();
        // Esta variable solo sirve para no poner una  coma en la primera iteración del bucle.
        boolean firstLoop = true;

        for (ServerPlayer player: targetPlayers) {
            if (firstLoop) {
                firstLoop = false;
                playersName.append(player.getScoreboardName());
            } else {
                playersName.append(", ").append(player.getScoreboardName());
            }
            // Para agregar un item al inventario, no se puede hacer directamente, para ello tenemos que crear un ItemStack.
            // Un ItemStack es una instancia de un Item, y la cantidad de ese Item en concreto.
            player.getInventory().add(new ItemStack(ModItemsRegister.ETTMETAL.get(), 64));
            player.getInventory().add(new ItemStack(ModItemsRegister.ALUMINUM_INGOT.get(), 64));
        }
        // Con el contexto, procedemos a enviar un mensaje "traducible" (Esta linea deberemos incluirla en el provider de traducciones") y después agregamos la lista de players.
        context.getSource().sendSystemMessage(Component.translatable("command.message.emd_test.get_all_items").append(Component.literal(": "+ playersName)));

        // Si nada explotó antes, aquí devolvemos 1, que significa que la ejecución ha sido correcta.
        return 1;
    }

    // Este método es igual que el de arriba, solo cambia como se aplica el efecto.
    private static int applyEffects(CommandContext<CommandSourceStack> context, List<ServerPlayer> targetPlayers) {
        StringBuilder playersName = new StringBuilder();
        boolean firstLoop = true;

        for (ServerPlayer player: targetPlayers) {
            if (firstLoop) {
                firstLoop = false;
                playersName.append(player.getScoreboardName());
            } else {
                playersName.append(", ").append(player.getScoreboardName());
            }

            // Para agregar un efecto, creamos una nueva instancia pasando por parametros: el tipo de efecto, la duracion en ticks (1s = 20 ticks), la potencia del efecto (0 de poder en regeneracion es Regeneracion 1),  y el resto es para mostrar el icono en la GUI, las burbujas en primera persona, y las burbujas en tercera persona
            player.addEffect(new MobEffectInstance(MobEffects.REGENERATION, 60, 0, true, true, true));
            player.addEffect(new MobEffectInstance(MobEffects.NIGHT_VISION, 600, 0, true, true, true));
            player.addEffect(new MobEffectInstance(MobEffects.FIRE_RESISTANCE, 60, 0, true, true, true));
            player.addEffect(new MobEffectInstance(MobEffects.JUMP, 300, 1, true, true, true));
            player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 300, 1, true, true, true));
            player.addEffect(new MobEffectInstance(MobEffects.DIG_SPEED, 60, 0, true, true, true));
            player.addEffect(new MobEffectInstance(MobEffects.WATER_BREATHING, 600, 0, true, true, true));

        }

        context.getSource().sendSystemMessage(Component.translatable("command.message.emd_test.apply_effects").append(Component.literal(": "+ playersName)));

        return 1;

    }
}

Ahora vamos a registrar el evento en la clase Registration, para ello vamos a crear un método muy pequeñito:

Java:
    @SubscribeEvent
    public void onCommandsRegister(RegisterCommandsEvent event) {
        ModCommandsRegister.register(event.getDispatcher());
    }




5. Generación de Mundo​

Ahora que tenemos nuestras menas de aluminio y Ettmetal estaría bien que aparecieran por el mundo y no solo en la pestaña de creativo, para que así se pudieran conseguir en modo supervivencia.

Para realizar la generación de mundo se hereda de DatapackBuiltinEntriesProvider.java para crear nuestro propio provider. pero este caso es algo especial, ya que no haremos nada en el propio provider, sino que debemos crear tres clases desde donde lo vamos a gestionar. Yo suelo crear estas clases bajo el paquete setup, llamándolo world_generation debido a que en realidad, su funcionamiento lógico es el de definir como se va a generar el mundo una sola vez, al arrancar el propio mundo.

Aclaración de lo anterior: En realidad, para ser puristas, todo esto se podría hacer en la propia clase del provider, ya que en realidad lo único que hacemos con todo es generar los archivos JSON correspondientes, pero hay que crear muchas constantes y bastantes métodos, así que tenerlos separados me resulta mas cómodo e intuitivo. Para ser extremadamente puristas, debería ir en la carpeta provider, pero me gusta mas en setup por como yo entiendo la lógica.


Vamos a empezar creando una clase muy pequeña (ModPlacement.java con dos metodos auxiliares para crear el como se van a colocar las menas en el mundo:
Java:
package net.rudahee.setup.world_generation;

import net.minecraft.world.level.levelgen.placement.*;

import java.util.List;

public class ModPlacement {

    // Metodo que va a devolver la lista de modificadores de colocacion de menas.
    public static List<PlacementModifier> orePlacement(PlacementModifier modifier, PlacementModifier modifier2) {
        return List.of(modifier, InSquarePlacement.spread(), modifier2, BiomeFilter.biome());
    }

    // Metodo donde al que llamaremos para colocar las menas mas comunes, gracias al metodo CountPlacement#of
    public static List<PlacementModifier> commonOrePlacement(int count, PlacementModifier modifier) {
        return orePlacement(CountPlacement.of(count), modifier);
    }

}

Ahora vamos a crear la clase ModConfigFeatures.java donde definiremos las reglas de generación en Stone y Deepslate, además de la propia configuración de la generación de la mena. La clase debe queda así:

Java:
package net.rudahee.setup.world_generation;

// imports...

public class ModConfigFeatures {

    // Definimos las reglas para que sustituyan tipos de bloques concretos. En este caso vamos a crear una mena para
    // stone, y una meena para deepslate
    public static final RuleTest DEEPSLATE_REPLACE_RULE = new TagMatchTest(BlockTags.DEEPSLATE_ORE_REPLACEABLES);
    public static final RuleTest STONE_REPLACE_RULE = new TagMatchTest(BlockTags.STONE_ORE_REPLACEABLES);

    // Para el aluminio y ettmetal, vamos a crear las configuraciones, que incluyen, que bloque se va a reemplazar,
    // y nuestro bloque de mena del metal especifico.
    public static final List<OreConfiguration.TargetBlockState> ALUMINUM_ORE_CONFIG_STONE =  List.of(OreConfiguration.target(STONE_REPLACE_RULE, ModBlockRegister.ALUMINUM_ORE.get().defaultBlockState()));
    public static final ResourceKey<ConfiguredFeature<?, ?>> ALUMINUM_ORE_STONE_KEY = registerKey("aluminum_ore_stone_key");

    public static final List<OreConfiguration.TargetBlockState> ETTMETAL_ORE_CONFIG_DEEPSLATE =  List.of(OreConfiguration.target(DEEPSLATE_REPLACE_RULE, ModBlockRegister.ETTMETAL_ORE.get().defaultBlockState()));
    public static final ResourceKey<ConfiguredFeature<?, ?>> ETTMETAL_ORE_DEEPSLATE_KEY = registerKey("ettmetal_ore_deepslate_key");

    public static void bootstrap(BootstapContext<ConfiguredFeature<?, ?>> context) {
        // Los parametros del OreConfiguration#new son los siguientes: la configuracion, el tamaño de la veta, y la probabilidad de que no aparezcan con una cara expuesta al aire.
        register(context, ALUMINUM_ORE_STONE_KEY, Feature.ORE, new OreConfiguration(ALUMINUM_ORE_CONFIG_STONE, 7, 0.5f));
        register(context, ETTMETAL_ORE_DEEPSLATE_KEY, Feature.ORE, new OreConfiguration(ETTMETAL_ORE_CONFIG_DEEPSLATE, 4, 0.7f));
    }

    // Metodo auxiliar para crear una key en el json para que Minecraft sepa identificar que mod esta tocando la generacion de mundo.
    public static ResourceKey<ConfiguredFeature<?, ?>> registerKey(String name) {
        return ResourceKey.create(Registries.CONFIGURED_FEATURE, new ResourceLocation(EmdTest.MOD_ID, name));
    }

    // Aqui registramos las propias configuraciones de las menas. para ello, le vamos a pasar el context del metodo boostrap,
    // la key que hemos generado, el tipo de objeto que se generará en el mundo y la configuracion de ese tipo de objeto (en este caso una mena)
    private static <FC extends FeatureConfiguration, F extends Feature<FC>> void register(BootstapContext<ConfiguredFeature<?, ?>> context,
                                                                                          ResourceKey<ConfiguredFeature<?, ?>> key, F feature, FC configuration) {
        context.register(key, new ConfiguredFeature<>(feature, configuration));
    }
}

Despues tenemos que crear la clase ModPlacedFeatures.java en la que definiremos como deben ser las vetas y otro tipo de datos relacionados. La explicacion detallada está en la propia clase:

Java:
package net.rudahee.setup.world_generation;

// imports..

public class ModPlacedFeatures {

    // Estas keys deben coincidir con los Jsons que debemos crear nosotros a mano.
    public static final ResourceKey<PlacedFeature> ALUMINUM_PLACED_STONE_KEY = createKey("aluminum_placed_stone");
    public static final ResourceKey<PlacedFeature> ETTMETAL_PLACED_DEEPSLATE_KEY = createKey("ettmetal_placed_deepslate");



    public static void bootstrap(BootstapContext<PlacedFeature> context) {

        // Hacemos el holder con el que obtendremos las caracteristicas ya configuradas. Nosotros las creamos en la clase ModConfiguredFeatures.
        // Si estas no se crean correctamente, entonces recibiras una excepcion en la consola.
        HolderGetter<ConfiguredFeature<?, ?>> configuredFeatures = context.lookup(Registries.CONFIGURED_FEATURE);

        // Aqui registramos ambas vetas de las metas de metal
        register(context, ALUMINUM_PLACED_STONE_KEY, configuredFeatures.getOrThrow(ModConfigFeatures.ALUMINUM_ORE_STONE_KEY),
                // Con el ModPlacement#commonOrePlacemente, definimos la cantidad de vetas que deben aparecer en un unico chunk,
                // asi como las alturas validas, en este caso yo tomo el la coordenada 0 desde lo mas profundo de Minecraft
                // (con el metodo aboveBottom, si puediera 10, estaría tomando la coordenada -55, al poner 0, tomo la coordenada -65,
                // el parametro suma esa cantidad de bloques a la coordenada en la que se empieza a generar la bedrock)
                // Y para finalizar, le pongo la coordenada absoluta 120, por tanto las menas se generaran de la coordenada -65 a 120.
                ModPlacement.commonOrePlacement(25, // veins per chunk
                        HeightRangePlacement.uniform(VerticalAnchor.aboveBottom(0), VerticalAnchor.absolute(120))));

        register(context, ETTMETAL_PLACED_DEEPSLATE_KEY, configuredFeatures.getOrThrow(ModConfigFeatures.ETTMETAL_ORE_DEEPSLATE_KEY),
                ModPlacement.commonOrePlacement(12, // veins per chunk
                        // En este caso, aunque a partir de la coordenada 0 no hay deepslate, mantengo el 120,
                        // ya que no influye, no va a reemplazar nada de stone, ya que solo puede reemplazar la propia deepslate.
                        HeightRangePlacement.uniform(VerticalAnchor.aboveBottom(0), VerticalAnchor.absolute(120))));
    }

    private static ResourceKey<PlacedFeature> createKey(String name) {
        return ResourceKey.create(Registries.PLACED_FEATURE, new ResourceLocation(EmdTest.MOD_ID, name));
    }

    private static void register(BootstapContext<PlacedFeature> context, ResourceKey<PlacedFeature> key, Holder<ConfiguredFeature<?, ?>> configuration,
                                 List<PlacementModifier> modifiers) {
        context.register(key, new PlacedFeature(configuration, List.copyOf(modifiers)));
    }
}

Ahora vamos con el provider como hemos visto anteriormente en el tutorial:
Java:
package net.rudahee.data.providers;

//imports...

// Nuestro provider de costumbre que nos va a generar los jsons.
public class ModWorldGenProvider extends DatapackBuiltinEntriesProvider {

    // Definimos un builder, al que le pasamos por parametros en los metodos RegistrySetBuilder#add tanto el configFeature,
    // como el placedFeature, asi como los metodos bootstrap que hemos creado previamente
    public static final RegistrySetBuilder BUILDER = new RegistrySetBuilder()
            .add(Registries.CONFIGURED_FEATURE, ModConfigFeatures::bootstrap)
            .add(Registries.PLACED_FEATURE, ModPlacedFeatures::bootstrap);

    // Generamos el constructor por defecto heredado de la clase DatapackBuiltInEntriesProvider.
    public ModWorldGenProvider(PackOutput output, CompletableFuture<HolderLookup.Provider> registries) {
        super(output, registries, BUILDER, Set.of(EmdTest.MOD_ID));
    }
}

Como hemos visto anteriormente en el tutorial, cuando hacemos un provider, debemos agregarlo a nuestro metodo DataGeneration#GatherData.
En este caso solo debemos agregar esta linea: gen.addProvider(event.includeServer(), new ModWorldGenProvider(packOutput, lookupProvider));.

Lo ultimo que debemos hacer ahora es crear dos archivos JSON en una ruta especifica para decirle a Minecraft que debe leer los JSONs que hemos generado con nuestro provider. Para hacer esto debemos usar el nombre de la key que hemos puesto en el ModPlacedFeature.

La ruta para crear los archivos debe ser exactamente la siguiente: resources/data/MOD_ID/forge/biome_modifier

En mi caso he creado estos dos archivos JSON:
JSON:
{
    "type": "forge:add_features",
    "biomes": "#is_overworld",
    "features": "emd_test:aluminum_placed_stone",
    "step": "underground_ores"
}
JSON:
{
    "type": "forge:add_features",
    "biomes": "#is_overworld",
    "features": "emd_test:ettmetal_placed_deepslate",
    "step": "underground_ores"
}

Aquí tenéis una captura de como se ve mi carpeta resources:
dmYL8ty_TmWMgbQa6ojtcg.png

Una vez realizado todo este proceso, solo debeis ejecutar la rutina Run data de Gradle, cuando empezeis un nuevo mundo, podreis ver como se genera las vetas de metal por el mundo:
Veta de aluminio:
qXst72t.png



Veta de ettmetal
mniLFWp.png



Repositorio de Github en este punto: EMDModTutorial-1.19.4 - Commit f03dfe73545

Archivos modificados:
- resources/data/emd_test/forge/biome_modifier/aluminum_stone_placed.json
- resources/data/emd_test/forge/biome_modifier/ettmetal_deepslate_placed.json
- data/providers/ModWorldGenProvider.java
- setup/world_generation/ModPlacement.java
- setup/world_generation/ModPlacedFeatures.java
- setup/world_generation/ModConfigFeatures.java
- setup/DataGeneration.java
- setup/Registration.java
- setup/registries/ModCommandsRegister.java
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

6. Networking


El networking es relativamente sencillo en Forge. Antes de explicar como funcionan los datos del jugador y como gestionarlos, voy a explicar un par de cosas importantes a tener en cuenta.

El código en el servidor y el cliente son el mismo (normalmente generamos un único archivo jar) y esto implica tener cierto control sobre que se ejecuta en cliente y que se ejecuta en servidor. Esto es importante incluso en el modo un jugador.

Aunque ejecutemos el juego en modo un jugador, Minecraft crea un servidor local, crea un cliente y nos conecta a el, aunque el funcionamiento es ligeramente distinto al de un servidor dedicado, si es importante tener en cuenta que siempre hay un server-side y un client-side aunque sea local.

El client-side esta encargado de la GUI, los atajos de teclado y de la lógica de cliente entre muchas otras cosas, y el server-side se encarga de la lógica dura de la aplicación, la lógica de generación de mundo, el registro de ítems y bloques, etc...

Teniendo esto en cuenta, no hay nada mas que un desperdicio de recursos o generar bugs tratar de pintar una GUI en el lado del servidor. o tratar de generar mundo en cliente.

¿Cómo indicamos si algo se ejecuta en servidor o cliente?

Pues es bastante sencillo, para cliente tenemos que anotar el método con la siguiente etiqueta:
Java:
@OnlyIn(Dist.CLIENT)  // Esta linea es la que define que esto solo se ejecutará en cliente.
@SubscribeEvent
public void onClientTick(final TickEvent.ClientTickEvent event) {

    if (Minecraft.getInstance().player == null) {
        return;
    }
}

Para servidor es menos intuitivo:

Java:
    @SubscribeEvent
    public static void onWorldTickEvent(final TickEvent.LevelTickEvent event) {
 
        // Este if hace que en el cliente salga del metodo y no haga absolutamente en cliente
        // por tanto el resto de la logica se ejecutará en servidor.
        if (event.side.isClient()) {
            return;
        }

        Level level = event.level;
        List<? extends Player> playerList = level.players();

        for (Player player : playerList) {
            if (player != null) {

            }
        }
    }

Con esto estamos listos para empezar a crear paquetes, y manejarlos. Así que vamos con ello.



6.1 Asignando datos al jugador

Asignar datos al jugador es realmente versátil, en este caso vamos a guardar el punto de respawn, aunque no sea muy útil. Pero podrías almacenar una carga, un estado, un contador de muertes por ejemplo, etc...

Para asignar datos a un jugador vamos a crear algo llamado: Capability. Para ello vamos a crear una clase y una interfaz, también vamos a hacer unos métodos para realizar la transformación a tags de Minecraft. Y para finalizar vamos a registrar el capability como tal.

Para la clase y la interfaz vamos a realizarlo bajo el directorio data/player

La clase principal va a ser ExtraPlayerData que contendrá los datos del propio jugador, y la lógica para convertirlo a tag.
Java:
package net.rudahee.data.player;

import net.minecraft.core.BlockPos;
import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.Level;

public class ExtraPlayerData implements IExtraPlayerData {

    // Dato que contendra el jugador.
    GlobalPos respawnPos;

    public ExtraPlayerData() {
        respawnPos = GlobalPos.of(Level.OVERWORLD, new BlockPos(0, 100, 0));
    }

    // Metodos desde los que deberemos acceder, son getters y setters comunes.
    @Override
    public GlobalPos getRespawnPos() {
        return respawnPos;
    }

    @Override
    public void setRespawnPos(GlobalPos respawnPos) {
        this.respawnPos = respawnPos;
    }

    // Metodos para la transformacion a nbt y al contrario.
    @Override
    public CompoundTag save() {
        CompoundTag playerData = new CompoundTag();

        // Guardamos la dimension como un string con el formato "minecraft:the_end" por ejemplo.
        playerData.putString("dimension", respawnPos.dimension().toString());

        // Guardamos el nbt como un Entero la posicion en el mundo del jugador.
        playerData.putInt("x", respawnPos.pos().getX());
        playerData.putInt("y", respawnPos.pos().getY());
        playerData.putInt("z", respawnPos.pos().getZ());

        return playerData;
    }

    @Override
    public void load(CompoundTag nbt) {
        String resourceKeyDimension = nbt.getString("dimension");

        // Ahora rellenamos los datos del usuario desde el nbt, en este caso comparamos el string y asignamos los datos.
        if (resourceKeyDimension.contains("overworld")) {
            respawnPos = GlobalPos.of(Level.OVERWORLD, new BlockPos(nbt.getInt("x"), nbt.getInt("y"), nbt.getInt("z")));
        } else if (resourceKeyDimension.contains("nether")) {
            respawnPos = GlobalPos.of(Level.NETHER, new BlockPos(nbt.getInt("x"), nbt.getInt("y"), nbt.getInt("z")));
        } else if (resourceKeyDimension.contains("end")) {
            respawnPos = GlobalPos.of(Level.END, new BlockPos(nbt.getInt("x"), nbt.getInt("y"), nbt.getInt("z")));
        } else {
            respawnPos = GlobalPos.of(Level.OVERWORLD, new BlockPos(nbt.getInt("x"), nbt.getInt("y"), nbt.getInt("z")));
        }
    }
}

De esto, debemos extraer a una interfaz todos los métodos (IExtraPlayerData) que no sean el propio constructor.
Java:
package net.rudahee.data.player;

import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.CompoundTag;

public interface IExtraPlayerData {
    GlobalPos getRespawnPos();

    void setRespawnPos(GlobalPos respawnPos);

    CompoundTag save();

    void load(CompoundTag nbt);
}

Y con esto pasamos al registro del capability, desde donde accederemos a estos datos en los próximos puntos. Vamos a crear la clase ExtraPlayerDataRegister para registrar el capability y vincularlos con las clases que acabamos de crear, irá en el paquete con el resto de registros.
Java:
public class ExtraPlayerDataRegister {

    // Es un register bastante normal, comparado a lo que hemos visto.
    public static final Capability<IExtraPlayerData> PLAYER_CAP = CapabilityManager.get(new CapabilityToken<IExtraPlayerData>() {
        @Override
        public String toString() {
            return super.toString();
        }
    });

    public static final ResourceLocation IDENTIFIER = new ResourceLocation(EmdTest.MOD_ID, "emd_data");

    public static void register(final RegisterCapabilitiesEvent event) {
        event.register(IExtraPlayerData.class);
    }

}



6.2 Creando y manejando paquetes de red

Lo primero es responder a la pregunta ¿Qué es y para que sirve un paquete?

Un paquete es un conjunto de información que se envía desde cliente a servidor o viceversa. Y lo vamos a usar para que el servidor envié los datos custom del usuario a cada cliente, para que esté actualizado cada tic del juego.

Para ello debemos crear el paquete y registrarlos de una forma especifica.

Para crear un paquete lo vamos a hacer desde esta ruta setup/network/packets y la primera clase que vamos a crear será SyncPlayerDataPacket


Java:
package net.rudahee.setup.network.packets;

// Imports....

public class SyncPlayerDataPacket {
  
    // Datos que tratará el paquete.
    private final CompoundTag tag;
    private final UUID uuid;

    // Constructor, donde asignamos datos al paquete
    public SyncPlayerDataPacket(IExtraPlayerData data, Player player) {
        this.uuid = player.getUUID();
        this.tag = (data != null && ExtraPlayerDataRegister.PLAYER_CAP != null) ? data.save() : new CompoundTag();
    }
  
    // Constructor, donde asignamos datos al paquete
    private SyncPlayerDataPacket(CompoundTag tag, UUID uuid) {
        this.tag = tag;
        this.uuid = uuid;
    }
  
    // El metodo decode recibirá un buffer de red, y leeremos los datos en el mismo orden que los
    // escribimos en el metodo encode, es importante, ya que la lectura debe ser ordenada, si nosotros
    // escribimos primero un string y despues un int, intentar leer un int en primer lugar generará una excepcion
    public static SyncPlayerDataPacket decode(FriendlyByteBuf buf) {
        return new SyncPlayerDataPacket(buf.readNbt(), buf.readUUID());
    }

    // El metodo enconde, escribirá los datos en un buffer de red, que debe respetar el orden con el que
    // se leeran en el metodo decode.
    public void encode(FriendlyByteBuf buf) {
        buf.writeNbt(this.tag);
        buf.writeUUID(this.uuid);
    }

    // Este metodo handle controla que debe pasar en el momento en el que se recibe un paquete.
    // en este caso, como este paquete solo irá en la direccion servidor -> cliente, vamos a obtener
    // al jugador y sincronizar los datos del capability del jugador.
    public void handle(Supplier<NetworkEvent.Context> ctx) {
        ctx.get().enqueueWork(() -> {
            Player player = Minecraft.getInstance().level.getPlayerByUUID(this.uuid);

            if (player != null && ExtraPlayerDataRegister.PLAYER_CAP != null) {
                player.getCapability(ExtraPlayerDataRegister.PLAYER_CAP).ifPresent(cap -> cap.load(this.tag));
            }
        });

        ctx.get().setPacketHandled(true);
    }
}

También crearemos la clase desde la que registraremos y gestionaremos los paquetes. La ruta serásetup/network y la clase ModNetwork


Java:
package net.rudahee.setup.network;

//Imports

public class ModNetwork {
  
    // La version la usaremos para controlar que versiones del jar de cliente se pueden conectar al jar
    // del servidor. Lo normal es actualizar la version con respecto a la que tenemos en gradle.
    private static final String VERSION = EmdTest.VERSION;


    public static final SimpleChannel INSTANCE = NetworkRegistry.newSimpleChannel(new ResourceLocation(EmdTest.MOD_ID,
            "network_tunnel"), () -> VERSION, VERSION::equals, VERSION::equals);

    private static int index = 0;

    private static int nextIndex() {
        return index++;
    }

    // Para registrar los paquetes debemos realizarlo de esta manera.
    public static void registerPackets() {
        INSTANCE.registerMessage(nextIndex(), SyncPlayerDataPacket.class, SyncPlayerDataPacket::encode, SyncPlayerDataPacket::decode, SyncPlayerDataPacket::handle);
    }

    // Conjunto de metodos auxiliares para gestionar los paquetes.
  
    // Client to Serv
    public static void sendToServer(Object msg) {
        INSTANCE.sendToServer(msg);
    }

    public static void sendTo(Object msg, PacketDistributor.PacketTarget target) {
        INSTANCE.send(target, msg);
    }
  
    // Este es el metodo que envia el mensaje al jugador (Al cliente).
    public static void sync(Object msg, Player player) {
        sendTo(msg, PacketDistributor.TRACKING_ENTITY_AND_SELF.with(() -> player));
    }

    // Metodo auxiliar para enviar el packet de player.
    public static void syncInvestedDataPacket(IExtraPlayerData capability, Player player) {
        sync(new SyncPlayerDataPacket(capability, player), player);
    }

}





Este apartado está muy ligado al siguiente punto, por tanto, tanto la prueba de funcionalidad, como las clases editadas y el código de github estarán al final del punto 7.
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

7. Eventos


Si habéis tocado otros frameworks o APIs sabréis lo común que es trabajar en base a eventos. Pues Minecraft funciona de la misma manera.

Cada vez que pasa algo en el juego salta un evento en el código. Por ejemplo, salta un evento al golpear algo, salta un evento cada tick del juego o salta un evento cada vez que haces clic derecho en un cofre.

Hay eventos que se usan en el lado de servidor y eventos que se usan en el lado de cliente (Por ejemplo, dibujar una GUI es un evento que salta solo de lado cliente). Por tanto, debemos tener en cuenta lo visto en el punto anterior.

Dentro de los eventos es donde ejecutaremos la lógica de negocio del mod, y es importante que esta lógica de negocio se ejecute solo en el lado correcto. Controlar esto es sencillo, lo veremos en el código.




7.1 Eventos de servidor

Vamos a ver dos eventos de servidor, el que se ejecuta cada tick y el que se ejecuta cuando una entidad va a recibir daño. Los vamos a crear bajo la clase ServerEventsHandler

Java:
package net.rudahee.modules.events.server;

//imports

public class ServerEventsHandler {


    // Al ser un evento nos tenemos que suscribir (decir que pase por aqui) para que se ejecute
    // El tipo de evento viene por el parametro. En este caso, un TickEvent.
    @SubscribeEvent
    public static void onWorldTickEvent(final TickEvent.LevelTickEvent event) {
        // Lo ejecutaremos despues de todo el resto del codigo de Minecraft. Asi que la fase es END
        if (event.phase != TickEvent.Phase.END) {
            return;
        }

        // Si esto salta en el lado cliente nos salimos.
        if (event.side.isClient()) {
            return;
        }

        // Obtenemos todos los jugadores del mundo
        Level level = event.level;
        List<? extends Player> playerList = level.players();

        for (Player player : playerList) {
            if (player != null) {

                // Ejecutamos el codigo...
          
                // Si tenemos un bloque de ettmetal en la mano, vamos a dar el efecto de velocidad 3 durante 2 segundos
                if (player.getMainHandItem().is(ModBlockRegister.ETTMETAL_BLOCK.get().asItem())) {
                    // MobEffectInstance(EffectType, amplificador (empezando en 0), duracion en segundos, visible en ambiente, visible en interfaz, visible en primera persona)
                    player.addEffect(new MobEffectInstance(MobEffects.MOVEMENT_SPEED, 2, 2, true, false, false));
                }
          
            }
        }
    }


    @SubscribeEvent
    public static void onDamageEvent(final LivingHurtEvent event) {
  
        // En este evento, que es cuando un jugador le hace daño a otra cosa (En este if, controlamos que sea otro jugador)
        if (event.getSource().getDirectEntity() instanceof ServerPlayer source
                                            && event.getEntity() instanceof ServerPlayer target) {

            // Vamos a hacer que si tiene un bloque de aluminio en la mano secundaria, duplique su daño.
            if (source.getOffhandItem().is(ModBlockRegister.ALUMINUM_BLOCK.get().asItem())) {
                event.setAmount(event.getAmount() * 2);
            }
      
        // En este evento, que es cuando un jugador le hace daño a otra cosa (En este if, controlamos que NO sea otro jugador)
        } else if (event.getSource().getDirectEntity() instanceof ServerPlayer source && !(event.getEntity() instanceof ServerPlayer)) {
            // Vamos a hacer que si tiene un bloque de aluminio en la mano secundaria, haga la mitad de daño.
            if (source.getOffhandItem().is(ModBlockRegister.ALUMINUM_BLOCK.get().asItem())) {
                event.setAmount(event.getAmount() * 0.5);
            }
        }
    }
}



7.2 Eventos de cliente

Para el lado cliente necesitamos muchos mas controles para que todo funcione correctamente. Vamos a ver cuales son en la clase ClientEventsHandler.

Java:
package net.rudahee.modules.events.client;

// imports

public class ClientEventsHandler {

    // En este caso debemos poner tambien que es "OnlyIn" lado cliente ademas de suscribirnos al evento.
    @OnlyIn(Dist.CLIENT)
    @SubscribeEvent
    public void onClientTick(final TickEvent.ClientTickEvent event) {
        Player player;

        // Si la instancia no contiene un player significa que estamos en el menu
        // (Si si, donde seleccionar si queremos jugar single player o multi player y eso)
        if (Minecraft.getInstance().player == null) {
            return;
        } else {
            player = Minecraft.getInstance().player;
        }

        // Si es servidor nos salimos
        if (event.type == TickEvent.Type.SERVER) {
            return;
        }
    
        // Si el no es el final del evento, si el juego esta en pausa o el jugador esta muerto tambien nos salimos
        if (event.phase != TickEvent.Phase.END || Minecraft.getInstance().isPaused() || !player.isAlive()) {
            return;
        }

        // En este caso vamos a envia un mensaje al chat si pulsamos la tecla de salto y la de agacharse de forma simultanea
        if (Minecraft.getInstance().options.keyJump.isDown() && Minecraft.getInstance().options.keyShift.isDown()) {
            player.sendSystemMessage(Component.literal("Imaginate que estoy pintando una interfaz :)"));
        }

    }
 
}



8. Generar Archivos de Configuración​

Los archivos de configuración son realmente simples de generar comparado con lo ya visto, solo necesitamos registrarlos y escribir el contenido por defecto del archivo.

El archivo de configuración por defecto se genera en la siguiente ruta: [ruta_minecraft]/saves/[mundo_creado]/serverconfig/nombre.toml

Lo que modifiquemos ahí se guardaran en una serie de variables y constantes en el código, que podemos usar normalmente en cualquier clase (Haciendo un simple if/else por ejemplo.

Como es reamente sencillo vamos a ver el código directamente.

La clase será ConfigRegister
Java:
// Debemos crear un registro
public class ConfigRegister {

    // Creamos un builder que servirá para registrar el config
    public static ForgeConfigSpec.Builder SERVER_CONFIG;

    // Creamos un valor para cada dato que crearemos en el archivo de configuracion.
    // Como será un mock en este caso, solo crearemos una de tipo String.
    public static ForgeConfigSpec.ConfigValue<String> OPTION;
 
    // Registramos la configuracion. Le decimos que será "tipo servidor", y el nombre del archivo.
    public static void register() {
        ModLoadingContext.get().registerConfig(ModConfig.Type.SERVER, SERVER_CONFIG.build(), "emd-test.toml");
    }

    // Ahora creamos el contenido del archivo
    private static void setupConfig(ForgeConfigSpec.Builder builder) {
        // Aqui escribimos un comentario que no hace nada, pero es para dejarlo bonito.
        builder.comment("""
             
                ##########################
                #    EMD TEST CONFIG    ##
                ##########################
                \n""");

        // En este caso escribimos una serie de comentarios de una sola linea.
        builder.comment("=============================================");
        builder.comment("\tDefault configs: ");
        builder.comment("=============================================");

        builder.comment(" Esto es una configuracion de prueba, no hace nada");
        // Aqui definimos el nombre que tendra en el archivo esa opcion, y el valor por defecto.
        // Tambien lo asignamos a la constante que habiamos creado antes.
        OPTION = builder.define("option", "EscribeLoQueQuierasSinEspacios");
     

    }



Repositorio de Github en este punto: En construcción

Archivos modificados:

En construcción
 
Última edición:
OP

RuDaHeee

Life before death~
Supporter
Mensajes
936
Reacciones
625
Puntos
582
Ubicación
Sevilla, España

9. ¿Cómo sigo aprendiendo?​

Lo primero ¡Felicidades! Eres uno o una de los pocos que ha sido capaz de terminar todo este tocho, pero tengo malas noticias. Esto es una introducción muy pequeña para todo lo que nos permite Forge.

A partir de aquí no vas a encontrar mucho mas contenido en Español, y eso es un problema, pero si tienes conocimientos solidos de Java puedes hacer lo que quieras a base de ver el código de otros mods (Y esto es clave), ya que muchos mods usan licencias que permiten compartir el código y lo tienen anclado en las webs donde se suelen publicar mods.

Por tanto tus grandes amigos van a ser CurseForge y Modrinth donde la mayoría de modders suben su trabajo. Si quisieras hacer un mod de magia, podrías irte al perfil de nuestro equipo de desarrollo y ver como funcionan nuestros mods, y de ahí escribir (No copiéis código sin dar crédito) vuestro propio código.

PzxGuUf.png

Si por el contrario sabes ingles tengo dos canales de YouTube que recomendarte: Kaupenjoe y TurtyWurty. También puedes pasarte por los foros de MinecraftForge que suelen ser de bastante ayuda.

Y para finalizar os quiero recordar que si tenéis dudas (sean o no del tutorial), habéis realizado vuestro propio mod, habéis portado un mod antiguo a una nueva versión, o si tenéis alguna sugerencia, os invito a dejarlas en los comentarios debajo de este tutorial. y las iré respondiendo cuando vaya teniendo tiempo.

¡Muchas gracias a todos!
 
Última edición:

CuchUwU

Chaos Bitch B)
Supporter
Mensajes
27
Reacciones
33
Puntos
34
Ubicación
Dentro de tus Paredes
ay que genial mi jefecito que bien programa, OLE MI NIÑOOOOO que lindo tuto te está quedando
 
Arriba Pie