diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3f7fedb..3cb888e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,31 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + network: + uses: ./.github/workflows/build_and_test.yml + with: + module: network + module_id: anvillib-network + mod_id: anvillib_network + ci_build: true + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + network-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - network + with: + module: network + module_id: anvillib-network + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} recipe: needs: - config @@ -146,6 +171,7 @@ jobs: - config - integration - moveable-entity-block + - network - recipe - registrum uses: ./.github/workflows/build_and_test.yml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index b4c0791..4aef354 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -67,6 +67,24 @@ jobs: signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + network: + uses: ./.github/workflows/build_and_test.yml + with: + module: network + module_id: anvillib-network + mod_id: anvillib_network + ci_build: true + pr_build: true + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} + recipe: needs: - config @@ -110,6 +128,7 @@ jobs: - config - integration - moveable-entity-block + - network - recipe - registrum uses: ./.github/workflows/build_and_test.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3ce959..c4e3495 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,6 +89,31 @@ jobs: signing_key: ${{ secrets.SIGNING_KEY }} signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} signing_password: ${{ secrets.SIGNING_PASSWORD }} + network: + uses: ./.github/workflows/build_and_test.yml + with: + module: network + module_id: anvillib-network + mod_id: anvillib_network + ci_build: false + pr_build: false + secrets: + maven_url: ${{ secrets.MAVEN_URL }} + maven_user: ${{ secrets.MAVEN_USER }} + maven_pass: ${{ secrets.MAVEN_PASS }} + network-maven-central-deploy: + uses: ./.github/workflows/publish_maven_central.yml + needs: + - network + with: + module: network + module_id: anvillib-network + secrets: + maven_central_username: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + maven_central_password: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + signing_key: ${{ secrets.SIGNING_KEY }} + signing_public_key: ${{ secrets.SIGNING_PUBLIC_KEY }} + signing_password: ${{ secrets.SIGNING_PASSWORD }} recipe: needs: - config @@ -146,6 +171,7 @@ jobs: - config - integration - moveable-entity-block + - network - recipe - registrum uses: ./.github/workflows/build_and_test.yml diff --git a/README.en.md b/README.en.md index e04bd54..a727bd5 100644 --- a/README.en.md +++ b/README.en.md @@ -3,21 +3,24 @@ [![Minecraft](https://img.shields.io/badge/Minecraft-1.21.1-green.svg)](https://minecraft.net/) [![Maven Central](https://img.shields.io/maven-central/v/dev.anvilcraft.lib/anvillib-neoforge-1.21.1)](https://central.sonatype.com/search?q=anvillib) [![NeoForge](https://img.shields.io/badge/NeoForge-21.1.x-orange.svg)](https://neoforged.net/) -[![License](https://img.shields.io/badge/License-MIT%20License-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) +[![License](https://img.shields.io/badge/License-MIT%20License-blue.svg)](https://opensource.org/licenses/MIT) -**AnvilLib** is a NeoForge mod library developed by [Anvil Dev](https://github.com/Anvil-Dev), providing Minecraft mod developers with a series of practical tools and frameworks. +**AnvilLib** is a NeoForge mod library developed by [Anvil Dev](https://github.com/Anvil-Dev), providing Minecraft mod developers with a +series of practical tools and frameworks. ## Features AnvilLib adopts a modular design and includes the following functional modules: -| Module | Description | -|---------------------------|---------------| -| **Config** | Annotation-based configuration system | -| **Integration** | Mod compatibility integration framework | -| **Recipe** | In-world recipe system | -| **Moveable Entity Block** | Support for block entities movable by pistons | -| **Registrum** | Simplified registration system | +| Module | Description | +|---------------------------|---------------------------------------------------| +| **Config** | Annotation-based configuration system | +| **Integration** | Mod compatibility integration framework | +| **Network** | Networking API with automatic packet registration | +| **Recipe** | In-world recipe system | +| **Moveable Entity Block** | Support for block entities movable by pistons | +| **Registrum** | Simplified registration system | +| **Main** | Aggregated module that bundles all submodules | ## Module Introduction @@ -36,7 +39,8 @@ Provides an annotation-based configuration management system to simplify the def **Usage Example:** ```java -@Config(name = "my_mod", typebirdsprite_ModConfig.Type.COMMON) + +@Config(name = "my_mod", type = ModConfig.Type.COMMON) public class MyModConfig { @Comment("Enable debug mode") public boolean debugMode = false; @@ -63,6 +67,7 @@ Provides a framework for mod integrations, supporting automatic loading of integ **Usage Example:** ```java + @Integration(value = "jei", version = "[19.0,)") public class JEIIntegration { public void init() { @@ -71,6 +76,27 @@ public class JEIIntegration { } ``` +### Network Module + +Provides a NeoForge networking abstraction with package-based packet auto-registration. + +**Key Features:** + +- Define packet direction using `IClientboundPacket` / `IServerboundPacket` / `IInsensitiveBiPacket` +- Automatically register packet classes in a package via `NetworkRegistrar.register(...)` +- Supports `PLAY`, `CONFIGURATION`, and `COMMON` protocols + +**Usage Example:** + +```java + +@SubscribeEvent +public static void onRegisterPayload(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1"); + NetworkRegistrar.register(registrar, "my_mod"); +} +``` + ### Recipe Module Provides an in-world recipe system, allowing recipes to be executed in the world (rather than in crafting tables). @@ -117,7 +143,8 @@ public class MyBlock extends Block implements IMoveableEntityBlock { ### Registrum Module -A registration system based on [Registrate](https://github.com/IThundxr/Registrate), simplifying the registration process for items, blocks, entities, etc. +A registration system based on [Registrate](https://github.com/IThundxr/Registrate), simplifying the registration process for items, blocks, +entities, etc. **Key Features:** @@ -137,6 +164,17 @@ public static final RegistryEntry MY_ITEM = REGISTRUM .register(); ``` +### Main Module + +`anvillib-neoforge-1.21.1` is the aggregate artifact. It bundles and re-exports: + +- `config` +- `integration` +- `network` +- `recipe` +- `moveable-entity-block` +- `registrum` + ## Dependency Integration ### Gradle (Groovy DSL) @@ -153,6 +191,7 @@ dependencies { // Or import individual modules as needed implementation "dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-integration-neoforge-1.21.1:2.0.0" + implementation "dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-moveable-entity-block-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-registrum-neoforge-1.21.1:2.0.0" @@ -168,9 +207,14 @@ repositories { dependencies { implementation("dev.anvilcraft.lib:anvillib-neoforge-1.21.1:2.0.0") + + // Optional single-module example + implementation("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:2.0.0") } ``` +> Keep the dependency version aligned with release tags (current project property is `mod_version=2.0.0`). + ## Building the Project ```bash @@ -178,8 +222,11 @@ dependencies { git clone https://github.com/Anvil-Dev/AnvilLib.git cd AnvilLib -# Build +# Build on macOS / Linux ./gradlew build + +# Build on Windows (PowerShell / CMD) +gradlew.bat build ``` ## Requirements @@ -192,7 +239,8 @@ cd AnvilLib This project is licensed under the [MIT License](https://www.opensource.org/licenses/MIT). -Part of the Registrum module code is based on [Registrate](https://github.com/IThundxr/Registrate) and follows the Mozilla Public License 2.0. +Part of the Registrum module code is based on [Registrate](https://github.com/IThundxr/Registrate) and follows the Mozilla Public License +2.0. ## Author diff --git a/README.md b/README.md index 0f5ad09..72f992f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Minecraft](https://img.shields.io/badge/Minecraft-1.21.1-green.svg)](https://minecraft.net/) [![Maven Central](https://img.shields.io/maven-central/v/dev.anvilcraft.lib/anvillib-neoforge-1.21.1)](https://central.sonatype.com/search?q=anvillib) [![NeoForge](https://img.shields.io/badge/NeoForge-21.1.x-orange.svg)](https://neoforged.net/) -[![License](https://img.shields.io/badge/License-MIT%20License-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) +[![License](https://img.shields.io/badge/License-MIT%20License-blue.svg)](https://opensource.org/licenses/MIT) **AnvilLib** 是一个由 [Anvil Dev](https://github.com/Anvil-Dev) 开发的 NeoForge 模组库,为 Minecraft 模组开发者提供一系列实用的工具和框架。 @@ -11,13 +11,15 @@ AnvilLib 采用模块化设计,包含以下功能模块: -| 模块 | 说明 | -|---------------------------|---------------| -| **Config** | 基于注解的配置系统 | -| **Integration** | 模组兼容性集成框架 | -| **Recipe** | 世界内配方系统 | -| **Moveable Entity Block** | 可被活塞推动的方块实体支持 | -| **Registrum** | 简化的注册系统 | +| 模块 | 说明 | +|---------------------------|----------------| +| **Config** | 基于注解的配置系统 | +| **Integration** | 模组兼容性集成框架 | +| **Network** | 网络通信与数据包自动注册框架 | +| **Recipe** | 世界内配方系统 | +| **Moveable Entity Block** | 可被活塞推动的方块实体支持 | +| **Registrum** | 简化的注册系统 | +| **Main** | 聚合模块(包含全部子模块) | ## 模块介绍 @@ -36,6 +38,7 @@ AnvilLib 采用模块化设计,包含以下功能模块: **使用示例:** ```java + @Config(name = "my_mod", type = ModConfig.Type.COMMON) public class MyModConfig { @Comment("启用调试模式") @@ -63,6 +66,7 @@ MyModConfig config = ConfigManager.register("my_mod", MyModConfig::new); **使用示例:** ```java + @Integration(value = "jei", version = "[19.0,)") public class JEIIntegration { public void init() { @@ -71,6 +75,27 @@ public class JEIIntegration { } ``` +### Network 模块 + +提供面向 NeoForge 的网络通信抽象,支持按包扫描并自动注册数据包。 + +**主要特性:** + +- 使用 `IClientboundPacket` / `IServerboundPacket` / `IInsensitiveBiPacket` 定义通信方向 +- 通过 `NetworkRegistrar.register(...)` 自动注册同一包下的数据包 +- 支持 `PLAY` / `CONFIGURATION` / `COMMON` 三种协议通道 + +**使用示例:** + +```java + +@SubscribeEvent +public static void onRegisterPayload(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1"); + NetworkRegistrar.register(registrar, "my_mod"); +} +``` + ### Recipe 模块 提供世界内配方系统,允许定义在世界中(而非工作台)执行的配方。 @@ -137,6 +162,17 @@ public static final RegistryEntry MY_ITEM = REGISTRUM .register(); ``` +### Main 模块 + +`anvillib-neoforge-1.21.1` 为聚合发行模块,默认打包并重导出以下子模块: + +- `config` +- `integration` +- `network` +- `recipe` +- `moveable-entity-block` +- `registrum` + ## 依赖引入 ### Gradle (Groovy DSL) @@ -153,6 +189,7 @@ dependencies { // 或按需引入单独模块 implementation "dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-integration-neoforge-1.21.1:2.0.0" + implementation "dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-moveable-entity-block-neoforge-1.21.1:2.0.0" implementation "dev.anvilcraft.lib:anvillib-registrum-neoforge-1.21.1:2.0.0" @@ -168,9 +205,14 @@ repositories { dependencies { implementation("dev.anvilcraft.lib:anvillib-neoforge-1.21.1:2.0.0") + + // 按需引入示例 + implementation("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:2.0.0") } ``` +> 版本号建议与项目发布版本保持一致(当前工程配置为 `mod_version=2.0.0`)。 + ## 构建项目 ```bash @@ -178,8 +220,11 @@ dependencies { git clone https://github.com/Anvil-Dev/AnvilLib.git cd AnvilLib -# 构建 +# macOS / Linux 构建 ./gradlew build + +# Windows PowerShell / CMD 构建 +gradlew.bat build ``` ## 环境要求 diff --git a/build.gradle b/build.gradle index 9995314..9b94d9b 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'dev.anvilcraft.lib' -version = '1.4.0' +version = '2.0.0' repositories { mavenCentral() diff --git a/module.dynamic-multiblock/build.gradle b/module.dynamic-multiblock/build.gradle new file mode 100644 index 0000000..c2baa3a --- /dev/null +++ b/module.dynamic-multiblock/build.gradle @@ -0,0 +1,275 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { + if (System.getenv("NOT_DEV") == 'true') { + jarJar(implementation("dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:latest.release")) + } else { + jarJar(implementation project(":anvillib-recipe-neoforge-1.21.1")) + } +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README_en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.dynamic-multiblock/gradle.properties b/module.dynamic-multiblock/gradle.properties new file mode 100644 index 0000000..2cced56 --- /dev/null +++ b/module.dynamic-multiblock/gradle.properties @@ -0,0 +1,31 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +java_version=21 +org.gradle.jvmargs=-Xmx6G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true +## Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=1.21.1 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[1.21.1,1.22) +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.1.79 +# The Neo version range can use any version of Neo as bounds +neo_version_range=[21,) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[4,) +parchment_minecraft_version=1.21.1 +parchment_mappings_version=2024.11.17 +## Mod Properties +# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} +# Must match the String constant located in the main mod class annotated with @Mod. +mod_id=anvillib_dynamic_multiblock +# The human-readable display name for the mod. +mod_name=AnvilLib-Dynamic-Multiblock +# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. +mod_description=In-world dynamic multiblock system diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/AnvilLibDynamicMultiblock.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/AnvilLibDynamicMultiblock.java new file mode 100644 index 0000000..892b788 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/AnvilLibDynamicMultiblock.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.multiblock; + +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; + +@Mod(AnvilLibDynamicMultiblock.MOD_ID) +public class AnvilLibDynamicMultiblock { + public static final String MAIN_ID = "anvillib"; + public static final String MOD_ID = "anvillib_dynamic_multiblock"; + + public AnvilLibDynamicMultiblock(IEventBus modEventBus, ModContainer modContainer) { + } + + public static ResourceLocation of(String path) { + return ResourceLocation.fromNamespaceAndPath(MAIN_ID, path); + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/DynamicMultiblockManager.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/DynamicMultiblockManager.java new file mode 100644 index 0000000..61a81c1 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/DynamicMultiblockManager.java @@ -0,0 +1,25 @@ +package dev.anvilcraft.lib.v2.multiblock; + +import net.minecraft.world.level.Level; + +import java.util.HashMap; +import java.util.Map; + +public class DynamicMultiblockManager { + + // ========================= ↓ 静态方法 ↓ ========================= // + + private static final Map MANAGERS = new HashMap<>(); + + public static DynamicMultiblockManager get(Level level) { + return DynamicMultiblockManager.MANAGERS.computeIfAbsent(level, DynamicMultiblockManager::new); + } + + // ========================= ↓ 成员方法 ↓ ========================= // + + private transient final Level level; + + public DynamicMultiblockManager(Level level) { + this.level = level; + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/Multiblock.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/Multiblock.java new file mode 100644 index 0000000..6a4142e --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/Multiblock.java @@ -0,0 +1,26 @@ +package dev.anvilcraft.lib.v2.multiblock; + +import dev.anvilcraft.lib.v2.multiblock.controller.IMultiblockController; +import dev.anvilcraft.lib.v2.multiblock.definition.MultiblockDefinition; +import lombok.Getter; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Block; + +@Getter +public class Multiblock { + private final IMultiblockController controller; + private final BlockPos controllerPos; + private final MultiblockDefinition definition; + + public Multiblock(IMultiblockController controller, BlockPos controllerPos, MultiblockDefinition definition) { + this.controller = controller; + this.controllerPos = controllerPos; + this.definition = definition; + } + + public Multiblock(Block controller, BlockPos controllerPos, MultiblockDefinition definition) { + this.controller = IMultiblockController.of(controller); + this.controllerPos = controllerPos; + this.definition = definition; + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/MultiblockValidator.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/MultiblockValidator.java new file mode 100644 index 0000000..5ef2dbf --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/MultiblockValidator.java @@ -0,0 +1,25 @@ +package dev.anvilcraft.lib.v2.multiblock; + +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +public class MultiblockValidator { + private static final Direction[] DIRECTIONS = new Direction[] { + Direction.NORTH, + Direction.SOUTH, + Direction.EAST, + Direction.WEST, + }; + + public enum State { + /** + * (非控制器)已与控制器连接 + */ + BOUND + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/capability/DynamicMultiblockCapability.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/capability/DynamicMultiblockCapability.java new file mode 100644 index 0000000..551d482 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/capability/DynamicMultiblockCapability.java @@ -0,0 +1,4 @@ +package dev.anvilcraft.lib.v2.multiblock.capability; + +public class DynamicMultiblockCapability { +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/IMultiblockController.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/IMultiblockController.java new file mode 100644 index 0000000..83e2292 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/IMultiblockController.java @@ -0,0 +1,19 @@ +package dev.anvilcraft.lib.v2.multiblock.controller; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +public interface IMultiblockController { + Block block(); + + static IMultiblockController of(Block block) { + if (block instanceof IMultiblockController controller) return controller; + return new SimpleMultiblockController(block); + } + + void onStructureValid(Level level, BlockPos pos, BlockState state); + + void onStructureInvalid(Level level, BlockPos pos, BlockState state); +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/MultiblockControllerInstance.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/MultiblockControllerInstance.java new file mode 100644 index 0000000..7276c09 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/MultiblockControllerInstance.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.multiblock.controller; + +import net.minecraft.core.BlockPos; + +public record MultiblockControllerInstance(IMultiblockController controller, BlockPos pos) { +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/SimpleMultiblockController.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/SimpleMultiblockController.java new file mode 100644 index 0000000..972bce4 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/controller/SimpleMultiblockController.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.multiblock.controller; + +import net.minecraft.world.level.block.Block; + +public record SimpleMultiblockController(Block block) implements IMultiblockController { +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/DefinitionSerializer.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/DefinitionSerializer.java new file mode 100644 index 0000000..87165a5 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/DefinitionSerializer.java @@ -0,0 +1,146 @@ +package dev.anvilcraft.lib.v2.multiblock.definition; + +import com.google.common.collect.ImmutableList; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.anvilcraft.lib.v2.recipe.component.BlockStatePredicate; +import dev.anvilcraft.lib.v2.recipe.util.CodecUtil; +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectMaps; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public record DefinitionSerializer(@Unmodifiable String[][] structure, @Unmodifiable Char2ObjectMap definitions) { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(ins -> ins.group( + Codec.STRING + .listOf() + .listOf() + .xmap(DefinitionSerializer::toArrays, DefinitionSerializer::toLists) + .fieldOf("structure") + .forGetter(DefinitionSerializer::structure), + Codec.unboundedMap(CodecUtil.CHAR_CODEC, BlockStatePredicate.CODEC) + .xmap(DefinitionSerializer::toC2OMap, Function.identity()) + .fieldOf("definitions") + .forGetter(DefinitionSerializer::definitions) + ).apply(ins, DefinitionSerializer::new)); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8 + .apply(ByteBufCodecs.list()) + .apply(ByteBufCodecs.list()) + .map(DefinitionSerializer::toArrays, DefinitionSerializer::toLists), + DefinitionSerializer::structure, + ByteBufCodecs.>map( + HashMap::new, + CodecUtil.CHAR_STREAM_CODEC, + BlockStatePredicate.STREAM_CODEC + ).map(DefinitionSerializer::toC2OMap, Function.identity()), + DefinitionSerializer::definitions, + DefinitionSerializer::new + ); + + MultiblockDefinition toDefinition() { + Vec3i controllerOffset = this.getControllerOffset(); + int sizeX = this.structure[0][0].length(); + int sizeZ = this.structure[0].length; + int sizeXZ = sizeX * sizeZ; + int sizeY = this.structure.length; + int sizeXYZ = sizeXZ * sizeY; + ImmutableList.Builder structure = ImmutableList.builder(); + int cursor = 0; + while (cursor < sizeXYZ) { + int indexX = cursor % sizeX; + int indexY = cursor / sizeXZ; + int indexZ = (cursor % sizeXZ) / sizeX; + + char key = this.getChar(indexX, indexY, indexZ); + if (this.isEmpty(key)) { + cursor++; + continue; + } + + // 找到非空位置,计算世界坐标 + int worldX = indexX - controllerOffset.getX(); + int worldY = sizeY - 1 - indexY - controllerOffset.getY(); + int worldZ = indexZ - controllerOffset.getZ(); + cursor++; + structure.add(new MultiblockPosInfo( + key, + new BlockPos(worldX, worldY, worldZ), + new BlockPos(indexX, indexY, indexZ) + )); + } + return new MultiblockDefinition(structure.build(), this.definitions, sizeX, sizeY, sizeZ); + } + + static DefinitionSerializer fromDefinition(MultiblockDefinition definition) { + String[][] structure = new String[definition.sizeY()][definition.sizeZ()]; + for (int y = 0; y < definition.sizeY(); y++) { + for (int z = 0; z < definition.sizeZ(); z++) { + char[] xs = new char[definition.sizeX()]; + Arrays.fill(xs, ' '); + for (MultiblockPosInfo pos : definition.structure()) { + if (pos.pos().getY() != y || pos.pos().getZ() != z) continue; + xs[pos.pos().getX()] = pos.key(); + } + structure[y][z] = new String(xs); + } + } + return new DefinitionSerializer(structure, definition.definitions()); + } + + public char getChar(int x, int y, int z) { + return this.structure[y][z].charAt(x); + } + + public boolean isEmpty(char key) { + return key == Character.MIN_VALUE; + } + + private Vec3i getControllerOffset() { + for (int y = 0; y < this.structure.length; y++) { + String[] xz = this.structure[y]; + for (int z = 0; z < xz.length; z++) { + String xs = xz[z]; + for (int x = 0; x < xs.length(); x++) { + if (xs.charAt(x) != MultiblockDefinition.CONTROLLER) continue; + return new Vec3i(x, y, z); + } + } + } + throw new IllegalStateException("Unexpected no controller in structure"); + } + + static @Unmodifiable String[][] toArrays(List> lists) { + int size = lists.size(); + String[][] result = new String[size][]; + for (int i = 0; i < size; i++) { + result[i] = lists.get(i).toArray(String[]::new); + } + return result; + } + + private static @Unmodifiable List> toLists(String[][] arrays) { + List> result = new ArrayList<>(); + for (String[] array : arrays) { + result.add(List.of(array)); + } + return ImmutableList.copyOf(result); + } + + private static @Unmodifiable Char2ObjectMap toC2OMap(Map map) { + return Char2ObjectMaps.unmodifiable(new Char2ObjectOpenHashMap<>(map)); + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/MultiblockDefinition.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/MultiblockDefinition.java new file mode 100644 index 0000000..418d102 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/MultiblockDefinition.java @@ -0,0 +1,211 @@ +package dev.anvilcraft.lib.v2.multiblock.definition; + +import com.mojang.serialization.Lifecycle; +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.recipe.component.BlockStatePredicate; +import it.unimi.dsi.fastutil.chars.Char2ObjectMap; +import it.unimi.dsi.fastutil.chars.Char2ObjectMaps; +import it.unimi.dsi.fastutil.chars.Char2ObjectOpenHashMap; +import lombok.Getter; +import lombok.experimental.Accessors; +import net.minecraft.core.Holder; +import net.minecraft.core.Vec3i; +import net.minecraft.data.worldgen.BootstrapContext; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.List; + +/** + * 多方块定义类 + */ +@Getter +@Accessors(fluent = true, chain = false) +public class MultiblockDefinition { + public static final char CONTROLLER = '0'; + public static final MapCodec CODEC = DefinitionSerializer.CODEC.xmap( + DefinitionSerializer::toDefinition, + DefinitionSerializer::fromDefinition + ); + public static final StreamCodec STREAM_CODEC = DefinitionSerializer.STREAM_CODEC.map( + DefinitionSerializer::toDefinition, + DefinitionSerializer::fromDefinition + ); + private final @Unmodifiable List structure; + private final @Unmodifiable Char2ObjectMap definitions; + private final int sizeX, sizeY, sizeZ; + + /** + * 构建多方块定义类 + * + * @param structure 结构 + * @param definitions 定义,记录了字符与方块状态的对应情况。 + */ + MultiblockDefinition( + @Unmodifiable List structure, + @Unmodifiable Char2ObjectMap definitions, + int sizeX, + int sizeY, + int sizeZ + ) { + this.structure = structure; + this.definitions = definitions; + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + } + + public static Builder builder() { + return new Builder(); + } + + public boolean test(LevelAccessor level, Vec3i localPos, BlockState state, @Nullable BlockEntity entity) { + return this.getState(localPos).test(level, state, entity); + } + + public BlockStatePredicate getState(Vec3i localPos) { + return this.definitions.get(this.getChar(localPos)); + } + + public char getChar(Vec3i localPos) { + return this.getChar(localPos.getX(), localPos.getY(), localPos.getZ()); + } + + public char getChar(int x, int y, int z) { + for (MultiblockPosInfo pos : this.structure) { + if (pos.pos().getX() == x && pos.pos().getY() == y && pos.pos().getZ() == z) return pos.key(); + } + return Character.MIN_VALUE; + } + + public boolean isEmpty(char key) { + return key == Character.MIN_VALUE; + } + + public boolean isEmpty(Vec3i localPos) { + return this.isEmpty(this.getChar(localPos)); + } + + public boolean isEmpty(int x, int y, int z) { + return this.isEmpty(this.getChar(x, y, z)); + } + + public static class Builder { + private final List> structure = new ArrayList<>(); + private final Char2ObjectMap definitions = new Char2ObjectOpenHashMap<>(); + + public Builder() { + } + + public Builder layer(String... layer) { + this.structure.add(List.of(layer)); + return this; + } + + public Builder defineController(BlockStatePredicate.Builder state) { + this.definitions.put(MultiblockDefinition.CONTROLLER, state.build()); + return this; + } + + public Builder define(char key, BlockStatePredicate.Builder state) { + this.definitions.put(key, state.build()); + return this; + } + + public void validate(String dmbId) { + if (!this.definitions.containsKey(MultiblockDefinition.CONTROLLER)) { + throw new IllegalArgumentException( + "Unlinked controller key %s in DMB id %s" + .formatted(MultiblockDefinition.CONTROLLER, dmbId) + ); + } + + int sizeX = -1; + int sizeZ = -1; + boolean checkedController = false; + for (int indexY = 0; indexY < this.structure.size(); indexY++) { + List layer = this.structure.get(indexY); + if (sizeZ == -1) sizeZ = layer.size(); + if (sizeZ != layer.size()) { + throw new IllegalArgumentException( + "Inconsistent z-width was found in layer %d, DMB id %s. Need %d, found %d" + .formatted(indexY, dmbId, sizeZ, layer.size()) + ); + } + + for (String xs : layer) { + for (int indexX = 0; indexX < xs.length(); indexX++) { + char key = xs.charAt(indexX); + if (sizeX == -1) sizeX = xs.length(); + if (sizeX != xs.length()) { + throw new IllegalArgumentException( + "Inconsistent x-width was found in layer %d, DMB id %s. Need %d, found %d" + .formatted(indexY, dmbId, sizeX, xs.length()) + ); + } + if (key == MultiblockDefinition.CONTROLLER) { + if (!checkedController) { + checkedController = true; + continue; + } else { + throw new IllegalArgumentException( + "Multiple center key (%s) found in DMB id %s" + .formatted(MultiblockDefinition.CONTROLLER, dmbId) + ); + } + } + if (!this.definitions.containsKey(key)) { + throw new IllegalArgumentException( + "Unlinked key %s in DMB id %s" + .formatted(key, dmbId) + ); + } + } + } + } + + if (!checkedController) { + throw new IllegalArgumentException( + "Undefined controller key (%s) in DMB id %s" + .formatted(MultiblockDefinition.CONTROLLER, dmbId) + ); + } + } + + public Holder.Reference register( + BootstrapContext context, + ResourceKey key + ) { + this.validate(key.location().toString()); + return context.register(key, this.build()); + } + + public Holder.Reference register( + BootstrapContext context, + ResourceKey key, + Lifecycle registryLifecycle + ) { + this.validate(key.location().toString()); + return context.register(key, this.build(), registryLifecycle); + } + + public Holder create() { + this.validate(""); + return Holder.direct(this.build()); + } + + public MultiblockDefinition build() { + return new DefinitionSerializer( + DefinitionSerializer.toArrays(this.structure), + Char2ObjectMaps.unmodifiable(this.definitions) + ).toDefinition(); + } + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/MultiblockPosInfo.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/MultiblockPosInfo.java new file mode 100644 index 0000000..d7670f7 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/MultiblockPosInfo.java @@ -0,0 +1,6 @@ +package dev.anvilcraft.lib.v2.multiblock.definition; + +import net.minecraft.core.BlockPos; + +public record MultiblockPosInfo(char key, BlockPos offset, BlockPos pos) { +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/package-info.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/package-info.java new file mode 100644 index 0000000..02fdb14 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/definition/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package dev.anvilcraft.lib.v2.multiblock.definition; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/BlockEventListener.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/BlockEventListener.java new file mode 100644 index 0000000..5c799c0 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/event/BlockEventListener.java @@ -0,0 +1,14 @@ +package dev.anvilcraft.lib.v2.multiblock.event; + +import dev.anvilcraft.lib.v2.multiblock.AnvilLibDynamicMultiblock; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.level.BlockEvent; + +@EventBusSubscriber(modid = AnvilLibDynamicMultiblock.MOD_ID) +public class BlockEventListener { + @SubscribeEvent + public static void onBlockBreak(BlockEvent.BreakEvent event) { + event.getState().anvillib$unbind(event.getLevel()); + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/LibCapabilities.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/LibCapabilities.java new file mode 100644 index 0000000..94eb0fd --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/LibCapabilities.java @@ -0,0 +1,12 @@ +package dev.anvilcraft.lib.v2.multiblock.init; + +import dev.anvilcraft.lib.v2.multiblock.AnvilLibDynamicMultiblock; +import dev.anvilcraft.lib.v2.multiblock.part.IMultiPart; +import net.neoforged.neoforge.capabilities.BlockCapability; + +public class LibCapabilities { + public static final BlockCapability MULTI_PART_CAPABILITY = BlockCapability.createVoid( + AnvilLibDynamicMultiblock.of("multipart"), + IMultiPart.class + ); +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/LibRegistries.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/LibRegistries.java new file mode 100644 index 0000000..5cfc5e1 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/init/LibRegistries.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.multiblock.init; + +import com.mojang.serialization.MapCodec; +import dev.anvilcraft.lib.v2.multiblock.AnvilLibDynamicMultiblock; +import dev.anvilcraft.lib.v2.multiblock.definition.MultiblockDefinition; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.registries.DataPackRegistryEvent; + +@EventBusSubscriber(modid = AnvilLibDynamicMultiblock.MOD_ID, bus = EventBusSubscriber.Bus.MOD) +public class LibRegistries { + public static final ResourceKey> DEFINITION_KEY = ResourceKey.createRegistryKey( + AnvilLibDynamicMultiblock.of("trigger") + ); + + @SubscribeEvent + public static void registerDataRegistries(DataPackRegistryEvent.NewRegistry event) { + MapCodec codec = MultiblockDefinition.CODEC; + event.dataPackRegistry(DEFINITION_KEY, codec.codec(), codec.codec()); + } +} diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java new file mode 100644 index 0000000..172b5c2 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package dev.anvilcraft.lib.v2.multiblock; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/part/IMultiPart.java b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/part/IMultiPart.java new file mode 100644 index 0000000..5cd5868 --- /dev/null +++ b/module.dynamic-multiblock/src/main/java/dev/anvilcraft/lib/v2/multiblock/part/IMultiPart.java @@ -0,0 +1,22 @@ +package dev.anvilcraft.lib.v2.multiblock.part; + +import dev.anvilcraft.lib.v2.multiblock.Multiblock; +import net.minecraft.world.level.LevelAccessor; + +public interface IMultiPart { + default boolean anvillib$isController() { + throw new AssertionError(); + } + + default void anvillib$bind(Multiblock multiblock) { + throw new AssertionError(); + } + + default boolean anvillib$isBound() { + throw new AssertionError(); + } + + default void anvillib$unbind(LevelAccessor level) { + throw new AssertionError(); + } +} diff --git a/module.dynamic-multiblock/src/main/resources/META-INF/neoforge.mods.toml b/module.dynamic-multiblock/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..27f7ba7 --- /dev/null +++ b/module.dynamic-multiblock/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.dynamic-multiblock/src/main/resources/anvillib_dynamic_multiblock.mixins.json b/module.dynamic-multiblock/src/main/resources/anvillib_dynamic_multiblock.mixins.json new file mode 100644 index 0000000..867e285 --- /dev/null +++ b/module.dynamic-multiblock/src/main/resources/anvillib_dynamic_multiblock.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.multiblock.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/module.main/build.gradle b/module.main/build.gradle index 463e7bb..3d683df 100644 --- a/module.main/build.gradle +++ b/module.main/build.gradle @@ -119,12 +119,14 @@ dependencies { jarJar(implementation("dev.anvilcraft.lib:anvillib-config-neoforge-1.21.1:latest.release")) jarJar(implementation("dev.anvilcraft.lib:anvillib-integration-neoforge-1.21.1:latest.release")) jarJar(implementation("dev.anvilcraft.lib:anvillib-moveable-entity-block-neoforge-1.21.1:latest.release")) + jarJar(implementation("dev.anvilcraft.lib:anvillib-network-neoforge-1.21.1:latest.release")) jarJar(implementation("dev.anvilcraft.lib:anvillib-recipe-neoforge-1.21.1:latest.release")) jarJar(implementation("dev.anvilcraft.lib:anvillib-registrum-neoforge-1.21.1:latest.release")) } else { jarJar(implementation project(":anvillib-config-neoforge-1.21.1")) jarJar(implementation project(":anvillib-integration-neoforge-1.21.1")) jarJar(implementation project(":anvillib-moveable-entity-block-neoforge-1.21.1")) + jarJar(implementation project(":anvillib-network-neoforge-1.21.1")) jarJar(implementation project(":anvillib-recipe-neoforge-1.21.1")) jarJar(implementation project(":anvillib-registrum-neoforge-1.21.1")) } diff --git a/module.network/build.gradle b/module.network/build.gradle new file mode 100644 index 0000000..eeb756d --- /dev/null +++ b/module.network/build.gradle @@ -0,0 +1,270 @@ +plugins { + id 'java-library' + id 'eclipse' + id 'idea' + id 'maven-publish' + alias libs.plugins.modDevGradle + alias libs.plugins.lombok + alias libs.plugins.machete + id 'org.jreleaser' version '1.23.0' + id "me.modmuss50.mod-publish-plugin" version "1.1.0" +} + +String buildType = 'snapshot' +String buildNumber +if (System.getenv("CI_BUILD") == 'false') buildNumber = null +else { + if (System.getenv("PR_BUILD") != 'false') buildType = 'pr' + buildNumber = System.getenv("GITHUB_RUN_NUMBER") +} +version = "${mod_version}" + (buildNumber != null ? "+${buildType}.${buildNumber}" : "") +group = mod_group_id + +base { + archivesName = "${project.mod_id}-neoforge-${libs.versions.minecraft.get()}" +} + +repositories { + maven { url = "https://server.cjsah.net:1002/maven/" } + mavenLocal() + mavenCentral() +} + +java.toolchain.languageVersion = JavaLanguageVersion.of(21) + +neoForge { + // Specify the version of NeoForge to use. + version = libs.versions.neoForge.get() + + parchment { + mappingsVersion = project.parchment_mappings_version + minecraftVersion = project.parchment_minecraft_version + } + + // This line is optional. Access Transformers are automatically detected + // accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg') + + interfaceInjectionData { + // from 'src/main/resources/interface_injections.json' + // publish file('src/main/resources/interface_injections.json') + } + + // Default run configurations. + // These can be tweaked, removed, or duplicated as needed. + runs { + client { + client() + + // Comma-separated list of namespaces to load gametests from. Empty = all namespaces. + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + server { + server() + programArgument '--nogui' + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + // This run config launches GameTestServer and runs all registered gametests, then exits. + // By default, the server will crash when no gametests are provided. + // The gametest system is also enabled by default for other run configs under the /test command. + gameTestServer { + type = "gameTestServer" + systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id + } + + data { + data() + + // example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it + // gameDirectory = project.file('run-data') + + // Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources. + programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath() + } + + // applies to all the run configs above + configureEach { + // Recommended logging data for a userdev environment + // The markers can be added/remove as needed separated by commas. + // "SCAN": For mods scan. + // "REGISTRIES": For firing of registry events. + // "REGISTRYDUMP": For getting the contents of all registries. + systemProperty 'forge.logging.markers', 'REGISTRIES' + + // Recommended logging level for the console + // You can set various levels here. + // Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels + logLevel = org.slf4j.event.Level.DEBUG + } + } + + mods { + // define mod <-> source bindings + // these are used to tell the game which sources are for which mod + // mostly optional in a single mod project + // but multi mod projects should define one per mod + "${mod_id}" { + sourceSet(sourceSets.main) + } + } +} + +// Include resources generated by data generators. +sourceSets.main.resources { srcDir 'src/generated/resources' } + + +dependencies { +} + +// This block of code expands all declared replace properties in the specified resource targets. +// A missing property will result in an error. Properties are expanded using ${} Groovy notation. +// When "copyIdeResources" is enabled, this will also run before the game launches in IDE environments. +// See https://docs.gradle.org/current/dsl/org.gradle.language.jvm.tasks.ProcessResources.html +tasks.withType(ProcessResources).configureEach { + var replaceProperties = [minecraft_version : minecraft_version, + minecraft_version_range: minecraft_version_range, + neo_version : neo_version, + neo_version_range : neo_version_range, + loader_version_range : loader_version_range, + mod_id : mod_id, + mod_name : mod_name, + mod_license : mod_license, + mod_version : version, + mod_authors : mod_authors, + mod_description : mod_description] + inputs.properties replaceProperties + + filesMatching(['META-INF/neoforge.mods.toml']) { + expand replaceProperties + } +} + +tasks.register('sourcesJar', Jar) { + from delombok.outputs.files + dependsOn delombok + archiveClassifier = "sources" +} + +tasks.register('javadocJar', Jar) { + from javadoc.destinationDir + dependsOn javadoc + archiveClassifier = "javadoc" +} + +javadoc { + options.encoding = 'UTF-8' + options.addStringOption('Xdoclint:none', '-quiet') +} + +jar { + from(rootProject.file("LICENSE")) + from(rootProject.file("ASSETS_LICENSE")) + from(rootProject.file("README.md")) + from(rootProject.file("README_en.md")) +} + +artifacts { + archives tasks.jar + archives tasks.sourcesJar + archives tasks.javadocJar +} + +def this_name = base.archivesName.get() +def this_description = mod_description + +publishing { + publications { + register('mavenJava', MavenPublication) { + from components.java + + artifact sourcesJar + artifact javadocJar + + versionMapping { + allVariants { + fromResolutionResult() + } + } + + pom { + name = this_name + description = this_description + url = 'https://github.com/Anvil-Dev/AnvilLib' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/license/mit' + } + } + developers { + developer { + id = 'Anvil-Dev' + name = 'AnvilCraft Dev' + email = 'admin@anvilcraft.dev' + } + } + scm { + url = 'https://github.com/Anvil-Dev/AnvilLib' + connection = 'https://github.com/Anvil-Dev/AnvilLib.git' + developerConnection = 'https://github.com/Anvil-Dev/AnvilLib.git' + } + } + } + } + repositories { + // Add repositories to publish to here. + def MAVEN_URL = System.getenv("MAVEN_URL") + if (MAVEN_URL != null) { + maven { + url MAVEN_URL + credentials { + username System.getenv("MAVEN_USERNAME") + password System.getenv("MAVEN_PASSWORD") + } + } + } + mavenLocal() + maven { + name = "staging" + url = layout.buildDirectory.dir("staging-deploy") + } + } +} + +jreleaser { + signing { + active = 'ALWAYS' + armored = true + } + + deploy { + maven { + mavenCentral { + sonatype { + active = 'ALWAYS' + url = 'https://central.sonatype.com/api/v1/publisher' + stagingRepository('build/staging-deploy') + } + } + } + } +} + +// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior. +idea { + module { + downloadSources = true + downloadJavadoc = true + } +} + +machete { + // disable machete locally for faster builds + enabled = false +} + +lombok { + version = "1.18.34" +} diff --git a/module.network/gradle.properties b/module.network/gradle.properties new file mode 100644 index 0000000..710c5cc --- /dev/null +++ b/module.network/gradle.properties @@ -0,0 +1,31 @@ +# Sets default memory used for gradle commands. Can be overridden by user or command line properties. +java_version=21 +org.gradle.jvmargs=-Xmx6G +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configuration-cache=true +## Environment Properties +# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge +# The Minecraft version must agree with the Neo version to get a valid artifact +minecraft_version=1.21.1 +# The Minecraft version range can use any release version of Minecraft as bounds. +# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly +# as they do not follow standard versioning conventions. +minecraft_version_range=[1.21.1,1.22) +# The Neo version must agree with the Minecraft version to get a valid artifact +neo_version=21.1.79 +# The Neo version range can use any version of Neo as bounds +neo_version_range=[21,) +# The loader version range can only use the major version of FML as bounds +loader_version_range=[4,) +parchment_minecraft_version=1.21.1 +parchment_mappings_version=2024.11.17 +## Mod Properties +# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} +# Must match the String constant located in the main mod class annotated with @Mod. +mod_id=anvillib_network +# The human-readable display name for the mod. +mod_name=AnvilLib-Network +# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list. +mod_description=Network API with direction-specific packets and automatic registry diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IClientboundPacket.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IClientboundPacket.java new file mode 100644 index 0000000..b4d1c59 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IClientboundPacket.java @@ -0,0 +1,27 @@ +package dev.anvilcraft.lib.v2.network.packet; + +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +/** + * 客户端侧网络包,允许服务端向客户端发送 + */ +public non-sealed interface IClientboundPacket extends IPacket { + /** + * 客户端处理器 + * + * @param ctx 网络包上下文 + */ + default void clientHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> this.handleOnClient(ctx.player())); + } + + /** + * 客户端侧处理逻辑 + * + * @param player 客户端玩家。其类型恒为 {@link net.minecraft.client.player.LocalPlayer LocalPlayer} + */ + @ApiStatus.OverrideOnly + void handleOnClient(Player player); +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IInsensitiveBiPacket.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IInsensitiveBiPacket.java new file mode 100644 index 0000000..b03d5eb --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IInsensitiveBiPacket.java @@ -0,0 +1,49 @@ +package dev.anvilcraft.lib.v2.network.packet; + +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +/** + * 双端网络包,允许两端各自向对方发送 + * + *

处理逻辑时方向不敏感,两端共用一套逻辑

+ */ +public interface IInsensitiveBiPacket extends IClientboundPacket, IServerboundPacket { + /** + * 双端处理器 + * + * @param ctx 网络包上下文 + */ + default void bidirectionalHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> this.handleOnBothSide(ctx.player())); + } + + /** + * 两端共用的处理逻辑 + * + * @param player 玩家。客户端为 + * {@link net.minecraft.client.player.LocalPlayer LocalPlayer},服务端为 + * {@link net.minecraft.server.level.ServerPlayer ServerPlayer} + */ + void handleOnBothSide(Player player); + + @Override + default void clientHandler(IPayloadContext ctx) { + this.bidirectionalHandler(ctx); + } + + @Override + default void handleOnClient(Player player) { + this.handleOnBothSide(player); + } + + @Override + default void serverHandler(IPayloadContext ctx) { + this.bidirectionalHandler(ctx); + } + + @Override + default void handleOnServer(Player player) { + this.handleOnBothSide(player); + } +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java new file mode 100644 index 0000000..39b8da3 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IPacket.java @@ -0,0 +1,21 @@ +package dev.anvilcraft.lib.v2.network.packet; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.ApiStatus; + +/** + * 网络包根接口 + */ +public sealed interface IPacket extends CustomPacketPayload permits IClientboundPacket, IServerboundPacket { + /** + * 构建网络包类型 + * + * @param id 网络包 ID + * @return 网络包类型 + * @param 网络包 Java 类型 + */ + static Type type(ResourceLocation id) { + return new Type<>(id); + } +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/ISensitiveBiPacket.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/ISensitiveBiPacket.java new file mode 100644 index 0000000..fa3c994 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/ISensitiveBiPacket.java @@ -0,0 +1,23 @@ +package dev.anvilcraft.lib.v2.network.packet; + +import net.neoforged.neoforge.network.handling.IPayloadContext; + +/** + * 双端网络包,允许两端各自向对方发送 + * + *

处理逻辑时方向敏感,两端各自用一套逻辑

+ */ +public interface ISensitiveBiPacket extends IClientboundPacket, IServerboundPacket { + /** + * 双端处理器 + * + * @param ctx 网络包上下文 + */ + default void bidirectionalHandler(IPayloadContext ctx) { + if (ctx.flow().isClientbound()) { + this.clientHandler(ctx); + } else if (ctx.flow().isServerbound()) { + this.serverHandler(ctx); + } + } +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IServerboundPacket.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IServerboundPacket.java new file mode 100644 index 0000000..07f7aab --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/IServerboundPacket.java @@ -0,0 +1,27 @@ +package dev.anvilcraft.lib.v2.network.packet; + +import net.minecraft.world.entity.player.Player; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import org.jetbrains.annotations.ApiStatus; + +/** + * 服务端侧网络包,允许客户端向服务端发送 + */ +public non-sealed interface IServerboundPacket extends IPacket { + /** + * 服务端处理器 + * + * @param ctx 网络包上下文 + */ + default void serverHandler(IPayloadContext ctx) { + ctx.enqueueWork(() -> this.handleOnServer(ctx.player())); + } + + /** + * 服务端侧处理逻辑 + * + * @param player 服务端玩家。其类型恒为 {@link net.minecraft.server.level.ServerPlayer ServerPlayer} + */ + @ApiStatus.OverrideOnly + void handleOnServer(Player player); +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java new file mode 100644 index 0000000..15ec89c --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/packet/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package dev.anvilcraft.lib.v2.network.packet; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/Network.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/Network.java new file mode 100644 index 0000000..09b91c6 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/Network.java @@ -0,0 +1,17 @@ +package dev.anvilcraft.lib.v2.network.register; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; + +/** + * 表示该软件包为网络包软件包 + */ +@Target(ElementType.PACKAGE) +public @interface Network { + /** + * 返回该软件包下所有网络包的连接协议 + * + * @return 该软件包下所有网络包的连接协议 + */ + PacketProtocol protocol() default PacketProtocol.PLAY; +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java new file mode 100644 index 0000000..4a2b3bf --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/NetworkRegistrar.java @@ -0,0 +1,118 @@ +package dev.anvilcraft.lib.v2.network.register; + +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.neoforged.fml.loading.LoadingModList; +import net.neoforged.fml.loading.modscan.ModAnnotation; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; +import net.neoforged.neoforgespi.language.IModFileInfo; +import net.neoforged.neoforgespi.language.ModFileScanData; +import org.objectweb.asm.Type; + +import java.lang.annotation.ElementType; + +/** + * 网络包注册器 + * + *

应在 {@link net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent RegisterPayloadHandlersEvent} 的侦听器中使用

+ * + * @see NetworkRegistrar#register(PayloadRegistrar, String) + */ +@Slf4j +public class NetworkRegistrar { + private static final String ANNOTATION_NAME = "L" + Network.class.getName().replace(".", "/") + ";"; + private static final String PACKET_PACKAGE_PREFIX = "L" + IPacket.class.getPackageName().replace(".", "/"); + + /** + * 注册对应 {@code modId} 的模组中所有使用 {@link Network} 注解的软件包下的网络包 + * + * @param registrar 网络包注册器。应通过 + * {@link + * net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent#registrar(String) + * RegisterPayloadHandlersEvent.registrar() + * } 获取 + * @param modId 模组 ID + */ + @SuppressWarnings("unchecked") + public static void register(PayloadRegistrar registrar, String modId) { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + IModFileInfo fileInfo = LoadingModList.get().getModFileById(modId); + ModFileScanData scanData = fileInfo.getFile().getScanResult(); + for (ModFileScanData.AnnotationData annotation : scanData.getAnnotations()) { + if ( + !annotation.annotationType().getDescriptor().equals(ANNOTATION_NAME) + || annotation.targetType() != ElementType.TYPE + ) { + continue; + } + + ModAnnotation.EnumHolder protocolHolder = (ModAnnotation.EnumHolder) annotation.annotationData().get("protocol"); + PacketProtocol protocol = PacketProtocol.PLAY; + if (protocolHolder != null) { + protocol = switch (protocolHolder.value()) { + case "CONFIGURATION" -> PacketProtocol.CONFIGURATION; + case "PLAY" -> PacketProtocol.PLAY; + case "COMMON" -> PacketProtocol.COMMON; + default -> throw new IllegalArgumentException("Unknown packet protocol: " + protocolHolder.value()); + }; + } + String packageName = annotation.memberName().substring(0, annotation.memberName().lastIndexOf('.')); + log.info("Considering network package {}", packageName); + + for (ModFileScanData.ClassData classData : scanData.getClasses()) { + String className = classData.clazz().getClassName(); + if (!className.substring(0, className.lastIndexOf('.')).equals(packageName)) continue; + + boolean isPacket = false; + for (Type anInterface : classData.interfaces()) { + if (!anInterface.getDescriptor().startsWith(NetworkRegistrar.PACKET_PACKAGE_PREFIX)) continue; + isPacket = true; + break; + } + if (!isPacket) continue; + + try { + Class packetClass = (Class) loader.loadClass(className); + NetworkRegistrar.register(registrar, protocol, packetClass); + } catch (ClassNotFoundException e) { + log.error("Cannot find packet class {}", className, e); + throw new IllegalStateException(); + } + } + } + } + + private static void register(PayloadRegistrar registrar, PacketProtocol protocol, Class packetClass) { + switch (protocol) { + case CONFIGURATION -> { + PacketData data = PacketData.find(packetClass); + log.debug("Registered packet {} for 'CONFIGURATION', '{}'", data.type().id(), data.direction()); + switch (data.direction()) { + case CLIENTBOUND -> registrar.configurationToClient(data.type(), data.streamCodec(), data.handler()); + case SERVERBOUND -> registrar.configurationToServer(data.type(), data.streamCodec(), data.handler()); + case BIDIRECTIONAL -> registrar.configurationBidirectional(data.type(), data.streamCodec(), data.handler()); + } + } + case PLAY -> { + PacketData data = PacketData.find(packetClass); + log.debug("Registered packet {} for 'PLAY', '{}'", data.type().id(), data.direction()); + switch (data.direction()) { + case CLIENTBOUND -> registrar.playToClient(data.type(), data.streamCodec(), data.handler()); + case SERVERBOUND -> registrar.playToServer(data.type(), data.streamCodec(), data.handler()); + case BIDIRECTIONAL -> registrar.playBidirectional(data.type(), data.streamCodec(), data.handler()); + } + } + case COMMON -> { + PacketData data = PacketData.find(packetClass); + log.debug("Registered packet {} for 'COMMON', '{}'", data.type().id(), data.direction()); + switch (data.direction()) { + case CLIENTBOUND -> registrar.commonToClient(data.type(), data.streamCodec(), data.handler()); + case SERVERBOUND -> registrar.commonToServer(data.type(), data.streamCodec(), data.handler()); + case BIDIRECTIONAL -> registrar.commonBidirectional(data.type(), data.streamCodec(), data.handler()); + } + } + } + } +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java new file mode 100644 index 0000000..440dd80 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketData.java @@ -0,0 +1,80 @@ +package dev.anvilcraft.lib.v2.network.register; + +import dev.anvilcraft.lib.v2.network.packet.IClientboundPacket; +import dev.anvilcraft.lib.v2.network.packet.IInsensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IPacket; +import dev.anvilcraft.lib.v2.network.packet.ISensitiveBiPacket; +import dev.anvilcraft.lib.v2.network.packet.IServerboundPacket; +import io.netty.buffer.ByteBuf; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.handling.IPayloadHandler; + +import java.lang.reflect.AccessFlag; +import java.lang.reflect.Field; +import java.util.Set; + +@Slf4j +record PacketData( + CustomPacketPayload.Type type, + StreamCodec streamCodec, + PacketDirection direction, + IPayloadHandler handler +) { + @SuppressWarnings("unchecked") + static PacketData find(Class packetClass) { + CustomPacketPayload.Type type = null; + StreamCodec codec = null; + try { + for (Field field : packetClass.getDeclaredFields()) { + Set accessFlags = field.accessFlags(); + if ( + !accessFlags.contains(AccessFlag.STATIC) + || !accessFlags.contains(AccessFlag.FINAL) + ) { + continue; + } + Class declaringClass = field.getType(); + if (declaringClass.isAssignableFrom(CustomPacketPayload.Type.class)) { + field.setAccessible(true); + type = (CustomPacketPayload.Type) field.get(null); + } else if (declaringClass.isAssignableFrom(StreamCodec.class)) { + field.setAccessible(true); + codec = (StreamCodec) field.get(null); + } + } + } catch (IllegalAccessException e) { + log.error("Cannot access the type/codec of packet {}", packetClass.getName(), e); + throw new IllegalStateException(); + } + if (type == null) { + log.error("Cannot find static final type of packet {}", packetClass.getName()); + throw new IllegalArgumentException(); + } + if (codec == null) { + log.error("Cannot find static final codec of packet {}", packetClass.getName()); + throw new IllegalArgumentException(); + } + + PacketDirection direction; + IPayloadHandler handler; + if (IInsensitiveBiPacket.class.isAssignableFrom(packetClass)) { + direction = PacketDirection.BIDIRECTIONAL; + handler = (packet, ctx) -> ((IInsensitiveBiPacket) packet).bidirectionalHandler(ctx); + } else if (ISensitiveBiPacket.class.isAssignableFrom(packetClass)) { + direction = PacketDirection.BIDIRECTIONAL; + handler = (packet, ctx) -> ((ISensitiveBiPacket) packet).bidirectionalHandler(ctx); + } else if (IClientboundPacket.class.isAssignableFrom(packetClass)) { + direction = PacketDirection.CLIENTBOUND; + handler = (packet, ctx) -> ((IClientboundPacket) packet).clientHandler(ctx); + } else if (IServerboundPacket.class.isAssignableFrom(packetClass)) { + direction = PacketDirection.SERVERBOUND; + handler = (packet, ctx) -> ((IServerboundPacket) packet).serverHandler(ctx); + } else { + log.error("Class {} extends IPacket, but not extends IClientboundPacket or IServerboundPacket", packetClass.getName()); + throw new IllegalStateException(); + } + return new PacketData<>(type, codec, direction, handler); + } +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketDirection.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketDirection.java new file mode 100644 index 0000000..f171164 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketDirection.java @@ -0,0 +1,5 @@ +package dev.anvilcraft.lib.v2.network.register; + +enum PacketDirection { + CLIENTBOUND, SERVERBOUND, BIDIRECTIONAL +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java new file mode 100644 index 0000000..930d756 --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/PacketProtocol.java @@ -0,0 +1,5 @@ +package dev.anvilcraft.lib.v2.network.register; + +enum PacketProtocol { + CONFIGURATION, PLAY, COMMON +} diff --git a/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java new file mode 100644 index 0000000..692698a --- /dev/null +++ b/module.network/src/main/java/dev/anvilcraft/lib/v2/network/register/package-info.java @@ -0,0 +1,7 @@ +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package dev.anvilcraft.lib.v2.network.register; + +import net.minecraft.MethodsReturnNonnullByDefault; + +import javax.annotation.ParametersAreNonnullByDefault; \ No newline at end of file diff --git a/module.network/src/main/resources/META-INF/neoforge.mods.toml b/module.network/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..27f7ba7 --- /dev/null +++ b/module.network/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,86 @@ +# This is an example mods.toml file. It contains the data relating to the loading mods. +# There are several mandatory fields (#mandatory), and many more that are optional (#optional). +# The overall format is standard TOML format, v0.5.0. +# Note that there are a couple of TOML lists in this file. +# Find more information on toml format here: https://github.com/toml-lang/toml +# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml +modLoader = "javafml" #mandatory +# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47. +loaderVersion = "${loader_version_range}" #mandatory +# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties. +# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here. +license = "${mod_license}" +# A URL to refer people to when problems occur with this mod +#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional +# A list of mods - how many allowed here is determined by the individual mod loader +[[mods]] #mandatory +# The modid of the mod +modId = "${mod_id}" #mandatory +# The version number of the mod +version = "${mod_version}" #mandatory +# A display name for the mod +displayName = "${mod_name}" #mandatory +# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/ +#updateJSONURL="https://change.me.example.invalid/updates.json" #optional +# A URL for the "homepage" for this mod, displayed in the mod UI +displayURL="https://github.com/Anvil-Dev/AnvilLib" +# A file name (in the root of the mod JAR) containing a logo for display +logoFile="icon.png" #optional +# A text field displayed in the mod UI +#credits="" #optional +# A text field displayed in the mod UI +authors = "${mod_authors}" #optional +# Display Test controls the display for your mod in the server connection screen +# MATCH_VERSION means that your mod will cause a red X if the versions on client and server differ. This is the default behaviour and should be what you choose if you have server and client elements to your mod. +# IGNORE_SERVER_VERSION means that your mod will not cause a red X if it's present on the server but not on the client. This is what you should use if you're a server only mod. +# IGNORE_ALL_VERSION means that your mod will not cause a red X if it's present on the client or the server. This is a special case and should only be used if your mod has no server component. +# NONE means that no display test is set on your mod. You need to do this yourself, see IExtensionPoint.DisplayTest for more information. You can define any scheme you wish with this value. +# IMPORTANT NOTE: this is NOT an instruction as to which environments (CLIENT or DEDICATED SERVER) your mod loads on. Your mod should load (and maybe do nothing!) whereever it finds itself. +#displayTest="MATCH_VERSION" # MATCH_VERSION is the default if nothing is specified (#optional) + +# The description text for the mod (multi line!) (#mandatory) +description = '''${mod_description}''' + +# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded. +[[mixins]] +config = "${mod_id}.mixins.json" + +# The [[accessTransformers]] block allows you to declare where your AT file is. +# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg +#[[accessTransformers]] +#file="META-INF/accesstransformer.cfg" + +# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json + +# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional. +[[dependencies."${mod_id}"]] #optional +# the modid of the dependency +modId = "neoforge" #mandatory +# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive). +# 'required' requires the mod to exist, 'optional' does not +# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning +type = "required" #mandatory +# Optional field describing why the dependency is required or why it is incompatible +# reason="..." +# The version range of the dependency +versionRange = "${neo_version_range}" #mandatory +# An ordering relationship for the dependency. +# BEFORE - This mod is loaded BEFORE the dependency +# AFTER - This mod is loaded AFTER the dependency +ordering = "NONE" +# Side this dependency is applied on - BOTH, CLIENT, or SERVER +side = "BOTH" +# Here's another dependency +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +# This version range declares a minimum of the current minecraft version up to but not including the next major version +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" + +# Features are specific properties of the game environment, that you may want to declare you require. This example declares +# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't +# stop your mod loading on the server for example. +#[features."${mod_id}"] +#openGLVersion="[3.2,)" diff --git a/module.network/src/main/resources/anvillib_network.mixins.json b/module.network/src/main/resources/anvillib_network.mixins.json new file mode 100644 index 0000000..5135eb6 --- /dev/null +++ b/module.network/src/main/resources/anvillib_network.mixins.json @@ -0,0 +1,14 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.anvilcraft.lib.v2.network.mixin", + "compatibilityLevel": "JAVA_8", + "refmap": "anvillib.refmap.json", + "mixins": [ + ], + "client": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/settings.gradle b/settings.gradle index 68be2d5..76b04e2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,15 +11,19 @@ plugins { } include 'module.config' +include 'module.dynamic-multiblock' include 'module.integration' include 'module.moveable-entity-block' +include 'module.network' include 'module.recipe' include 'module.registrum' include 'module.main' project(':module.config').name = 'anvillib-config-neoforge-1.21.1' +project(':module.dynamic-multiblock').name = 'anvillib-dynamic-multiblock-neoforge-1.21.1' project(':module.integration').name = 'anvillib-integration-neoforge-1.21.1' project(':module.moveable-entity-block').name = 'anvillib-moveable-entity-block-neoforge-1.21.1' +project(':module.network').name = 'anvillib-network-neoforge-1.21.1' project(':module.recipe').name = 'anvillib-recipe-neoforge-1.21.1' project(':module.registrum').name = 'anvillib-registrum-neoforge-1.21.1' project(':module.main').name = 'anvillib-neoforge-1.21.1'