mirror of
https://github.com/SquidDev-CC/CC-Tweaked
synced 2025-10-23 09:57:39 +00:00
Clean up Javadocs a little
I've no motivation for modding right now, but always got time for build
system busywork!
CC:T (and CC before that) has always published its API docs. However,
they're not always the most helpful — they're useful if you know what
you're looking for, but aren't a good getting-started guide.
Part of the issue here is there's no examples, and everything is
described pretty abstractly. I have occasionally tried to improve this
(e.g. the peripheral docs in bdffabc08e
),
but it's a long road.
This commit adds a new example mod, which registers peripherals, an API
and a turtle upgrade. While the mod itself isn't exported as part of the
docs, we reference blocks of it using Java's new {@snippet} tag.
- Switch the Forge project to use NeoForge's new Legacy MDG plugin. We
don't *need* to do this, but it means the build logic for Forge and
NeoForge is more closely aligned.
- Add a new SnippetTaglet, which is a partial backport of Java 18+'s
{@snippet}.
- Add an example mod. This is a working multi-loader mod, complete with
datagen (albeit with no good multi-loader abstractions).
- Move our existing <pre>{@code ...}</pre> blocks into the example mod,
replacing them with {@snippet}s.
- Add a new overview page to the docs, providing some getting-started
information. We had this already in the dan200.computercraft.api
package docs, but it's not especially visible there.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
import cc.tweaked.gradle.*
|
||||
import net.minecraftforge.gradle.common.util.RunConfig
|
||||
import net.neoforged.moddevgradle.dsl.RunModel
|
||||
|
||||
plugins {
|
||||
id("cc-tweaked.forge")
|
||||
@@ -19,105 +19,122 @@ cct {
|
||||
allProjects.forEach { externalSources(it) }
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
resources.srcDir("src/generated/resources")
|
||||
legacyForge {
|
||||
val computercraft by mods.registering {
|
||||
cct.sourceDirectories.get().forEach {
|
||||
if (it.classes) sourceSet(it.sourceSet)
|
||||
}
|
||||
}
|
||||
|
||||
val computercraftDatagen by mods.registering {
|
||||
cct.sourceDirectories.get().forEach {
|
||||
if (it.classes) sourceSet(it.sourceSet)
|
||||
}
|
||||
sourceSet(sourceSets.datagen.get())
|
||||
}
|
||||
|
||||
val testMod by mods.registering {
|
||||
sourceSet(sourceSets.testMod.get())
|
||||
sourceSet(sourceSets.testFixtures.get())
|
||||
sourceSet(project(":core").sourceSets["testFixtures"])
|
||||
}
|
||||
|
||||
val exampleMod by mods.registering {
|
||||
sourceSet(sourceSets.examples.get())
|
||||
}
|
||||
}
|
||||
|
||||
minecraft {
|
||||
runs {
|
||||
// configureEach would be better, but we need to eagerly configure configs or otherwise the run task doesn't
|
||||
// get set up properly.
|
||||
all {
|
||||
property("forge.logging.markers", "REGISTRIES")
|
||||
property("forge.logging.console.level", "debug")
|
||||
|
||||
mods.register("computercraft") {
|
||||
cct.sourceDirectories.get().forEach {
|
||||
if (it.classes) sources(it.sourceSet)
|
||||
}
|
||||
}
|
||||
configureEach {
|
||||
ideName = "Forge - ${name.capitalise()}"
|
||||
systemProperty("forge.logging.markers", "REGISTRIES")
|
||||
systemProperty("forge.logging.console.level", "debug")
|
||||
loadedMods.add(computercraft)
|
||||
}
|
||||
|
||||
val client by registering {
|
||||
workingDirectory(file("run"))
|
||||
register("client") {
|
||||
client()
|
||||
}
|
||||
|
||||
val server by registering {
|
||||
workingDirectory(file("run/server"))
|
||||
arg("--nogui")
|
||||
register("server") {
|
||||
server()
|
||||
gameDirectory = file("run/server")
|
||||
programArgument("--nogui")
|
||||
}
|
||||
|
||||
val data by registering {
|
||||
workingDirectory(file("run"))
|
||||
args(
|
||||
"--mod", "computercraft", "--all",
|
||||
"--output", layout.buildDirectory.dir("generatedResources").getAbsolutePath(),
|
||||
"--existing", project(":common").file("src/main/resources/"),
|
||||
"--existing", file("src/main/resources/"),
|
||||
fun RunModel.configureForData(mod: String, sourceSet: SourceSet) {
|
||||
data()
|
||||
gameDirectory = file("run/run${name.capitalise()}")
|
||||
programArguments.addAll(
|
||||
"--mod", mod, "--all",
|
||||
"--output",
|
||||
layout.buildDirectory.dir(sourceSet.getTaskName("generateResources", null))
|
||||
.getAbsolutePath(),
|
||||
"--existing", project.project(":common").file("src/${sourceSet.name}/resources/").absolutePath,
|
||||
"--existing", project.file("src/${sourceSet.name}/resources/").absolutePath,
|
||||
)
|
||||
}
|
||||
|
||||
register("data") {
|
||||
configureForData("computercraft", sourceSets.main.get())
|
||||
loadedMods = listOf(computercraftDatagen.get())
|
||||
}
|
||||
|
||||
fun RunModel.configureForGameTest() {
|
||||
systemProperty(
|
||||
"cctest.sources",
|
||||
project.project(":common").file("src/testMod/resources/data/cctest").absolutePath,
|
||||
)
|
||||
|
||||
mods.named("computercraft") {
|
||||
source(sourceSets["datagen"])
|
||||
}
|
||||
programArgument("--mixin.config=computercraft-gametest.mixins.json")
|
||||
loadedMods.add(testMod)
|
||||
|
||||
jvmArgument("-ea")
|
||||
}
|
||||
|
||||
fun RunConfig.configureForGameTest() {
|
||||
val old = lazyTokens["minecraft_classpath"]
|
||||
lazyToken("minecraft_classpath") {
|
||||
// Add all files in testMinecraftLibrary to the classpath.
|
||||
val allFiles = mutableSetOf<String>()
|
||||
|
||||
val oldVal = old?.get()
|
||||
if (!oldVal.isNullOrEmpty()) allFiles.addAll(oldVal.split(File.pathSeparatorChar))
|
||||
|
||||
for (file in configurations["testMinecraftLibrary"].resolve()) allFiles.add(file.absolutePath)
|
||||
|
||||
allFiles.joinToString(File.pathSeparator)
|
||||
}
|
||||
|
||||
property("cctest.sources", project(":common").file("src/testMod/resources/data/cctest").absolutePath)
|
||||
|
||||
arg("--mixin.config=computercraft-gametest.mixins.json")
|
||||
|
||||
mods.register("cctest") {
|
||||
source(sourceSets["testMod"])
|
||||
source(sourceSets["testFixtures"])
|
||||
source(project(":core").sourceSets["testFixtures"])
|
||||
}
|
||||
}
|
||||
|
||||
val testClient by registering {
|
||||
workingDirectory(file("run/testClient"))
|
||||
parent(client.get())
|
||||
register("testClient") {
|
||||
client()
|
||||
gameDirectory = file("run/testClient")
|
||||
configureForGameTest()
|
||||
|
||||
property("cctest.tags", "client,common")
|
||||
systemProperty("cctest.tags", "client,common")
|
||||
}
|
||||
|
||||
val gameTestServer by registering {
|
||||
workingDirectory(file("run/testServer"))
|
||||
register("gametest") {
|
||||
type = "gameTestServer"
|
||||
configureForGameTest()
|
||||
|
||||
property("forge.logging.console.level", "info")
|
||||
jvmArg("-ea")
|
||||
systemProperty("forge.logging.console.level", "info")
|
||||
systemProperty(
|
||||
"cctest.gametest-report",
|
||||
layout.buildDirectory.dir("test-results/runGametest.xml").getAbsolutePath(),
|
||||
)
|
||||
gameDirectory = file("run/gametest")
|
||||
}
|
||||
|
||||
register("exampleClient") {
|
||||
client()
|
||||
loadedMods.add(exampleMod.get())
|
||||
}
|
||||
|
||||
register("exampleData") {
|
||||
configureForData("examplemod", sourceSets.examples.get())
|
||||
loadedMods.add(exampleMod.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
configurations {
|
||||
minecraftLibrary { extendsFrom(minecraftEmbed.get()) }
|
||||
additionalRuntimeClasspath { extendsFrom(jarJar.get()) }
|
||||
|
||||
// Move minecraftLibrary/minecraftEmbed out of implementation, and into runtimeOnly.
|
||||
implementation { setExtendsFrom(extendsFrom - setOf(minecraftLibrary.get(), minecraftEmbed.get())) }
|
||||
runtimeOnly { extendsFrom(minecraftLibrary.get(), minecraftEmbed.get()) }
|
||||
|
||||
val testMinecraftLibrary by registering {
|
||||
val testAdditionalRuntimeClasspath by registering {
|
||||
isCanBeResolved = true
|
||||
isCanBeConsumed = false
|
||||
// Prevent ending up with multiple versions of libraries on the classpath.
|
||||
shouldResolveConsistentlyWith(minecraftLibrary.get())
|
||||
shouldResolveConsistentlyWith(additionalRuntimeClasspath.get())
|
||||
}
|
||||
|
||||
for (testConfig in listOf("testClientAdditionalRuntimeClasspath", "gametestAdditionalRuntimeClasspath")) {
|
||||
named(testConfig) { extendsFrom(testAdditionalRuntimeClasspath.get()) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,31 +143,22 @@ dependencies {
|
||||
annotationProcessorEverywhere(libs.autoService)
|
||||
|
||||
clientCompileOnly(variantOf(libs.emi) { classifier("api") })
|
||||
libs.bundles.externalMods.forge.compile.get().map { compileOnly(fg.deobf(it)) }
|
||||
libs.bundles.externalMods.forge.runtime.get().map { runtimeOnly(fg.deobf(it)) }
|
||||
|
||||
// fg.debof only accepts a closure to configure the dependency, so doesn't work with Kotlin. We create and configure
|
||||
// the dep first, and then pass it off to ForgeGradle.
|
||||
(create(variantOf(libs.create.forge) { classifier("slim") }.get()) as ExternalModuleDependency)
|
||||
.apply { isTransitive = false }.let { compileOnly(fg.deobf(it)) }
|
||||
modCompileOnly(libs.bundles.externalMods.forge.compile)
|
||||
modRuntimeOnly(libs.bundles.externalMods.forge.runtime)
|
||||
modCompileOnly(variantOf(libs.create.forge) { classifier("slim") })
|
||||
|
||||
// Depend on our other projects.
|
||||
api(commonClasses(project(":forge-api"))) { cct.exclude(this) }
|
||||
clientApi(clientClasses(project(":forge-api"))) { cct.exclude(this) }
|
||||
implementation(project(":core")) { cct.exclude(this) }
|
||||
|
||||
minecraftEmbed(libs.cobalt) {
|
||||
val version = libs.versions.cobalt.get()
|
||||
jarJar.ranged(this, "[$version,${getNextVersion(version)})")
|
||||
}
|
||||
minecraftEmbed(libs.jzlib) {
|
||||
jarJar.ranged(this, "[${libs.versions.jzlib.get()},)")
|
||||
}
|
||||
jarJar(libs.cobalt)
|
||||
jarJar(libs.jzlib)
|
||||
// We don't jar-in-jar our additional netty dependencies (see the tasks.jarJar configuration), but still want them
|
||||
// on the legacy classpath.
|
||||
minecraftLibrary(libs.netty.http) { isTransitive = false }
|
||||
minecraftLibrary(libs.netty.socks) { isTransitive = false }
|
||||
minecraftLibrary(libs.netty.proxy) { isTransitive = false }
|
||||
additionalRuntimeClasspath(libs.netty.http) { isTransitive = false }
|
||||
additionalRuntimeClasspath(libs.netty.socks) { isTransitive = false }
|
||||
additionalRuntimeClasspath(libs.netty.proxy) { isTransitive = false }
|
||||
|
||||
testFixturesApi(libs.bundles.test)
|
||||
testFixturesApi(libs.bundles.kotlin)
|
||||
@@ -163,8 +171,8 @@ dependencies {
|
||||
testModImplementation(testFixtures(project(":forge")))
|
||||
|
||||
// Ensure our test fixture dependencies are on the classpath
|
||||
"testMinecraftLibrary"(libs.bundles.kotlin)
|
||||
"testMinecraftLibrary"(libs.bundles.test)
|
||||
"testAdditionalRuntimeClasspath"(libs.bundles.kotlin)
|
||||
"testAdditionalRuntimeClasspath"(libs.bundles.test)
|
||||
|
||||
testFixturesImplementation(testFixtures(project(":core")))
|
||||
}
|
||||
@@ -181,23 +189,6 @@ tasks.processResources {
|
||||
}
|
||||
|
||||
tasks.jar {
|
||||
finalizedBy("reobfJar")
|
||||
archiveClassifier.set("slim")
|
||||
|
||||
for (source in cct.sourceDirectories.get()) {
|
||||
if (source.classes && source.external) from(source.sourceSet.output)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.sourcesJar {
|
||||
for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource)
|
||||
}
|
||||
|
||||
tasks.jarJar {
|
||||
finalizedBy("reobfJarJar")
|
||||
archiveClassifier.set("")
|
||||
duplicatesStrategy = DuplicatesStrategy.FAIL
|
||||
|
||||
// Include all classes from other projects except core.
|
||||
val coreSources = project(":core").sourceSets["main"]
|
||||
for (source in cct.sourceDirectories.get()) {
|
||||
@@ -210,7 +201,9 @@ tasks.jarJar {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.assemble { dependsOn("jarJar") }
|
||||
tasks.sourcesJar {
|
||||
for (source in cct.sourceDirectories.get()) from(source.sourceSet.allSource)
|
||||
}
|
||||
|
||||
// Check tasks
|
||||
|
||||
@@ -218,22 +211,15 @@ tasks.test {
|
||||
systemProperty("cct.test-files", layout.buildDirectory.dir("tmp/testFiles").getAbsolutePath())
|
||||
}
|
||||
|
||||
val runGametest by tasks.registering(JavaExec::class) {
|
||||
group = LifecycleBasePlugin.VERIFICATION_GROUP
|
||||
description = "Runs tests on a temporary Minecraft instance."
|
||||
dependsOn("cleanRunGametest")
|
||||
val runGametest = tasks.named<JavaExec>("runGametest") {
|
||||
usesService(MinecraftRunnerService.get(gradle))
|
||||
|
||||
setRunConfig(minecraft.runs["gameTestServer"])
|
||||
|
||||
systemProperty("cctest.gametest-report", layout.buildDirectory.dir("test-results/$name.xml").getAbsolutePath())
|
||||
}
|
||||
cct.jacoco(runGametest)
|
||||
tasks.check { dependsOn(runGametest) }
|
||||
|
||||
val runGametestClient by tasks.registering(ClientJavaExec::class) {
|
||||
description = "Runs client-side gametests with no mods"
|
||||
setRunConfig(minecraft.runs["testClient"])
|
||||
copyFromForge("runTestClient")
|
||||
tags("client")
|
||||
}
|
||||
cct.jacoco(runGametestClient)
|
||||
@@ -247,22 +233,12 @@ tasks.register("checkClient") {
|
||||
// Upload tasks
|
||||
|
||||
modPublishing {
|
||||
output.set(tasks.jarJar)
|
||||
}
|
||||
|
||||
// Don't publish the slim jar
|
||||
for (cfg in listOf(configurations.apiElements, configurations.runtimeElements)) {
|
||||
cfg.configure { artifacts.removeIf { it.classifier == "slim" } }
|
||||
output.set(tasks.reobfJar)
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
named("maven", MavenPublication::class) {
|
||||
fg.component(this)
|
||||
// jarJar.component is broken (https://github.com/MinecraftForge/ForgeGradle/issues/914), so declare the
|
||||
// artifact explicitly.
|
||||
artifact(tasks.jarJar)
|
||||
|
||||
mavenDependencies {
|
||||
cct.configureExcludes(this)
|
||||
exclude(libs.jei.forge.get())
|
||||
|
@@ -0,0 +1,92 @@
|
||||
package com.example.examplemod;
|
||||
|
||||
import com.example.examplemod.peripheral.BrewingStandPeripheral;
|
||||
import dan200.computercraft.api.peripheral.IPeripheral;
|
||||
import dan200.computercraft.api.turtle.TurtleUpgradeSerialiser;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
|
||||
import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.common.capabilities.Capability;
|
||||
import net.minecraftforge.common.capabilities.CapabilityManager;
|
||||
import net.minecraftforge.common.capabilities.CapabilityToken;
|
||||
import net.minecraftforge.common.capabilities.ICapabilityProvider;
|
||||
import net.minecraftforge.common.util.LazyOptional;
|
||||
import net.minecraftforge.event.AttachCapabilitiesEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent;
|
||||
import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext;
|
||||
import net.minecraftforge.registries.RegisterEvent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* The main entry point for the Forge version of our example mod.
|
||||
*/
|
||||
@Mod(ExampleMod.MOD_ID)
|
||||
public class ForgeExampleMod {
|
||||
public ForgeExampleMod() {
|
||||
// Register our turtle upgrade. If writing a Forge-only mod, you'd normally use DeferredRegister instead.
|
||||
// However, this is an easy way to implement this in a multi-loader-compatible manner.
|
||||
|
||||
// @start region=turtle_upgrades
|
||||
var modBus = FMLJavaModLoadingContext.get().getModEventBus();
|
||||
modBus.addListener((RegisterEvent event) -> {
|
||||
event.register(TurtleUpgradeSerialiser.registryId(), new ResourceLocation(ExampleMod.MOD_ID, "example_turtle_upgrade"), () -> ExampleMod.EXAMPLE_TURTLE_UPGRADE);
|
||||
});
|
||||
// @end region=turtle_upgrades
|
||||
|
||||
modBus.addListener((FMLCommonSetupEvent event) -> ExampleMod.registerComputerCraft());
|
||||
|
||||
MinecraftForge.EVENT_BUS.addGenericListener(BlockEntity.class, ForgeExampleMod::attachPeripherals);
|
||||
}
|
||||
|
||||
// @start region=peripherals
|
||||
// The main function to attach peripherals to block entities. This should be added to the Forge event bus.
|
||||
public static void attachPeripherals(AttachCapabilitiesEvent<BlockEntity> event) {
|
||||
if (event.getObject() instanceof BrewingStandBlockEntity brewingStand) {
|
||||
PeripheralProvider.attach(event, brewingStand, BrewingStandPeripheral::new);
|
||||
}
|
||||
}
|
||||
|
||||
// Boilerplate for adding a new capability provider
|
||||
|
||||
public static final Capability<IPeripheral> CAPABILITY_PERIPHERAL = CapabilityManager.get(new CapabilityToken<>() {
|
||||
});
|
||||
private static final ResourceLocation PERIPHERAL = new ResourceLocation(ExampleMod.MOD_ID, "peripheral");
|
||||
|
||||
// A {@link ICapabilityProvider} that lazily creates an {@link IPeripheral} when required.
|
||||
private static final class PeripheralProvider<O extends BlockEntity> implements ICapabilityProvider {
|
||||
private final O blockEntity;
|
||||
private final Function<O, IPeripheral> factory;
|
||||
private @Nullable LazyOptional<IPeripheral> peripheral;
|
||||
|
||||
private PeripheralProvider(O blockEntity, Function<O, IPeripheral> factory) {
|
||||
this.blockEntity = blockEntity;
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
private static <O extends BlockEntity> void attach(AttachCapabilitiesEvent<BlockEntity> event, O blockEntity, Function<O, IPeripheral> factory) {
|
||||
var provider = new PeripheralProvider<>(blockEntity, factory);
|
||||
event.addCapability(PERIPHERAL, provider);
|
||||
event.addListener(provider::invalidate);
|
||||
}
|
||||
|
||||
private void invalidate() {
|
||||
if (peripheral != null) peripheral.invalidate();
|
||||
peripheral = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> LazyOptional<T> getCapability(Capability<T> capability, @Nullable Direction direction) {
|
||||
if (capability != CAPABILITY_PERIPHERAL) return LazyOptional.empty();
|
||||
if (blockEntity.isRemoved()) return LazyOptional.empty();
|
||||
|
||||
var peripheral = this.peripheral;
|
||||
return (peripheral == null ? (this.peripheral = LazyOptional.of(() -> factory.apply(blockEntity))) : peripheral).cast();
|
||||
}
|
||||
}
|
||||
// @end region=peripherals
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
package com.example.examplemod;
|
||||
|
||||
import dan200.computercraft.api.client.turtle.RegisterTurtleModellersEvent;
|
||||
import dan200.computercraft.api.client.turtle.TurtleUpgradeModeller;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* The client-side entry point for the Forge version of our example mod.
|
||||
*/
|
||||
@Mod.EventBusSubscriber(modid = ExampleMod.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
|
||||
public class ForgeExampleModClient {
|
||||
// @start region=turtle_modellers
|
||||
@SubscribeEvent
|
||||
public static void onRegisterTurtleModellers(RegisterTurtleModellersEvent event) {
|
||||
event.register(ExampleMod.EXAMPLE_TURTLE_UPGRADE, TurtleUpgradeModeller.flatItem());
|
||||
}
|
||||
// @end region=turtle_modellers
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
package com.example.examplemod;
|
||||
|
||||
import com.example.examplemod.data.ExampleModDataGenerators;
|
||||
import net.minecraftforge.data.event.GatherDataEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.fml.common.Mod;
|
||||
|
||||
/**
|
||||
* The data generator entrypoint for the Forge version of our example mod.
|
||||
*
|
||||
* @see ExampleModDataGenerators The main implementation
|
||||
*/
|
||||
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD)
|
||||
public class ForgeExampleModDataGenerator {
|
||||
@SubscribeEvent
|
||||
public static void gather(GatherDataEvent event) {
|
||||
ExampleModDataGenerators.run(event.getGenerator().getVanillaPack(true));
|
||||
}
|
||||
}
|
14
projects/forge/src/examples/resources/META-INF/mods.toml
Normal file
14
projects/forge/src/examples/resources/META-INF/mods.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
modLoader="javafml"
|
||||
loaderVersion="[1,)"
|
||||
license="CC0-1.0"
|
||||
|
||||
[[mods]]
|
||||
modId="examplemod"
|
||||
version="1.0.0"
|
||||
|
||||
[[dependencies.examplemod]]
|
||||
modId="computercraft"
|
||||
mandatory=true
|
||||
versionRange="[1.0,)"
|
||||
ordering="AFTER"
|
||||
side="BOTH"
|
6
projects/forge/src/examples/resources/pack.mcmeta
Normal file
6
projects/forge/src/examples/resources/pack.mcmeta
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"pack": {
|
||||
"pack_format": 15,
|
||||
"description": "Example Mod"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user