Skip to content
Browse docs

Search documentation

Release 0.2.2

Enter a topic to search.

    Create your own add-on

    Build a separate add-on with one controllable electrical load. You need basic Java, Java 21 and the released WWPG 0.2.2 developer SDK. You do not need WWPG’s implementation source or previous NeoForge experience. This project is a creative teaching example, not a survival-balanced machine.

    Download the developer SDK, and check the published checksums. Extract the outer ZIP. Extract maven/wwpg-api-1.0.0-maven.zip into wwpg-api-maven; its root contains org/cha0scollective/wwpg.

    Extract examples/tutorial-load-0.1.0-source.zip from the SDK bundle, or copy the text files below and the wrapper from the SDK. This included starter contains one load. Its addon ID is tutorial_load; the supplied examples use different IDs, so they can run together. The starter’s own version is 0.1.0, while its compile-time API is 1.0.0.

    For a fresh project built by copying files: create an empty tutorial-load directory, copy gradlew, gradlew.bat and the entire gradle/wrapper directory from the SDK’s extracted general example, copy its LICENSE, then add the complete text files from the website file listings. Do not copy the nine-device example’s Java or resources. All text files are included in the website tutorial; the starter directory has the same files and wrapper. Keep its MIT license file at the project root; the build packages it into your jar.

    For the ZIP route, download both the SDK and the starter source ZIP before these commands. The examples below assume they are in your Downloads directory. Use new destination directories; do not extract over another project.

    Terminal window
    Expand-Archive -LiteralPath "$env:USERPROFILE/Downloads/wwpg-0.2.1-developer-sdk.zip" -DestinationPath "C:/Dev/WWPG SDK"
    Expand-Archive -LiteralPath "C:/Dev/WWPG SDK/maven/wwpg-api-1.0.0-maven.zip" -DestinationPath "C:/Dev/WWPG API Maven"
    Expand-Archive -LiteralPath "$env:USERPROFILE/Downloads/tutorial-load-0.1.0-source.zip" -DestinationPath "C:/Dev/Tutorial Load"
    Set-Location "C:/Dev/Tutorial Load"
    Terminal window
    mkdir -p "$HOME/Dev"
    unzip "$HOME/Downloads/wwpg-0.2.1-developer-sdk.zip" -d "$HOME/Dev/WWPG SDK"
    unzip "$HOME/Dev/WWPG SDK/maven/wwpg-api-1.0.0-maven.zip" -d "$HOME/Dev/WWPG API Maven"
    unzip "$HOME/Downloads/tutorial-load-0.1.0-source.zip" -d "$HOME/Dev/Tutorial Load"
    cd "$HOME/Dev/Tutorial Load"
    chmod +x gradlew

    Expect settings.gradle and src in the project root, and org/cha0scollective/wwpg directly in the Maven directory. Existing SDK example archives require chmod +x gradlew; the documentation companion and future exports also carry executable Unix metadata.

    The wrapper runs the project’s Gradle version. settings.gradle names the project; build.gradle declares the NeoForge plugin and compile-only API; gradle.properties controls Gradle’s JVM. None is a Minecraft gameplay class.

    Name Where it is used
    tutorial_load @Mod, NeoForge metadata, Gradle’s mods entry and resource namespace
    org.example.tutorialload Java package and matching directories under src/main/java
    tutorial_load:load Block/item registry ID and the WWPG provider ID
    positive, negative Stable terminal keys; native IDs 0 and 1
    load_in Internal node between the switch and resistor
    enable, load Stable switch and resistor element IDs

    To name your own add-on, change the mod ID in Java, TOML dependency table names, Gradle’s mods entry, resource directories and JSON references together. Change the Java package with its directories and imports. Rename the jar’s archive name and display strings too. Do not rename terminal/element IDs in an existing saved world unless you intentionally replace its electrical identity. Keep the general example installed during the first circuit test. Install both SDK examples for the final naming/collision check; the native example also needs its declared CEE/PG prerequisites. The resulting jar is <archivesName>-<version>.jar; the filenames below assume the starter is unchanged.

    1. Register: TutorialLoadMod registers ordinary NeoForge blocks, items and block entities. On the mod bus’s RegisterCapabilitiesEvent, it gives WWPG the LoadDevice capability.
    2. Describe: terminals() returns immutable block-local geometry. prepare() captures the enabled flag and resistance in a switch-plus-resistor circuit. It has no gameplay effects and advances no electrical history.
    3. Solve: WWPG sends that description through PG’s existing electrical schedule. Your add-on does not create or run a solver.
    4. Consume: commit() reads accepted measurements and changes the lamp indication. Unavailable results clear the indication and readout, while configuration remains saved.

    Both contacts are near the top of the block’s north face (negative-Z side). Native terminal 0 is positive; 1 is the return. This simple block has fixed orientation. If you add rotation later, transform the positions consistently in both terminal descriptions without changing their IDs.

    An off state is still a wired device. The starter keeps its terminal list, internal node and two element IDs, changing ElectricalModel.Switch.closed. Returning CircuitDescription.empty() here would be wrong: that declares no terminals while terminals() still declares two. The runtime checks complete terminal records as a set, including position and hit radius.

    LoadBlock handles empty-hand controls and lets held spools/meters reach their native item handlers. LoadDevice stores settings in versioned block-entity NBT. The inherited slotId() is "main": this block has one device slot. The empty ground set adds no deliberate earth connection; this two-wire circuit measures voltage differences against the connected source, while PG supplies its native reference for a floating network. An explicit ground fixes a declared node to the common reference; do not add one merely to force a meter reading between unrelated floating networks. This resistive load has no electrical storage history; a capacitor/inductor add-on must additionally save accepted electricalState() values and must not overwrite them on unavailable results.

    Run from the starter directory containing settings.gradle:

    Terminal window
    chmod +x gradlew
    ./gradlew build "-PapiRepository=/absolute/path/to/wwpg-api-maven" --no-daemon
    Terminal window
    .\gradlew.bat build "-PapiRepository=C:/Dev/WWPG API Maven" --no-daemon

    Both commands use an absolute repository path; quote the complete property argument when it contains spaces. Expect BUILD SUCCESSFUL and build/libs/tutorial-load-0.1.0.jar. The project compiles against NeoForge 21.1.231; adding "-Pneo_version=21.1.248" selects the other tested loader for compilation.

    If you do not yet have a NeoForge instance, follow the official client setup or dedicated-server setup, selecting Minecraft 1.21.1 and NeoForge 21.1.248 (or the original tested 21.1.231). Start it once, stop it, and locate its game directory containing mods and logs.

    Stop that separate Minecraft instance, then install the starter jar, WWPG 0.2.2, and the required mods from Installation. Add the supplied wwpg-api-example-1.0.0.jar from the example-worlds bundle’s workshop-mods directory for its 20 V source. Install matching jars on the dedicated server and every client. Keep SDK API/source/Javadoc jars out of mods. Remove older copies of your starter jar before restarting.

    There is deliberately no runClient or runServer task in this project. Use the build-and-install loop, including debugger attachment, rather than trying commands the project does not provide.

    Create a Creative test world. Obtain API Source (wwpg_api_example:source) and Tutorial Adjustable Load (tutorial_load:load) from the Creative Redstone Blocks tab, or use /give with those IDs.

    Place them a few blocks apart. Obtain a stack of 64 Copper Wire (/give @s powergrid:wire 64) before starting; native placement checks the material available for the wire’s length. With that PG wire, right-click the source’s red contact (terminal 0), then the contact near the top of the load’s north face at the western side (terminal 0: local x = 0.25, y = 0.85). A hanging wire appears when the second endpoint is accepted. Repeat from the source’s blue contact (terminal 1) to the load’s eastern contact (terminal 1: local x = 0.75, y = 0.85). Terminal hit radius is 12% of a block; the lamp texture has no painted contacts. Facing south at the north face, the western contact is on your right and the eastern contact is on your left. Aim close to those positions; reversed contacts still heat the resistor but give negative signed current/voltage. This connects source terminal 0 → load 0, then source 1 → load 1. Both wires are required. Leave the source enabled at 20 V. The load starts ON / 100 Ω and lights when it consumes power. CEE wire tools can build the same two connections instead; do not place overlapping duplicate wires for this first circuit.

    Action on the load, with an empty hand Expected result
    Start enabled at 100 Ω About 20 V, 0.2 A, 4 W; lit
    Sneak-right-click once 200 Ω: about 0.1 A, 2 W
    Sneak-right-click again 400 Ω: about 0.05 A, 1 W
    Sneak-right-click again Returns to 100 Ω
    Right-click the top face Toggles OFF/ON; OFF draws approximately zero current and goes dark

    Right-click a side or the front with an empty hand to read without changing anything. After a setting change, the immediate message says unavailable: waiting for next solve. Wait a moment, then click the front again for the newly accepted voltage, current and power. The closed switch adds 0.01 Ω, and real wires add resistance, so expect small departures from the ideal values above. For the electrical-only comparison, I = 20 / (R + 0.01 + total wire resistance) and resistor power is I²R.

    Use PG’s multimeter across the terminals to check voltage and on a wire to check current. Hold CEE’s clamp meter on either wire for the same current check. The native instruments must still work when held during block interaction. An open load may still have 20 V across its external terminals; an off state is not the same as an unavailable result.

    Set the load to OFF / 400 Ω, save and stop the world, then reopen it. Check that its settings remain OFF / 400 Ω before enabling it; a fresh default must not silently hide state loss. Read settings with an empty-hand front click. For a real chunk-unload check, build away from spawn/forced chunks, such as (24000, 64, 24000), set the server view and simulation distances to 5, teleport to (0, 100, 0), and wait at least 20 seconds before returning. Use a breakpoint in LoadDevice.onLoad() to confirm a fresh load actually occurred; walking away alone does not prove an unload. Inspect saved fields at the end of loadAdditional() before resuming, as explained in the debugger guide. To reset the circuit, use PG wire cutters on the two wires and reconnect them; break and replace the load for default ON / 100 Ω settings. Removing and replacing the block creates a new load with default settings and new wire ownership.

    An unavailable readout can mean loading, isolation or failed solving; it is not a measured zero. Inspect the author troubleshooting guide and the constructor limits. The complete general example demonstrates storage and other models after this first device works. CEE properties and PG boards have different registration prerequisites; use the native example for those routes.

    Dedicated-server checks pass on Windows/Linux and both backends. Separate Linux software-rendered client checks pass on native and Java backends in Dev Suite, including controls, visible power and the multimeter. This does not certify Steam Deck hardware. The next SDK still requires acceptance against its own frozen release artifacts. The original 0.2.1 API Workshop and its verification are separate.

    Each file below is complete, not pseudocode. Create the shown path under your project root. The source ZIP also contains the wrapper and MIT license.

    LICENSE
    MIT License
    Copyright (c) 2026 Cha0sCollective and contributors
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
    build.gradle
    plugins {
    id 'java-library'
    id 'net.neoforged.moddev' version '2.0.89'
    }
    group = 'org.example.tutorialload'
    version = '0.1.0'
    base { archivesName = 'tutorial-load' }
    java { toolchain.languageVersion = JavaLanguageVersion.of(21) }
    repositories {
    maven { url = uri(providers.gradleProperty('apiRepository').get()) }
    mavenCentral()
    }
    neoForge {
    version = providers.gradleProperty('neo_version').getOrElse('21.1.231')
    mods { tutorial_load { sourceSet sourceSets.main } }
    }
    dependencies { compileOnly 'org.cha0scollective.wwpg:wwpg-api:1.0.0' }
    tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' }
    tasks.withType(Jar).configureEach {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
    includeEmptyDirs = false
    }
    tasks.named('jar') {
    from('LICENSE') { into 'META-INF'; rename { 'TUTORIAL-LOAD-LICENSE.txt' } }
    }
    // Read the actual resolved artifact, so a successful compile cannot hide a wrong SDK.
    tasks.register('sdkProvenance') {
    dependsOn 'jar'
    doLast {
    def artifacts = configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts
    def api = artifacts.findAll { it.moduleVersion.id.group == 'org.cha0scollective.wwpg' }
    if (api.size() != 1 || api[0].moduleVersion.id.name != 'wwpg-api' || api[0].moduleVersion.id.version != '1.0.0')
    throw new GradleException('Starter must compile against only the published general API 1.0.0')
    def digest = { file -> java.security.MessageDigest.getInstance('SHA-256').digest(file.bytes).encodeHex().toString() }
    def out = layout.buildDirectory.file('sdk-provenance.json').get().asFile
    out.text = groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson([
    coordinate:api[0].moduleVersion.id.toString(), api_sha256:digest(api[0].file),
    jar_sha256:digest(tasks.jar.archiveFile.get().asFile),
    wwpg_project_dependencies:configurations.compileClasspath.allDependencies.withType(org.gradle.api.artifacts.ProjectDependency).collect { it.name }
    ]))+'\n'
    }
    }
    gradle.properties
    org.gradle.jvmargs=-Xmx2G
    org.gradle.parallel=false
    settings.gradle
    pluginManagement { repositories { gradlePluginPortal(); maven { url = 'https://maven.neoforged.net/releases' } } }
    rootProject.name = 'tutorial-load'
    src/main/java/org/example/tutorialload/LoadBlock.java
    package org.example.tutorialload;
    import net.minecraft.core.BlockPos;
    import net.minecraft.core.Direction;
    import net.minecraft.network.chat.Component;
    import net.minecraft.world.InteractionHand;
    import net.minecraft.world.InteractionResult;
    import net.minecraft.world.ItemInteractionResult;
    import net.minecraft.world.entity.player.Player;
    import net.minecraft.world.item.ItemStack;
    import net.minecraft.world.level.Level;
    import net.minecraft.world.level.block.Block;
    import net.minecraft.world.level.block.EntityBlock;
    import net.minecraft.world.level.block.entity.BlockEntity;
    import net.minecraft.world.level.block.state.BlockBehaviour;
    import net.minecraft.world.level.block.state.BlockState;
    import net.minecraft.world.level.block.state.StateDefinition;
    import net.minecraft.world.level.block.state.properties.BooleanProperty;
    import net.minecraft.world.phys.BlockHitResult;
    /** Gameplay interaction and the visible powered indication. No solver code. */
    public final class LoadBlock extends Block implements EntityBlock {
    public static final BooleanProperty LIT = BooleanProperty.create("lit");
    public LoadBlock() {
    super(BlockBehaviour.Properties.of().strength(2).lightLevel(state -> state.getValue(LIT) ? 15 : 0));
    registerDefaultState(stateDefinition.any().setValue(LIT, false));
    }
    @Override protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) { builder.add(LIT); }
    @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new LoadDevice(pos, state); }
    @Override protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
    Player player, InteractionHand hand, BlockHitResult hit) {
    // A held spool or meter must reach its native Item.useOn handler.
    return stack.isEmpty() ? ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION : ItemInteractionResult.SKIP_DEFAULT_BLOCK_INTERACTION;
    }
    @Override protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) {
    if (!level.isClientSide && level.getBlockEntity(pos) instanceof LoadDevice device) {
    if (player.isShiftKeyDown()) device.cycleResistance();
    else if (hit.getDirection() == Direction.UP) device.toggleEnabled();
    // Other faces read without changing the configuration.
    player.displayClientMessage(Component.literal(device.settings() + " | " + device.reading()), false);
    }
    return InteractionResult.sidedSuccess(level.isClientSide);
    }
    }
    src/main/java/org/example/tutorialload/LoadDevice.java
    package org.example.tutorialload;
    import net.minecraft.core.BlockPos;
    import net.minecraft.core.HolderLookup;
    import net.minecraft.nbt.CompoundTag;
    import net.minecraft.server.level.ServerLevel;
    import net.minecraft.world.level.block.entity.BlockEntity;
    import net.minecraft.world.level.block.state.BlockState;
    import org.cha0scollective.wwpg.api.*;
    import org.cha0scollective.wwpg.api.neoforge.ElectricalDevices;
    import java.util.List;
    import java.util.Locale;
    import java.util.Set;
    /** WWPG integration and native saved settings. No PG or CEE implementation imports. */
    public final class LoadDevice extends BlockEntity implements ElectricalDevice {
    private static final double[] RESISTANCES = {100, 200, 400};
    private static final List<Terminal> TERMINALS = List.of(
    new Terminal("positive", 0, new LocalPoint(.25, .85, 0), .12),
    new Terminal("negative", 1, new LocalPoint(.75, .85, 0), .12));
    private boolean enabled = true;
    private int resistanceIndex;
    private String reading = "unavailable: waiting for electrical results";
    public LoadDevice(BlockPos pos, BlockState state) { super(TutorialLoadMod.LOAD_TYPE.get(), pos, state); }
    @Override public String providerId() { return TutorialLoadMod.MOD_ID + ":load"; }
    @Override public List<Terminal> terminals() { return TERMINALS; }
    @Override public CircuitDescription prepare(TickContext context) {
    // The off state keeps the same terminals, internal node and element IDs.
    return new CircuitDescription(TERMINALS, List.of(new InternalNode("load_in")), List.of(
    new Element("enable", "positive", "load_in", new ElectricalModel.Switch(enabled, .01)),
    new Element("load", "load_in", "negative", new ElectricalModel.Resistor(resistanceOhms()))), Set.of());
    }
    @Override public void commit(DeviceResults results) {
    boolean lit = false;
    if (results.status() == MeasurementStatus.VALID) {
    var load = results.element("load").orElseThrow();
    double volts = results.node("positive").orElseThrow().lastVoltageVolts()
    - results.node("negative").orElseThrow().lastVoltageVolts();
    reading = String.format(Locale.ROOT, "%.3f V / %.3f A / %.3f W", volts, load.meanCurrentAmps(), load.meanPowerWatts());
    lit = load.meanPowerWatts() > .01;
    } else {
    reading = "unavailable: " + results.status() + " — " + results.detail();
    // Do not change enabled/resistance, or overwrite any saved storage history.
    }
    if (level instanceof ServerLevel && getBlockState().getValue(LoadBlock.LIT) != lit)
    level.setBlock(worldPosition, getBlockState().setValue(LoadBlock.LIT, lit), 3);
    }
    public boolean enabled() { return enabled; }
    public double resistanceOhms() { return RESISTANCES[resistanceIndex]; }
    public String reading() { return reading; }
    public String settings() { return (enabled ? "ON" : "OFF") + " / " + (int) resistanceOhms() + " ohms"; }
    public void toggleEnabled() { enabled = !enabled; configurationChanged(); }
    public void cycleResistance() { resistanceIndex = (resistanceIndex + 1) % RESISTANCES.length; configurationChanged(); }
    private void configurationChanged() {
    reading = "unavailable: waiting for next solve after configuration change";
    setChanged();
    if (level instanceof ServerLevel server) {
    if (getBlockState().getValue(LoadBlock.LIT))
    server.setBlock(worldPosition, getBlockState().setValue(LoadBlock.LIT, false), 3);
    ElectricalDevices.changed(server, worldPosition);
    }
    }
    @Override public void onLoad() {
    super.onLoad();
    if (level instanceof ServerLevel server) ElectricalDevices.changed(server, worldPosition);
    }
    @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
    super.saveAdditional(tag, registries);
    tag.putInt("TutorialLoadVersion", 1);
    tag.putBoolean("Enabled", enabled);
    tag.putInt("ResistanceIndex", resistanceIndex);
    }
    @Override protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
    super.loadAdditional(tag, registries);
    if (!tag.contains("TutorialLoadVersion")) return; // A new block keeps its defaults.
    if (tag.getInt("TutorialLoadVersion") != 1) throw new IllegalArgumentException("Unknown tutorial load save format");
    int restored = tag.getInt("ResistanceIndex");
    if (restored < 0 || restored >= RESISTANCES.length) throw new IllegalArgumentException("Invalid saved tutorial resistance index");
    enabled = tag.getBoolean("Enabled");
    resistanceIndex = restored;
    }
    }
    src/main/java/org/example/tutorialload/TutorialLoadMod.java
    package org.example.tutorialload;
    import net.minecraft.core.registries.Registries;
    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.entity.BlockEntityType;
    import net.neoforged.bus.api.IEventBus;
    import net.neoforged.fml.common.Mod;
    import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
    import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
    import net.neoforged.neoforge.registries.DeferredRegister;
    import org.cha0scollective.wwpg.api.neoforge.ElectricalProviders;
    import java.util.function.Supplier;
    /** Ordinary NeoForge registration, plus one WWPG capability registration. */
    @Mod(TutorialLoadMod.MOD_ID)
    public final class TutorialLoadMod {
    public static final String MOD_ID = "tutorial_load";
    private static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(Registries.BLOCK, MOD_ID);
    private static final DeferredRegister<Item> ITEMS = DeferredRegister.create(Registries.ITEM, MOD_ID);
    private static final DeferredRegister<BlockEntityType<?>> TYPES = DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, MOD_ID);
    public static final Supplier<LoadBlock> LOAD = BLOCKS.register("load", LoadBlock::new);
    public static final Supplier<Item> LOAD_ITEM = ITEMS.register("load", () -> new BlockItem(LOAD.get(), new Item.Properties()));
    public static final Supplier<BlockEntityType<LoadDevice>> LOAD_TYPE = TYPES.register("load", () ->
    BlockEntityType.Builder.of(LoadDevice::new, LOAD.get()).build(null));
    public TutorialLoadMod(IEventBus modBus) {
    BLOCKS.register(modBus);
    ITEMS.register(modBus);
    TYPES.register(modBus);
    modBus.addListener(TutorialLoadMod::registerElectricalDevice);
    modBus.addListener(TutorialLoadMod::creativeTab);
    }
    private static void registerElectricalDevice(RegisterCapabilitiesEvent event) {
    ElectricalProviders.registerBlockEntity(event, MOD_ID + ":load", LOAD_TYPE.get(), (entity, context) -> entity);
    }
    private static void creativeTab(BuildCreativeModeTabContentsEvent event) {
    if (event.getTabKey() == CreativeModeTabs.REDSTONE_BLOCKS) event.accept(LOAD_ITEM.get());
    }
    }
    src/main/resources/META-INF/neoforge.mods.toml
    modLoader="javafml"
    loaderVersion="[4,)"
    license="MIT"
    [[mods]]
    modId="tutorial_load"
    version="0.1.0"
    displayName="Tutorial Adjustable Load"
    description="One stationary load built against the published WWPG API."
    [[dependencies.tutorial_load]]
    modId="minecraft"
    type="required"
    versionRange="[1.21.1]"
    side="BOTH"
    [[dependencies.tutorial_load]]
    modId="neoforge"
    type="required"
    versionRange="[21.1.231,)"
    side="BOTH"
    [[dependencies.tutorial_load]]
    modId="wwpg"
    type="required"
    versionRange="[0.2.1,)"
    ordering="AFTER"
    side="BOTH"
    src/main/resources/assets/tutorial_load/blockstates/load.json
    {
    "variants": {
    "lit=false": {
    "model": "tutorial_load:block/load"
    },
    "lit=true": {
    "model": "tutorial_load:block/load_lit"
    }
    }
    }
    src/main/resources/assets/tutorial_load/lang/en_us.json
    {
    "block.tutorial_load.load": "Tutorial Adjustable Load"
    }
    src/main/resources/assets/tutorial_load/models/block/load.json
    {
    "parent": "minecraft:block/cube_all",
    "textures": {
    "all": "minecraft:block/redstone_lamp"
    }
    }
    src/main/resources/assets/tutorial_load/models/block/load_lit.json
    {
    "parent": "minecraft:block/cube_all",
    "textures": {
    "all": "minecraft:block/redstone_lamp_on"
    }
    }
    src/main/resources/assets/tutorial_load/models/item/load.json
    {
    "parent": "tutorial_load:block/load"
    }