From 3e1deee2ca7052ac22e690ff2f2d58dc03700845 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:21:54 +0800 Subject: [PATCH 01/82] Update gradle.properties --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index db760d3..9f166b4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ minecraft_version=26.1.2 # as they do not follow standard versioning conventions. minecraft_version_range=[26.1.2] # The Neo version must agree with the Minecraft version to get a valid artifact -neo_version=26.1.2.75 +neo_version=26.1.2.76 # JEI (Just Enough Items) — optional compile-time API + dev-runtime dependency. # Versions: https://maven.blamejared.com/mezz/jei/jei-26.1.2-neoforge/ jei_version=29.6.2.31 From 838fef76d7a524d37cd24cc8a950d9c90937da7f Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:47:39 +0800 Subject: [PATCH 02/82] Add multiloader Architectury/Stonecutter scaffold Introduce a self-contained multiloader scaffold for Architectury (common/fabric/neoforge) with Stonecutter-ready version-axis wiring. Adds: module Gradle files, settings, multiloader gradle.properties, stonecutter stub, README, common loader-agnostic entry (NerospaceCommon), platform service seam (IPlatformHelper, Services), example registries (ModRegistries), Fabric/NeoForge entry points and platform implementations, mixin and mod metadata. Scaffold is inert and does not touch the existing root single-loader build; verify and pin real Fabric/Architectury/NeoForge versions in gradle.properties before building. --- multiloader/README.md | 141 ++++++++++++++++++ multiloader/build.gradle | 78 ++++++++++ multiloader/common/build.gradle | 23 +++ .../neroland/nerospace/NerospaceCommon.java | 39 +++++ .../nerospace/platform/IPlatformHelper.java | 28 ++++ .../neroland/nerospace/platform/Services.java | 31 ++++ .../nerospace/registry/ModRegistries.java | 49 ++++++ .../resources/nerospace-common.mixins.json | 11 ++ multiloader/fabric/build.gradle | 46 ++++++ .../nerospace/fabric/NerospaceFabric.java | 23 +++ .../fabric/NerospaceFabricClient.java | 21 +++ .../platform/FabricPlatformHelper.java | 31 ++++ ...eroland.nerospace.platform.IPlatformHelper | 1 + .../fabric/src/main/resources/fabric.mod.json | 27 ++++ .../src/main/resources/nerospace.mixins.json | 11 ++ multiloader/gradle.properties | 59 ++++++++ multiloader/neoforge/build.gradle | 47 ++++++ .../nerospace/neoforge/NerospaceNeoForge.java | 31 ++++ .../platform/NeoForgePlatformHelper.java | 32 ++++ .../resources/META-INF/neoforge.mods.toml | 29 ++++ ...eroland.nerospace.platform.IPlatformHelper | 1 + multiloader/settings.gradle | 63 ++++++++ multiloader/stonecutter.gradle | 33 ++++ 23 files changed, 855 insertions(+) create mode 100644 multiloader/README.md create mode 100644 multiloader/build.gradle create mode 100644 multiloader/common/build.gradle create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java create mode 100644 multiloader/common/src/main/resources/nerospace-common.mixins.json create mode 100644 multiloader/fabric/build.gradle create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper create mode 100644 multiloader/fabric/src/main/resources/fabric.mod.json create mode 100644 multiloader/fabric/src/main/resources/nerospace.mixins.json create mode 100644 multiloader/gradle.properties create mode 100644 multiloader/neoforge/build.gradle create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java create mode 100644 multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper create mode 100644 multiloader/settings.gradle create mode 100644 multiloader/stonecutter.gradle diff --git a/multiloader/README.md b/multiloader/README.md new file mode 100644 index 0000000..06e354f --- /dev/null +++ b/multiloader/README.md @@ -0,0 +1,141 @@ +# Nerospace multiloader scaffold + +A self-contained skeleton for building Nerospace on **both NeoForge and Fabric** +(loader axis, via **Architectury**) and across **multiple Minecraft versions** +(version axis, via **Stonecutter**). + +> **This does not touch the working build.** The single-loader NeoForge mod at +> the repo root is unchanged and still builds normally. This directory is a +> parallel, reviewable scaffold you promote to the root only when you're ready +> to commit to the migration. See [`docs/MULTILOADER.md`](../docs/MULTILOADER.md) +> for the full subsystem-by-subsystem migration plan. + +## What is and isn't here + +This is a **skeleton**, not a port. It contains: + +- the Gradle wiring for a 3-module Architectury project (`common` / `fabric` / `neoforge`); +- loader entry points that delegate to a shared `NerospaceCommon.init()`; +- a dependency-free platform abstraction (`platform/Services` + `IPlatformHelper`) + with a Fabric and a NeoForge implementation; +- one example cross-loader registration (`registry/ModRegistries`) proving the path; +- mod metadata for both loaders (`neoforge.mods.toml`, `fabric.mod.json`); +- Stonecutter declared and ready to activate for the version axis. + +It does **not** contain the migrated mod. None of the ~200 existing source files +have been moved — that is the actual migration work, sequenced in +[`docs/MULTILOADER.md`](../docs/MULTILOADER.md). + +## Layout + +``` +multiloader/ +├── settings.gradle loader split (active) + Stonecutter (ready) +├── build.gradle Architectury root + shared subproject config +├── gradle.properties all version pins (per-MC-version) +├── stonecutter.gradle version-axis controller (stub until activated) +├── common/ vanilla-only + cross-loader abstractions +│ ├── NerospaceCommon shared entry point +│ ├── platform/Services ServiceLoader resolver +│ ├── platform/IPlatformHelper the loader seam (grow this during migration) +│ └── registry/ModRegistries Architectury DeferredRegister example +├── fabric/ Fabric entry points + platform impl + fabric.mod.json +└── neoforge/ NeoForge @Mod entry + platform impl + neoforge.mods.toml +``` + +## ⚠️ Before the first build — pin real versions + +The Fabric and Architectury artifacts in `gradle.properties` are +**placeholders shaped like real coordinates**, not confirmed-resolvable values. +Bleeding-edge Minecraft versions often ship NeoForge first, with Fabric API and +Architectury following later. Confirm and update, for **each** version in +`mc_versions`: + +| Property | Where to confirm | +| --- | --- | +| `fabric_loader_version`, `fabric_api_version_` | | +| `architectury_loom_version`, `architectury_plugin_version`, `architectury_api_version_` | / [maven.architectury.dev](https://maven.architectury.dev) | +| `neo_version_` | | + +If Fabric/Architectury have **not** ported to your target Minecraft version yet, +the `fabric` and `neoforge` configuration will fail to resolve dependencies. +That is an ecosystem-availability limit, not a scaffold defect — the NeoForge +side of the matrix can proceed independently in the meantime. + +The plugin versions in `settings.gradle` (Stonecutter) and `build.gradle` +(`architectury-plugin`, `dev.architectury.loom`) are pinned literally because +the Gradle `plugins {}` block can't read `gradle.properties`. Keep them in sync +with the matching `*_version` values. + +## Building + +The scaffold is a nested Gradle build; drive it with the repo's wrapper: + +```bash +# from the repo root +./gradlew -p multiloader build # both loaders, active MC version +./gradlew -p multiloader :fabric:build +./gradlew -p multiloader :neoforge:build +``` + +Output jars land in `multiloader//build/libs/`. The active Minecraft +version is `minecraft_version` in `gradle.properties`. + +## Finishing the version axis (Stonecutter) + +The loader axis (Architectury) is active. The version axis is **declared but not +wired into the build**, so the skeleton configures cleanly on its own. To build +the full 2×2 matrix (versions × loaders), pick one: + +**A — Stonecraft (recommended, turnkey).** [Stonecraft](https://stonecraft.meza.gg/) +is a settings plugin that wires Stonecutter to Architectury automatically from +the `mc_versions` list. Swap the `dev.kikugie.stonecutter` plugin in +`settings.gradle` for `dev.meza.stonecraft` and follow its quick-start. Least +hand-wiring. + +**B — Hand-wired Stonecutter.** Uncomment the `stonecutter { create(rootProject) }` +block in `settings.gradle`, flesh out `stonecutter.gradle` (the stub shows the +shape), and use version comments in source to absorb signature drift between +Minecraft versions: + +```java +//? if >=26.2 { +/*newSignature(); +*///?} else { +oldSignature(); +//?} +``` + +Either way, keep the version list in `settings.gradle` / `stonecutter.gradle` in +sync with `mc_versions` in `gradle.properties`. + +## The platform seam + +Common code must not import `net.neoforged.*` or `net.fabricmc.*`. Where it needs +loader behaviour, it calls `Services.PLATFORM.()`. Each loader module +provides one `IPlatformHelper` implementation, registered through its +`META-INF/services/...IPlatformHelper` file and resolved at runtime by +`ServiceLoader`. Expand `IPlatformHelper` (and add sibling service interfaces) as +you migrate capabilities, networking, config and attachments — this interface is +where every "NeoForge does X, Fabric does Y" decision is funnelled. + +## Promoting to the repo root + +When the migration is far enough along to replace the single-loader build: + +1. Move the existing `src/main/java/...` business logic into `common` (strip + loader imports behind `Services`); keep NeoForge-only code in `neoforge`. +2. Move `multiloader/{settings,build}.gradle` + `gradle.properties` to the repo + root (merging the JEI/datagen/tooling config from the current root build). +3. Repoint the `tools/` generators and `tools/gradle-mcp` at the new module + paths (they're loader-agnostic and otherwise unchanged). +4. Delete this `multiloader/` directory. + +Until then, the root build remains the source of truth. + +## References + +- [`docs/MULTILOADER.md`](../docs/MULTILOADER.md) — full migration plan & subsystem map +- [Architectury docs](https://docs.architectury.dev) · [architectury-loom](https://github.com/architectury/architectury-loom) +- [Stonecutter](https://stonecutter.kikugie.dev/) · [Stonecraft](https://stonecraft.meza.gg/) +- [MultiLoader-Template (illusivesoulworks)](https://github.com/illusivesoulworks/multiloader-template) diff --git a/multiloader/build.gradle b/multiloader/build.gradle new file mode 100644 index 0000000..5d24abb --- /dev/null +++ b/multiloader/build.gradle @@ -0,0 +1,78 @@ +// ===================================================================== +// Nerospace multiloader scaffold — ROOT build +// Loader axis via Architectury. Shared config for all sub-modules. +// +// Plugin versions are pinned here (the plugins {} block cannot read +// gradle.properties). Keep them in sync with the *_version values in +// gradle.properties. +// ===================================================================== +plugins { + id 'architectury-plugin' version '3.4-SNAPSHOT' + id 'dev.architectury.loom' version '1.11-SNAPSHOT' apply false +} + +architectury { + minecraft = project.minecraft_version +} + +allprojects { + group = rootProject.mod_group_id + version = rootProject.mod_version +} + +subprojects { + apply plugin: 'dev.architectury.loom' + apply plugin: 'architectury-plugin' + + base { + // produces e.g. nerospace-fabric-1.0.0-alpha.1.jar + archivesName = "${rootProject.mod_id}-${project.name}" + } + + repositories { + mavenCentral() + maven { url 'https://maven.fabricmc.net/' } + maven { url 'https://maven.architectury.dev/' } + maven { url 'https://maven.neoforged.net/releases/' } + maven { + name 'JEI' + url 'https://maven.blamejared.com/' + } + } + + dependencies { + minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" + // 26.x is de-obfuscated; use official Mojang names (matches the + // root project's mappings convention — no Parchment). + mappings loom.officialMojangMappings() + } + + java { + withSourcesJar() + // Project targets Java 25 (see root CLAUDE.md). + toolchain.languageVersion = JavaLanguageVersion.of(25) + } + + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + options.release = 25 + } + + tasks.withType(ProcessResources).configureEach { + def expand = [ + mod_id : rootProject.mod_id, + mod_name : rootProject.mod_name, + mod_version : rootProject.mod_version, + mod_license : rootProject.mod_license, + mod_authors : rootProject.mod_authors, + mod_group_id : rootProject.mod_group_id, + minecraft_version : rootProject.minecraft_version, + minecraft_version_range: rootProject.minecraft_version_range, + fabric_loader_version: rootProject.fabric_loader_version, + ] + inputs.properties(expand) + filesMatching(['fabric.mod.json', 'META-INF/neoforge.mods.toml', 'pack.mcmeta']) { + expand(expand) + } + } +} diff --git a/multiloader/common/build.gradle b/multiloader/common/build.gradle new file mode 100644 index 0000000..28e9e57 --- /dev/null +++ b/multiloader/common/build.gradle @@ -0,0 +1,23 @@ +// common: vanilla-Minecraft-only code + cross-loader abstractions. +// No loader (NeoForge/Fabric) APIs may be referenced here directly — +// reach loader behaviour through the platform Services abstraction. + +def mc = rootProject.minecraft_version +def architecturyApi = project.findProperty("architectury_api_version_${mc}") + +architectury { + // Mark this as the common module shared by both platforms. + common('fabric', 'neoforge') +} + +dependencies { + // Fabric Loader is pulled in here only so the @Environment annotations + // and mixin tooling are available to shared code; it does NOT make this + // module Fabric-specific. + modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" + + // Architectury API (cross-loader helpers used by common code). + if (architecturyApi != null) { + modApi "dev.architectury:architectury:${architecturyApi}" + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java new file mode 100644 index 0000000..7baf8f4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java @@ -0,0 +1,39 @@ +package za.co.neroland.nerospace; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import za.co.neroland.nerospace.platform.Services; +import za.co.neroland.nerospace.registry.ModRegistries; + +/** + * Loader-agnostic entry point. + * + *

Both {@code NerospaceFabric} and {@code NerospaceNeoForge} call + * {@link #init()} from their own loader entry points. All shared setup + * (registration, config wiring, common event hooks) belongs here or in + * the packages it touches — never in a loader module. + * + *

Anything that must reach loader-specific behaviour goes through + * {@link Services} (a Java {@link java.util.ServiceLoader} abstraction), + * keeping this module free of {@code net.neoforged.*} and + * {@code net.fabricmc.*} imports. + */ +public final class NerospaceCommon { + + public static final String MOD_ID = "nerospace"; + public static final Logger LOGGER = LoggerFactory.getLogger("Nerospace"); + + private NerospaceCommon() { + } + + /** Called once per loader during mod construction. */ + public static void init() { + LOGGER.info("[Nerospace] common init on platform: {} (dev={})", + Services.PLATFORM.getPlatformName(), + Services.PLATFORM.isDevelopmentEnvironment()); + + // Cross-loader content registration lives in ModRegistries and is + // realised per-loader by each module's bootstrap (see register()). + ModRegistries.register(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java new file mode 100644 index 0000000..e6e5762 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java @@ -0,0 +1,28 @@ +package za.co.neroland.nerospace.platform; + +/** + * The loader-specific behaviour the common module is allowed to depend on. + * + *

Each loader module ships exactly one implementation, registered via a + * {@code META-INF/services} file so {@link Services} can load it with + * {@link java.util.ServiceLoader}. + * + *

Grow this interface as the migration proceeds — it is the seam where + * NeoForge capabilities, networking, config, attachments, etc. get their + * cross-loader abstractions. See {@code docs/MULTILOADER.md} for the full + * subsystem map. + */ +public interface IPlatformHelper { + + /** Human-readable platform name ("Fabric" / "NeoForge"). */ + String getPlatformName(); + + /** True when running in a development (dev/data/test) environment. */ + boolean isDevelopmentEnvironment(); + + /** True when the named mod is loaded. */ + boolean isModLoaded(String modId); + + /** True on the physical client (renderers, screens, HUD available). */ + boolean isClient(); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java new file mode 100644 index 0000000..26df523 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java @@ -0,0 +1,31 @@ +package za.co.neroland.nerospace.platform; + +import java.util.ServiceLoader; +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Loads loader-specific {@link IPlatformHelper} (and future service) + * implementations via {@link ServiceLoader}. + * + *

This is the lightweight, dependency-free alternative to Architectury's + * {@code @ExpectPlatform}. Common code calls {@code Services.PLATFORM.xxx()}; + * the correct Fabric or NeoForge implementation is resolved at runtime from + * the {@code META-INF/services} entry in each loader module. + */ +public final class Services { + + public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class); + + private Services() { + } + + public static T load(Class clazz) { + final T loaded = ServiceLoader.load(clazz) + .findFirst() + .orElseThrow(() -> new NullPointerException( + "No implementation found for service " + clazz.getName())); + NerospaceCommon.LOGGER.debug("Loaded service {} -> {}", + clazz.getSimpleName(), loaded.getClass().getName()); + return loaded; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java new file mode 100644 index 0000000..282cee5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -0,0 +1,49 @@ +package za.co.neroland.nerospace.registry; + +import dev.architectury.registry.registries.DeferredRegister; +import dev.architectury.registry.registries.RegistrySupplier; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Cross-loader content registration, the Architectury way. + * + *

{@link DeferredRegister} works identically on Fabric and NeoForge, so + * this single file replaces the root project's per-type NeoForge + * {@code DeferredRegister.Blocks/Items/...} classes. During migration, each + * existing registry class collapses into entries here. + * + *

The one example below proves the registration path end to end. Note + * that vanilla constructor signatures (e.g. {@code Block} / {@code Item} + * properties needing a registry key) drift between Minecraft versions — that + * is exactly what Stonecutter's version comments handle on the version axis. + */ +public final class ModRegistries { + + public static final DeferredRegister BLOCKS = + DeferredRegister.create(NerospaceCommon.MOD_ID, Registries.BLOCK); + public static final DeferredRegister ITEMS = + DeferredRegister.create(NerospaceCommon.MOD_ID, Registries.ITEM); + + // --- example content ------------------------------------------------- + public static final RegistrySupplier NEROSIUM_BLOCK = + BLOCKS.register("nerosium_block", + () -> new Block(BlockBehaviour.Properties.of().strength(3.0F))); + + public static final RegistrySupplier NEROSIUM_BLOCK_ITEM = + ITEMS.register("nerosium_block", + () -> new BlockItem(NEROSIUM_BLOCK.get(), new Item.Properties())); + + private ModRegistries() { + } + + /** Realises every DeferredRegister. Called from {@link NerospaceCommon#init()}. */ + public static void register() { + BLOCKS.register(); + ITEMS.register(); + } +} diff --git a/multiloader/common/src/main/resources/nerospace-common.mixins.json b/multiloader/common/src/main/resources/nerospace-common.mixins.json new file mode 100644 index 0000000..0692f04 --- /dev/null +++ b/multiloader/common/src/main/resources/nerospace-common.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "za.co.neroland.nerospace.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/multiloader/fabric/build.gradle b/multiloader/fabric/build.gradle new file mode 100644 index 0000000..b50d285 --- /dev/null +++ b/multiloader/fabric/build.gradle @@ -0,0 +1,46 @@ +// fabric: Fabric entry point + platform service implementations. +// Depends on :common and bundles its transformed classes into the jar. + +plugins { + id 'com.gradleup.shadow' version '8.3.5' +} + +def mc = rootProject.minecraft_version +def fabricApi = project.findProperty("fabric_api_version_${mc}") +def architecturyApi = project.findProperty("architectury_api_version_${mc}") + +architectury { + platformSetupLoomIde() + fabric() +} + +configurations { + common + shadowBundle + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentFabric.extendsFrom common +} + +dependencies { + modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" + + if (fabricApi != null) { + modApi "net.fabricmc.fabric-api:fabric-api:${fabricApi}" + } + if (architecturyApi != null) { + modApi "dev.architectury:architectury-fabric:${architecturyApi}" + } + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowBundle(project(path: ':common', configuration: 'transformProductionFabric')) +} + +shadowJar { + configurations = [project.configurations.shadowBundle] + archiveClassifier = 'dev-shadow' +} + +remapJar { + inputFile.set shadowJar.archiveFile +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java new file mode 100644 index 0000000..c3dcf38 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -0,0 +1,23 @@ +package za.co.neroland.nerospace.fabric; + +import net.fabricmc.api.ModInitializer; +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Fabric common-side entry point. Delegates all shared setup to + * {@link NerospaceCommon#init()}. + * + *

As the migration proceeds, Fabric-specific wiring goes here: + * registering networking via {@code PayloadTypeRegistry} + + * {@code ServerPlayNetworking}, event callbacks ({@code ServerTickEvents}, + * {@code ServerPlayConnectionEvents}, ...), capability/storage providers via + * the Fabric Transfer API, and Fabric data generation. + */ +public final class NerospaceFabric implements ModInitializer { + + @Override + public void onInitialize() { + NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); + NerospaceCommon.init(); + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java new file mode 100644 index 0000000..f494c31 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -0,0 +1,21 @@ +package za.co.neroland.nerospace.fabric; + +import net.fabricmc.api.ClientModInitializer; +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Fabric client entry point. The equivalent of the root project's + * {@code NerospaceClient} client-only registrations. + * + *

Migration target for: entity renderers ({@code EntityRendererRegistry}), + * screens ({@code HandledScreens}), HUD ({@code HudLayerRegistrationCallback}), + * and ModMenu config screen integration. + */ +public final class NerospaceFabricClient implements ClientModInitializer { + + @Override + public void onInitializeClient() { + NerospaceCommon.LOGGER.info("[Nerospace] Fabric client bootstrap"); + // TODO (migration): client-only registrations. + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java new file mode 100644 index 0000000..28a6dd6 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java @@ -0,0 +1,31 @@ +package za.co.neroland.nerospace.platform; + +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; + +/** + * Fabric implementation of {@link IPlatformHelper}. Registered via + * {@code META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper}. + */ +public final class FabricPlatformHelper implements IPlatformHelper { + + @Override + public String getPlatformName() { + return "Fabric"; + } + + @Override + public boolean isDevelopmentEnvironment() { + return FabricLoader.getInstance().isDevelopmentEnvironment(); + } + + @Override + public boolean isModLoaded(String modId) { + return FabricLoader.getInstance().isModLoaded(modId); + } + + @Override + public boolean isClient() { + return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; + } +} diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper new file mode 100644 index 0000000..73911ae --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.FabricPlatformHelper diff --git a/multiloader/fabric/src/main/resources/fabric.mod.json b/multiloader/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..3528f37 --- /dev/null +++ b/multiloader/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${mod_version}", + "name": "${mod_name}", + "description": "Nerospace — multiloader build (Fabric).", + "authors": ["${mod_authors}"], + "license": "${mod_license}", + "environment": "*", + "entrypoints": { + "main": ["za.co.neroland.nerospace.fabric.NerospaceFabric"], + "client": ["za.co.neroland.nerospace.fabric.NerospaceFabricClient"] + }, + "mixins": [ + "nerospace-common.mixins.json", + "nerospace.mixins.json" + ], + "depends": { + "fabricloader": ">=${fabric_loader_version}", + "minecraft": ">=${minecraft_version}", + "java": ">=21" + }, + "suggests": { + "fabric-api": "*", + "architectury": "*" + } +} diff --git a/multiloader/fabric/src/main/resources/nerospace.mixins.json b/multiloader/fabric/src/main/resources/nerospace.mixins.json new file mode 100644 index 0000000..4ab9ab8 --- /dev/null +++ b/multiloader/fabric/src/main/resources/nerospace.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "za.co.neroland.nerospace.fabric.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties new file mode 100644 index 0000000..98f2ff8 --- /dev/null +++ b/multiloader/gradle.properties @@ -0,0 +1,59 @@ +# ---------------------------------------------------------------------------- +# Nerospace multiloader scaffold — version variables +# +# This is a SELF-CONTAINED scaffold. It does not affect the working +# single-loader build at the repo root. See multiloader/README.md. +# +# Two axes: +# * Loaders -> Architectury (common / fabric / neoforge sub-modules) +# * Versions -> Stonecutter (the mc_versions list below) +# +# !! VERIFY the Fabric / Architectury versions before the first build !! +# Look them up at: https://fabricmc.net/develop and +# https://docs.architectury.dev — pin the values that exist for the +# Minecraft version you are building. The placeholders below are the +# last-known-good shapes, NOT guaranteed to resolve for 26.x yet. +# ---------------------------------------------------------------------------- + +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=true +org.gradle.parallel=true +# Architectury Loom + configuration cache do not always agree; leave OFF here. +org.gradle.configuration-cache=false + +## Mod metadata (shared by both loaders) ------------------------------------- +mod_id=nerospace +mod_name=Nerospace +mod_version=1.0.0-alpha.1 +mod_group_id=za.co.neroland.nerospace +mod_license=All Rights Reserved (modpacks allowed - see LICENSE) +mod_authors=Neroland + +## Active Minecraft version -------------------------------------------------- +# Stonecutter overrides this per version-node; it is also the value used when +# building a single version directly. Keep in sync with the mc_versions list. +minecraft_version=26.1.2 +minecraft_version_range=[26.1.2,) + +## Stonecutter version axis -------------------------------------------------- +# Comma-separated list of the Minecraft versions you want to ship. +mc_versions=26.1.2,26.2 + +## Per-version dependency pins ---------------------------------------------- +# NeoForge artifact (encodes the MC version: 26.1.2.x is ONLY for MC 26.1.2). +neo_version_26.1.2=26.1.2.76 +neo_version_26.2=26.2.0.1 + +# Fabric Loader (version-agnostic, but pin a known-good release). +fabric_loader_version=0.18.4 + +# Fabric API (per MC version — VERIFY these exist for your target). +fabric_api_version_26.1.2=0.130.0+26.1.2 +fabric_api_version_26.2=0.131.0+26.2 + +## Architectury ------------------------------------------------------------- +# architectury-loom is the Gradle plugin; architectury-api is the runtime lib. +architectury_loom_version=1.11-SNAPSHOT +architectury_plugin_version=3.4-SNAPSHOT +architectury_api_version_26.1.2=18.0.0 +architectury_api_version_26.2=18.1.0 diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle new file mode 100644 index 0000000..fdce02d --- /dev/null +++ b/multiloader/neoforge/build.gradle @@ -0,0 +1,47 @@ +// neoforge: NeoForge entry point + platform service implementations. +// Depends on :common and bundles its transformed classes into the jar. + +plugins { + id 'com.gradleup.shadow' version '8.3.5' +} + +def mc = rootProject.minecraft_version +def neoVersion = project.findProperty("neo_version_${mc}") +def architecturyApi = project.findProperty("architectury_api_version_${mc}") + +architectury { + platformSetupLoomIde() + neoForge() +} + +configurations { + common + shadowBundle + compileClasspath.extendsFrom common + runtimeClasspath.extendsFrom common + developmentNeoForge.extendsFrom common +} + +dependencies { + neoForge "net.neoforged:neoforge:${neoVersion}" + + common(project(path: ':common', configuration: 'namedElements')) { transitive false } + shadowBundle(project(path: ':common', configuration: 'transformProductionNeoForge')) + + if (architecturyApi != null) { + modApi "dev.architectury:architectury-neoforge:${architecturyApi}" + } +} + +processResources { + // mod metadata token expansion is configured in the root build.gradle +} + +shadowJar { + configurations = [project.configurations.shadowBundle] + archiveClassifier = 'dev-shadow' +} + +remapJar { + inputFile.set shadowJar.archiveFile +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java new file mode 100644 index 0000000..7e27502 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -0,0 +1,31 @@ +package za.co.neroland.nerospace.neoforge; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.Mod; +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * NeoForge entry point. Mirrors the root project's {@code Nerospace} class + * but does nothing loader-specific beyond construction — all shared setup + * is delegated to {@link NerospaceCommon#init()}. + * + *

As the migration proceeds, the per-loader registration wiring (binding + * Architectury DeferredRegisters, capability providers via + * {@code RegisterCapabilitiesEvent}, payload registration, etc.) lives here + * and in sibling classes in this module. + */ +@Mod(NerospaceCommon.MOD_ID) +public final class NerospaceNeoForge { + + public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { + NerospaceCommon.LOGGER.info("[Nerospace] NeoForge bootstrap"); + // Shared, loader-agnostic init. + NerospaceCommon.init(); + + // TODO (migration): wire NeoForge-specific listeners on modEventBus, + // e.g. RegisterCapabilitiesEvent, RegisterPayloadHandlersEvent, + // EntityAttributeCreationEvent, GatherDataEvent, client renderer + // registration (under a Dist.CLIENT guard). + } +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java new file mode 100644 index 0000000..8ec1137 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -0,0 +1,32 @@ +package za.co.neroland.nerospace.platform; + +import net.neoforged.fml.ModList; +import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.api.distmarker.Dist; + +/** + * NeoForge implementation of {@link IPlatformHelper}. Registered via + * {@code META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper}. + */ +public final class NeoForgePlatformHelper implements IPlatformHelper { + + @Override + public String getPlatformName() { + return "NeoForge"; + } + + @Override + public boolean isDevelopmentEnvironment() { + return !FMLEnvironment.production; + } + + @Override + public boolean isModLoaded(String modId) { + return ModList.get().isLoaded(modId); + } + + @Override + public boolean isClient() { + return FMLEnvironment.dist == Dist.CLIENT; + } +} diff --git a/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..a3300f4 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,29 @@ +modLoader = "javafml" +loaderVersion = "[1,)" +license = "${mod_license}" + +[[mods]] +modId = "${mod_id}" +version = "${mod_version}" +displayName = "${mod_name}" +authors = "${mod_authors}" +description = ''' +Nerospace — multiloader build (NeoForge). +''' + +[[mixins]] +config = "nerospace-common.mixins.json" + +[[dependencies.${mod_id}]] +modId = "neoforge" +type = "required" +versionRange = "[0,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.${mod_id}]] +modId = "minecraft" +type = "required" +versionRange = "${minecraft_version_range}" +ordering = "NONE" +side = "BOTH" diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper new file mode 100644 index 0000000..1db8f90 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.IPlatformHelper @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.NeoForgePlatformHelper diff --git a/multiloader/settings.gradle b/multiloader/settings.gradle new file mode 100644 index 0000000..035d661 --- /dev/null +++ b/multiloader/settings.gradle @@ -0,0 +1,63 @@ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + maven { url 'https://maven.fabricmc.net/' } // Fabric Loom, Loader, API + maven { url 'https://maven.architectury.dev/' } // Architectury plugin + loom + API + maven { url 'https://maven.neoforged.net/releases/' } // NeoForge + maven { url 'https://maven.minecraftforge.net/' } // (transitive, Loom) + maven { url 'https://maven.kikugie.dev/releases' } // Stonecutter + maven { url 'https://maven.kikugie.dev/snapshots' } + maven { url 'https://maven.parchmentmc.org' } + } +} + +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + // Stonecutter: the multi-version manager. Available to the whole build. + // The active version axis is configured in the `stonecutter { }` block + // below — see the activation notes there and in README.md. + id 'dev.kikugie.stonecutter' version '0.9.2' +} + +rootProject.name = 'nerospace-multiloader' + +// ===================================================================== +// LOADER AXIS — Architectury multiloader split (ACTIVE) +// --------------------------------------------------------------------- +// `common` : vanilla-Minecraft-only code + cross-loader abstractions. +// `fabric` : Fabric entry point + platform service implementations. +// `neoforge` : NeoForge entry point + platform service implementations. +// A normal build of this scaffold produces one fabric jar and one +// neoforge jar for the ACTIVE Minecraft version (gradle.properties -> +// minecraft_version). +// ===================================================================== +include 'common' +include 'fabric' +include 'neoforge' + +// ===================================================================== +// VERSION AXIS — Stonecutter (SCAFFOLDED / READY TO ACTIVATE) +// --------------------------------------------------------------------- +// Combining the Architectury module split (above) with Stonecutter's +// version nodes is the "Stonecraft" pattern. Two supported ways to +// finish wiring the 2x2 (versions x loaders) matrix — pick one, see +// README.md "Finishing the version axis": +// +// A) Stonecraft (turnkey): replace this block + the plugin above with +// the `dev.meza.stonecraft` settings plugin, which wires Stonecutter +// to Architectury automatically from the `mc_versions` property. +// +// B) Hand-wired Stonecutter: enable the block below. It declares one +// node per Minecraft version; each node rebuilds the common/fabric/ +// neoforge set against that version's dependency pins. +// +// Left commented so the Architectury skeleton configures cleanly on its +// own. Uncomment (and choose path A or B) when you adopt the version axis. +// +// stonecutter { +// create(rootProject) { +// versions '26.1.2', '26.2' // keep in sync with gradle.properties mc_versions +// vcsVersion = '26.1.2' // the version your source is authored against +// } +// } diff --git a/multiloader/stonecutter.gradle b/multiloader/stonecutter.gradle new file mode 100644 index 0000000..98fc671 --- /dev/null +++ b/multiloader/stonecutter.gradle @@ -0,0 +1,33 @@ +// ===================================================================== +// Stonecutter controller (version axis) — SCAFFOLD STUB +// +// This file is only consumed once you activate Stonecutter by +// uncommenting the `stonecutter { create(rootProject) { ... } }` block in +// settings.gradle. Until then it is inert. +// +// When active, Stonecutter applies this script to the version controller +// and exposes per-node helpers. The typical contents: +// +// plugins { id 'dev.kikugie.stonecutter' } +// +// stonecutter active '26.1.2' // the version checked out in your IDE +// +// // Aggregate task that builds every declared version, e.g. +// // ./gradlew chiseledBuild +// stonecutter registerChiseled tasks.register('chiseledBuild', stonecutter.chiseled) { +// group = 'project' +// ofTask 'build' +// } +// +// Preprocessor usage in source (handles signature/API drift between +// Minecraft versions without branching): +// +// //? if >=26.2 { +// /*newSignature(); +// *///?} else { +// oldSignature(); +// //?} +// +// See README.md "Finishing the version axis" for the two wiring options +// (Stonecraft turnkey, or hand-wired Stonecutter). +// ===================================================================== From 10ba36f891ea98004f53d39a01f0270d7f2f9333 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:01:43 +0800 Subject: [PATCH 03/82] Add multiloader CI, VS Code tasks, and docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce multiloader build support: add a GitHub Actions workflow (.github/workflows/multiloader.yml) that iterates loader × Minecraft version matrix (experimental continue-on-error cells) and uploads built jars. Add VS Code run/debug templates and tasks (.vscode/launch.json, .vscode/tasks.json) to build/run NeoForge and Fabric targets and to regenerate loom genVsCodeRuns. Update multiloader/README.md with usage, CI, and VS Code instructions and matrix explanation. Adjust multiloader/build.gradle to derive minecraft_version_range for resource expansion, and enrich multiloader/gradle.properties with matrix pins, upstream availability notes, and per-version dependency placeholders. --- .github/workflows/multiloader.yml | 79 ++++++++++++++++++++++++++ .vscode/launch.json | 50 +++++++++++++++++ .vscode/tasks.json | 92 +++++++++++++++++++++++++++++++ multiloader/README.md | 56 ++++++++++++++++++- multiloader/build.gradle | 6 +- multiloader/gradle.properties | 40 +++++++++----- 6 files changed, 306 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/multiloader.yml diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml new file mode 100644 index 0000000..813cdc7 --- /dev/null +++ b/.github/workflows/multiloader.yml @@ -0,0 +1,79 @@ +name: Multiloader Build + +# Builds the multiloader scaffold (multiloader/) across the loader x Minecraft +# version matrix. Independent of the root single-loader build (build.yml). +# +# Every combo is currently `experimental: true` (continue-on-error) because the +# Architectury / Fabric / NeoForge toolchains for MC 26.1.2 and 26.2 are not all +# published yet (see multiloader/gradle.properties "UPSTREAM AVAILABILITY"). +# As each combo becomes buildable, flip its `experimental` to false so a +# regression turns the workflow red. Suggested first: neoforge @ 26.1.2. + +on: + push: + paths: + - "multiloader/**" + - ".github/workflows/multiloader.yml" + pull_request: + paths: + - "multiloader/**" + - ".github/workflows/multiloader.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.loader }} @ MC ${{ matrix.mc }} + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} + strategy: + fail-fast: false + matrix: + include: + - loader: neoforge + mc: "26.1.2" + experimental: true # flip to false once Architectury ships for 26.1.2 + - loader: fabric + mc: "26.1.2" + experimental: true # Fabric API for 26.1.2 pending (newest stable is 26.1.1) + - loader: neoforge + mc: "26.2" + experimental: true # NeoForge 26.2 pending (MC 26.2 released 2026-06-16) + - loader: fabric + mc: "26.2" + experimental: true # Fabric API for 26.2 pending + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup JDK 25 + uses: actions/setup-java@v5 + with: + java-version: "25" + distribution: "temurin" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Build ${{ matrix.loader }} for MC ${{ matrix.mc }} + run: >- + ./gradlew -p multiloader :${{ matrix.loader }}:build + -Pminecraft_version=${{ matrix.mc }} + --stacktrace + + - name: Upload ${{ matrix.loader }} ${{ matrix.mc }} jars + if: success() + uses: actions/upload-artifact@v4 + with: + name: nerospace-${{ matrix.loader }}-${{ matrix.mc }} + path: multiloader/${{ matrix.loader }}/build/libs/*.jar + if-no-files-found: ignore diff --git a/.vscode/launch.json b/.vscode/launch.json index b44c3d7..92f3b91 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -106,6 +106,56 @@ "env": {}, "console": "internalConsole", "shortenCommandLine": "none" + }, + { + "//": "===== Multiloader scaffold (multiloader/) — Architectury, loom-based =====" + }, + { + "//": "These are TEMPLATES shaped like loom's `genVsCodeRuns` output. Loom owns the exact classpath/vmArgs, so run the 'ML: Regenerate VS Code run configs (genVsCodeRuns)' task once (it also requires the 26.x toolchain to resolve) to materialize the authoritative entries. For a no-setup run, use the 'ML: Run ...' tasks in tasks.json instead. The MC version of these java launches follows whatever was last built; switch versions via the tasks (-Pminecraft_version)." + }, + { + "type": "java", + "request": "launch", + "name": "ML: NeoForge Client", + "presentation": { "group": "Multiloader (Architectury)", "order": 0 }, + "projectName": "neoforge", + "mainClass": "net.neoforged.devlaunch.Main", + "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", + "cwd": "${workspaceFolder}/multiloader/neoforge/run", + "console": "internalConsole" + }, + { + "type": "java", + "request": "launch", + "name": "ML: NeoForge Server", + "presentation": { "group": "Multiloader (Architectury)", "order": 1 }, + "projectName": "neoforge", + "mainClass": "net.neoforged.devlaunch.Main", + "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", + "cwd": "${workspaceFolder}/multiloader/neoforge/run", + "console": "internalConsole" + }, + { + "type": "java", + "request": "launch", + "name": "ML: Fabric Client", + "presentation": { "group": "Multiloader (Architectury)", "order": 2 }, + "projectName": "fabric", + "mainClass": "net.fabricmc.devlaunchinjector.Main", + "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", + "cwd": "${workspaceFolder}/multiloader/fabric/run", + "console": "internalConsole" + }, + { + "type": "java", + "request": "launch", + "name": "ML: Fabric Server", + "presentation": { "group": "Multiloader (Architectury)", "order": 3 }, + "projectName": "fabric", + "mainClass": "net.fabricmc.devlaunchinjector.Main", + "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", + "cwd": "${workspaceFolder}/multiloader/fabric/run", + "console": "internalConsole" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d0d7997..cc61c87 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -37,6 +37,98 @@ "args": ["prepareDataRun"], "presentation": { "reveal": "silent", "panel": "shared", "clear": false }, "problemMatcher": [] + }, + + { "//": "------------------------------------------------------------------" }, + { "//": "Multiloader scaffold (multiloader/) — Architectury x Stonecutter." }, + { "//": "These shell out to `gradlew -p multiloader` and pick the Minecraft" }, + { "//": "version via the mlMcVersion input. Run them from the Command Palette" }, + { "//": "-> 'Tasks: Run Task'. The matching launch.json group debugs them." }, + { "//": "------------------------------------------------------------------" }, + { + "label": "ML: Build both loaders (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", "build", "-Pminecraft_version=${input:mlMcVersion}"], + "group": "build", + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Build NeoForge (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", ":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}"], + "group": "build", + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Build Fabric (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", ":fabric:build", "-Pminecraft_version=${input:mlMcVersion}"], + "group": "build", + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Run NeoForge Client (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", ":neoforge:runClient", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Run NeoForge Server (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", ":neoforge:runServer", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Run Fabric Client (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", ":fabric:runClient", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Run Fabric Server (pick MC)", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", ":fabric:runServer", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Regenerate VS Code run configs (genVsCodeRuns)", + "//": "Loom writes the authoritative classpath-based launch configs. Re-run after a version switch or dependency bump.", + "type": "process", + "command": "${workspaceFolder}/gradlew", + "windows": { "command": "${workspaceFolder}/gradlew.bat" }, + "args": ["-p", "multiloader", "genVsCodeRuns", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + } + ], + "inputs": [ + { + "id": "mlMcVersion", + "type": "pickString", + "description": "Minecraft version for the multiloader build", + "options": ["26.1.2", "26.2"], + "default": "26.1.2" } ] } diff --git a/multiloader/README.md b/multiloader/README.md index 06e354f..a22808c 100644 --- a/multiloader/README.md +++ b/multiloader/README.md @@ -73,13 +73,63 @@ The scaffold is a nested Gradle build; drive it with the repo's wrapper: ```bash # from the repo root -./gradlew -p multiloader build # both loaders, active MC version +./gradlew -p multiloader build # both loaders, default MC ./gradlew -p multiloader :fabric:build ./gradlew -p multiloader :neoforge:build + +# target a specific Minecraft version (the "configurations"): +./gradlew -p multiloader :neoforge:build -Pminecraft_version=26.1.2 +./gradlew -p multiloader :fabric:build -Pminecraft_version=26.2 ``` -Output jars land in `multiloader//build/libs/`. The active Minecraft -version is `minecraft_version` in `gradle.properties`. +Output jars land in `multiloader//build/libs/`. The default Minecraft +version is `minecraft_version` in `gradle.properties`; `-Pminecraft_version` +overrides it and selects the matching `*_` dependency pins. + +## The version matrix (26.1 and 26.2) + +The "different configurations" are the cross product of loader × Minecraft +version: + +| | MC 26.1.2 | MC 26.2 | +| --- | --- | --- | +| **NeoForge** | NeoForge `26.1.2.76` — real | pending (no 26.2 NeoForge yet) | +| **Fabric** | pending (Fabric newest stable is 26.1.1) | pending (Fabric API for 26.2 not out) | + +MC 26.2 ("Chaos Cubed") released 2026-06-16, so its modding toolchains haven't +shipped. The matrix is wired now and each cell lights up automatically when its +`*_` pin in `gradle.properties` resolves — no structural changes needed. + +A build targets one version at a time (one MC version per jar — see +[`docs/MULTILOADER.md`](../docs/MULTILOADER.md) §1). Stonecutter is only needed +once the *source* diverges between 26.1 and 26.2 APIs; until then the +property-driven matrix above is the version mechanism, and Stonecutter stays +declared-and-ready (see "Finishing the version axis"). + +## CI/CD + +`.github/workflows/multiloader.yml` builds the full matrix on pushes/PRs that +touch `multiloader/**` (plus manual `workflow_dispatch`), independent of the +root build (`build.yml`). It runs +`./gradlew -p multiloader ::build -Pminecraft_version=` per cell and +uploads the jars. Every cell is `experimental: true` (continue-on-error) while +the 26.x toolchains are pending — flip a cell's `experimental` to `false` once +it builds for real so a regression fails the workflow (start with +NeoForge @ 26.1.2). + +## VS Code + +Run configs live in the repo-root `.vscode/` so they show up alongside the +existing NeoForge ones: + +- **`tasks.json`** — `ML: …` tasks for build / runClient / runServer per loader, + plus `genVsCodeRuns`. They prompt for the Minecraft version (`26.1.2` / `26.2`) + and shell out to `gradlew -p multiloader …`. This is the no-setup way to run + the configurations. +- **`launch.json`** — a "Multiloader (Architectury)" group of debug launches. + These are templates matching loom's `genVsCodeRuns` output; run the + `ML: Regenerate VS Code run configs` task once (the toolchain must resolve + first) to populate the authoritative classpaths/args. ## Finishing the version axis (Stonecutter) diff --git a/multiloader/build.gradle b/multiloader/build.gradle index 5d24abb..154e021 100644 --- a/multiloader/build.gradle +++ b/multiloader/build.gradle @@ -59,6 +59,10 @@ subprojects { } tasks.withType(ProcessResources).configureEach { + // Range tracks the active version unless one is passed explicitly, + // so a -Pminecraft_version=26.2 build emits "[26.2,)". + def mcRange = project.findProperty('minecraft_version_range') + ?: "[${rootProject.minecraft_version},)" def expand = [ mod_id : rootProject.mod_id, mod_name : rootProject.mod_name, @@ -67,7 +71,7 @@ subprojects { mod_authors : rootProject.mod_authors, mod_group_id : rootProject.mod_group_id, minecraft_version : rootProject.minecraft_version, - minecraft_version_range: rootProject.minecraft_version_range, + minecraft_version_range: mcRange, fabric_loader_version: rootProject.fabric_loader_version, ] inputs.properties(expand) diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties index 98f2ff8..75fb454 100644 --- a/multiloader/gradle.properties +++ b/multiloader/gradle.properties @@ -6,13 +6,23 @@ # # Two axes: # * Loaders -> Architectury (common / fabric / neoforge sub-modules) -# * Versions -> Stonecutter (the mc_versions list below) +# * Versions -> the mc_versions matrix below. A build targets ONE version, +# selected with -Pminecraft_version= (CI iterates the +# matrix; see .github/workflows/multiloader.yml). Stonecutter +# layers on top once 26.1/26.2 source actually diverges. # -# !! VERIFY the Fabric / Architectury versions before the first build !! -# Look them up at: https://fabricmc.net/develop and -# https://docs.architectury.dev — pin the values that exist for the -# Minecraft version you are building. The placeholders below are the -# last-known-good shapes, NOT guaranteed to resolve for 26.x yet. +# UPSTREAM AVAILABILITY (checked 2026-06-17): +# * 26.1 line: NeoForge 26.1.2.76 is REAL. Fabric API/Architectury for +# 26.1.2 are NOT published yet (Fabric's newest stable is 26.1.1) — the +# Fabric column is "ready, pending upstream". +# * 26.2 line: Minecraft 26.2 released 2026-06-16; NO modding toolchain +# (NeoForge, Fabric API, Architectury) has shipped for it yet. The whole +# 26.2 column is "ready, pending upstream". +# CI marks the pending combos continue-on-error so they light up +# automatically once the artifacts below resolve. VERIFY + update every +# value tagged PENDING when upstream publishes: +# https://fabricmc.net/develop https://docs.architectury.dev +# https://projects.neoforged.net/neoforged/neoforge # ---------------------------------------------------------------------------- org.gradle.jvmargs=-Xmx3G @@ -30,24 +40,27 @@ mod_license=All Rights Reserved (modpacks allowed - see LICENSE) mod_authors=Neroland ## Active Minecraft version -------------------------------------------------- -# Stonecutter overrides this per version-node; it is also the value used when -# building a single version directly. Keep in sync with the mc_versions list. +# Default target; override per build with -Pminecraft_version=26.2. +# The module build scripts pick the matching *_ pins below, and +# minecraft_version_range is derived from this value (see build.gradle). minecraft_version=26.1.2 -minecraft_version_range=[26.1.2,) -## Stonecutter version axis -------------------------------------------------- -# Comma-separated list of the Minecraft versions you want to ship. +## Version matrix (the "different configurations" CI iterates) --------------- +# Comma-separated Minecraft versions to ship. Keep in sync with the CI matrix +# and (when activated) the Stonecutter version list. mc_versions=26.1.2,26.2 ## Per-version dependency pins ---------------------------------------------- # NeoForge artifact (encodes the MC version: 26.1.2.x is ONLY for MC 26.1.2). neo_version_26.1.2=26.1.2.76 +# PENDING: no NeoForge build for MC 26.2 yet (released 2026-06-16). neo_version_26.2=26.2.0.1 -# Fabric Loader (version-agnostic, but pin a known-good release). +# Fabric Loader (version-agnostic; pin a known-good release). VERIFY latest. fabric_loader_version=0.18.4 -# Fabric API (per MC version — VERIFY these exist for your target). +# Fabric API (per MC version). PENDING for both: Fabric's newest stable is +# 26.1.1 — neither 26.1.2 nor 26.2 is published yet. fabric_api_version_26.1.2=0.130.0+26.1.2 fabric_api_version_26.2=0.131.0+26.2 @@ -55,5 +68,6 @@ fabric_api_version_26.2=0.131.0+26.2 # architectury-loom is the Gradle plugin; architectury-api is the runtime lib. architectury_loom_version=1.11-SNAPSHOT architectury_plugin_version=3.4-SNAPSHOT +# PENDING for both MC versions until Architectury ports. architectury_api_version_26.1.2=18.0.0 architectury_api_version_26.2=18.1.0 From b40782c666a6e25973f037b7539d1d3fad0f76e4 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:01:58 +0800 Subject: [PATCH 04/82] Create MULTILOADER.md --- docs/MULTILOADER.md | 150 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 docs/MULTILOADER.md diff --git a/docs/MULTILOADER.md b/docs/MULTILOADER.md new file mode 100644 index 0000000..a81540c --- /dev/null +++ b/docs/MULTILOADER.md @@ -0,0 +1,150 @@ +# Multiloader & multi-version support for Nerospace + +Scope of this document: what it would take to ship Nerospace on **both NeoForge and Fabric**, and whether one project can target **multiple Minecraft versions** (e.g. 26.1 *and* 26.2) at the same time. + +Read the second question first — it changes how you structure everything. + +--- + +## 1. Can one project support Minecraft 26.1 *and* 26.2 at the same time? + +**A single built jar targets exactly one Minecraft version.** You cannot produce one artifact that loads on both 26.1 and 26.2. Reasons: + +- NeoForge artifacts are pinned per MC version. The version string itself encodes it: `26.1.2.76` is *only* for MC 26.1.2; MC 26.2 gets its own `26.2.x.y` line. Fabric Loader is version-agnostic, but Fabric API and the Yarn/Mojmap mappings are per-MC-version. +- Minecraft's internal classes, method signatures and registries change between minor versions. Code compiled against 26.1 mappings will not resolve against 26.2 (this project already relies on exact 26.1 signatures — see the `interact`/`interactAt` merge note in `CLAUDE.md`). + +**So "support both versions" means "produce a separate jar per version from one source tree."** Two ways to do that: + +| Strategy | What it is | Cost | +|---|---|---| +| **Git branches per MC version** (status quo for most mods) | One branch per MC version; cherry-pick fixes across them. | Cheap to start, expensive to maintain — every fix is N cherry-picks. | +| **[Stonecutter](https://stonecutter.kikugie.dev/)** (recommended) | A Gradle plugin that keeps a *single* source tree and uses preprocessor comments to swap version-specific fragments at build time. Builds one jar per declared version into `versions//build/libs/`. | Higher up-front setup; near-zero per-version maintenance after. | + +With Stonecutter you annotate the handful of lines that differ between versions, e.g.: + +```java +//? if >=26.2 { +/*newSignature(); +*///?} else { +oldSignature(); +//?} +``` + +Everything else stays as ordinary code shared by all versions. + +### The two axes are independent and combine + +Loader (NeoForge / Fabric) and MC version (26.1 / 26.2) are orthogonal. Supporting both on both is a **matrix** of jars: + +``` + MC 26.1 MC 26.2 +NeoForge nerospace-26.1-neoforge nerospace-26.2-neoforge +Fabric nerospace-26.1-fabric nerospace-26.2-fabric +``` + +[**Stonecraft**](https://stonecraft.meza.gg/) is a Gradle plugin that wires Stonecutter (versions) together with Architectury (loaders) specifically to manage this matrix with less boilerplate. If you want both axes, start there. + +--- + +## 2. Supporting both NeoForge and Fabric + +### 2.1 Reality check first + +Nerospace is **deeply** coupled to NeoForge — far more than a typical "blocks and items" mod. The hard dependencies are: + +- The entire machine/storage/pipe/rocket system is built on the **NeoForge capabilities + `net.neoforged.neoforge.transfer` framework** (Energy / Fluid / Item / custom Gas `ResourceHandler`s) — ~30+ files. +- **20+ event handlers** via `@SubscribeEvent` / `@EventBusSubscriber`. +- **NeoForge-only features**: data Attachments, `FluidType`/`BaseFlowingFluid`, biome modifiers, `ModConfigSpec`, the payload networking system, `GuiLayer` HUD rendering. + +Fabric has equivalents for *most* of these, but **not the same APIs** — in several cases (energy, attachments, fluids) the ecosystems are fundamentally different. This is a real port, not a config flag. Budget accordingly; the capability/transfer abstraction alone is the bulk of the work. + +### 2.2 Choose a project layout + +Two established patterns. Both split loader-specific code from shared code. + +**Option A — Architectury** ([API](https://www.curseforge.com/minecraft/mc-mods/architectury-api)) +A `common` module written against Architectury's cross-loader abstractions (`@ExpectPlatform`, Architectury `DeferredRegister`, Event API), plus thin `fabric` and `neoforge` modules. Most batteries included; adds the Architectury API as a runtime dependency. + +**Option B — MultiLoader-Template** ([illusivesoulworks](https://github.com/illusivesoulworks/multiloader-template) / Jared-style) +A `common` source set compiled against **vanilla Mojmap only** (no loader APIs), plus `fabric` and `neoforge` subprojects. Loader-specific behavior is reached through a hand-rolled `@ExpectPlatform`-style services pattern using Java's `ServiceLoader`. No extra runtime dependency; you write more of the glue yourself. + +**Recommendation for Nerospace:** Option A (Architectury). With this much capability and event code to abstract, the ready-made cross-loader registries and event API save substantial work, and Stonecraft can wire it to Stonecutter if you also want multi-version. + +### 2.3 Target build structure + +The current single ModDevGradle build becomes the `neoforge` subproject. New top-level layout: + +``` +settings.gradle // includes :common, :fabric, :neoforge +build.gradle // shared config, Architectury loom plugin +gradle.properties // versions for both loaders + Fabric API/Loom +common/ // vanilla-only + Architectury abstractions + src/main/java/za/co/neroland/nerospace/... +fabric/ + src/main/java/.../fabric/ // FabricModInit, platform impls + src/main/resources/fabric.mod.json +neoforge/ + src/main/java/.../neoforge/ // current Nerospace.java lives here + src/main/resources/META-INF/neoforge.mods.toml // existing template +``` + +Build tooling: Fabric uses **Loom**; NeoForge keeps **ModDevGradle** (or NeoGradle under Architectury). The `tools/gradle-mcp` server and the Python asset/model generators are loader-agnostic and keep working — they operate on `src/main/resources` and Blockbench sources, which move to `common`. + +### 2.4 Work breakdown — what changes, file by file + +The pattern throughout: **shared logic moves to `common`; anything importing `net.neoforged.*` becomes an interface in `common` with a NeoForge impl and a Fabric impl.** + +| Subsystem | Current (NeoForge) | Fabric equivalent | Migration | +|---|---|---|---| +| **Mod entry** | `Nerospace.java` `@Mod` + `IEventBus` ctor | `ModInitializer.onInitialize()` | Common `init()` called from both. NeoForge keeps `@Mod`; Fabric adds `FabricModInit implements ModInitializer` + `ClientModInitializer`. | +| **Registration** | `DeferredRegister`, `DeferredBlock/Item/Holder` (12 registry classes) | `Registry.register(...)` / Fabric registry helpers | Replace with Architectury `DeferredRegister` (one API, both loaders) in `common`. Largest mechanical change but low-risk. | +| **Energy/Fluid/Item/Gas transfer** ⚠️ | `net.neoforged.neoforge.transfer.*` `ResourceHandler`s, `Capabilities.Energy/Fluid/Item`, custom `GasCapability` (~30 files) | Fabric Transfer API (`Storage`, `FluidVariant`, `ItemVariant`) + **Team Reborn Energy** for power | **Hardest part.** No shared energy standard: NeoForge energy ≠ Fabric/TR energy. Define `common` capability interfaces (`EnergyContainer`, fluid/item/gas storage) used by all block-entity logic; implement provider exposure per loader (NeoForge `RegisterCapabilitiesEvent` vs Fabric `*Storage.SIDED.registerForBlockEntity`). Plan real time here. | +| **Events** (20+ `@SubscribeEvent`) | NeoForge event bus | Fabric callbacks (`ServerTickEvents`, `ServerPlayConnectionEvents`, `AttackBlockCallback`, `ServerEntityEvents`…) + Mixins where no callback exists | Move handler *logic* to `common` static methods; register them per loader. Architectury Event API covers many. `LivingFallEvent`, `PlayerInteractEvent` etc. map to Fabric callbacks; some need a Mixin. | +| **Networking** | `RegisterPayloadHandlersEvent`, `PayloadRegistrar`, `PacketDistributor` | `PayloadTypeRegistry` + `ServerPlayNetworking`/`ClientPlayNetworking` | Payload record classes (`OxygenFieldSyncPayload`, `SetPipeModePayload`) implement vanilla `CustomPacketPayload` → mostly shared in `common`. Registration + send/distribute differ per loader. | +| **Config** | `ModConfigSpec` (`Config.java`) | cloth-config / midnightlib, or plain JSON | Abstract config access behind a `common` interface; back it with `ModConfigSpec` (NeoForge) and a Fabric lib. | +| **Attachments** ⚠️ | `AttachmentType` (`ModAttachments.java`) — oxygen, progress, etc. | No direct equivalent | Use **Cardinal Components API** on Fabric, or a custom per-entity NBT layer. Non-trivial; design a `common` attachment abstraction. | +| **Fluids** | `FluidType` + `BaseFlowingFluid` (`ModFluids.java`) | Fabric fluid API (`FlowableFluid`) + fluid render handler | Reimplement fluid registration/rendering per loader behind a common factory. | +| **Creative tabs** | `ModCreativeModeTabs` (NeoForge builder) | `FabricItemGroup` / vanilla `CreativeModeTab` | Small; abstract the tab builder. | +| **Menus** | `IMenuTypeExtension` (`ModMenuTypes`) | `ExtendedScreenHandlerType` | Both support extra open-data; thin per-loader factory. | +| **Biome/worldgen mods** | `BiomeModifier`, `AddFeaturesBiomeModifier` | Fabric `BiomeModifications` API | Reimplement the modifier per loader; configured/placed features (`ModConfiguredFeatures`, `ModPlacedFeatures`) are vanilla and stay in `common`. | +| **Datagen** | `GatherDataEvent` + NeoForge providers (`ModModelProvider` uses `ExtendedModelTemplate`, `BlockTagsProvider`, `LanguageProvider`…) | Fabric Data Generation API (`fabric-datagen`) | Each provider re-parents to its loader's base class. Provider *content* (recipes, loot, tags, lang) is shared logic. NeoForge `ExtendedModelTemplate` has no Fabric equivalent — rework those models or run datagen only on NeoForge and copy outputs. | +| **Chunk loading** | `RegisterTicketControllersEvent`, `TicketController` (terraformer, quarry) | Fabric `ServerChunkManager`/`LoadingValidationCallback` forced-chunk API | Abstract a `common` chunk-loader service; implement per loader. | +| **Client HUD** | `GuiLayer`, `RegisterGuiLayersEvent`, `RenderGuiLayerEvent` (oxygen HUD) | `HudLayerRegistrationCallback` / `HudRenderCallback` | Move draw logic to `common`; register per loader. | +| **Client renderers** | `EntityRenderersEvent`, `RegisterMenuScreensEvent` | `EntityRendererRegistry`, `MenuScreens`/`HandledScreens` | Renderer/screen classes themselves are mostly vanilla; only registration changes. | +| **Config screen** | `IConfigScreenFactory` (NeoForge) | **ModMenu API** on Fabric | Per-loader; optional. | +| **Commands** | `RegisterCommandsEvent` | Fabric `CommandRegistrationCallback` | Command bodies are vanilla Brigadier → shared; registration differs. | +| **Gametests** | `RegisterGameTestsEvent` | Fabric `fabric-gametest` | Per-loader registration; test bodies shared. | +| **JEI compat** | `compat/jei` (NeoForge JEI) | JEI also ships a Fabric build | The JEI plugin API is the same across loaders; mostly recompile against the Fabric JEI artifact. | +| **Telemetry** | `FMLEnvironment` for client/server detect, Sentry JarJar | Fabric `FabricLoader.getEnvironmentType()`; Fabric jar-in-jar nesting | Abstract environment detection; both loaders support nested jars. | + +⚠️ = highest-effort / highest-risk items. + +### 2.5 What does *not* change + +These are vanilla Minecraft and live in `common` untouched: entity AI and creature classes, block/block-entity mechanics, the pipe-network routing math, world-gen feature *logic*, Brigadier command bodies, screen layout code, and all art/asset pipelines (`tools/gen_textures.py`, `gen_bbmodels.py`, `model_sync.py`, Blockbench sources). Textures, models, lang, loot and recipe JSON under `src/main/resources` move to `common` and are shared by both loaders. + +### 2.6 Suggested sequence + +1. Stand up the Architectury `common`/`fabric`/`neoforge` skeleton; move existing NeoForge code into `:neoforge` and confirm it still builds (`mcp__gradle__gradle_build`). +2. Migrate registration to Architectury `DeferredRegister` in `common`. Re-verify NeoForge build. +3. Define `common` abstractions for **capabilities/transfer** and implement the NeoForge side (re-wiring existing handlers). This is the long pole. +4. Bring up the Fabric side incrementally: registration → networking → events → transfer (Team Reborn Energy + Fabric Transfer API) → attachments (Cardinal Components) → client. +5. Add `fabric.mod.json`; split datagen per loader. +6. (Optional) Layer in Stonecutter/Stonecraft for 26.1 + 26.2 once the loader split is stable. + +Verify each step with the gradle MCP server before moving on, per `CLAUDE.md`. + +--- + +## 3. Bottom line + +- **One jar = one MC version.** Supporting 26.1 and 26.2 means separate jars from one source tree — use **Stonecutter** rather than divergent branches. +- **NeoForge + Fabric is a substantial port**, not a setting. Use **Architectury** (Option A) for the loader split; expect the **capabilities/energy-transfer** layer, **attachments**, and **fluids** to dominate the effort because Fabric's equivalents are different APIs, not drop-in replacements. +- Both axes combine into a 2×2 build matrix; **Stonecraft** exists to manage exactly that combination. + +### Sources +- [Stonecutter — multi-version Gradle plugin](https://stonecutter.kikugie.dev/) +- [Stonecraft — Stonecutter + Architectury wiring](https://stonecraft.meza.gg/) +- [Architectury API](https://www.curseforge.com/minecraft/mc-mods/architectury-api) +- [illusivesoulworks/multiloader-template](https://github.com/illusivesoulworks/multiloader-template) +- [Kotlin-Multiloader-Template (common source set rationale)](https://github.com/Erdragh/Kotlin-Multiloader-Template) From 0a4dd5eb08a1e40d2813c5d6e60d409cbb36a2a0 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:37:58 +0800 Subject: [PATCH 05/82] Use multiloader Gradle wrapper; update tooling Add a dedicated Gradle wrapper (9.5.1) for the multiloader nested build and make CI/tasks/VS Code use it (chmod multiloader/gradlew, set working-directory/cwd and point commands at multiloader/gradlew). Update multiloader build files: pin architectury-loom 1.17.483, adjust mappings commentary, and update fabric/architectury version pins. Improve VS Code launch/presentation formatting and rewrite tasks.json to call the multiloader wrapper (add a "Refresh loom caches" task). Expand docs (docs/MULTILOADER.md and multiloader/README.md) with architecture guidance and troubleshooting notes explaining why the separate wrapper is required and that architectury-loom currently lacks de-obfuscated MC 26.x support, which blocks builds. --- .github/workflows/multiloader.yml | 7 +- .vscode/launch.json | 24 +- .vscode/tasks.json | 77 +++--- docs/MULTILOADER.md | 47 ++++ multiloader/README.md | 108 ++++++-- multiloader/build.gradle | 18 +- multiloader/gradle.properties | 26 +- multiloader/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 11 + multiloader/gradlew | 251 ++++++++++++++++++ multiloader/gradlew.bat | 94 +++++++ 11 files changed, 587 insertions(+), 76 deletions(-) create mode 100644 multiloader/gradle/wrapper/gradle-wrapper.jar create mode 100644 multiloader/gradle/wrapper/gradle-wrapper.properties create mode 100644 multiloader/gradlew create mode 100644 multiloader/gradlew.bat diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml index 813cdc7..7d8782f 100644 --- a/.github/workflows/multiloader.yml +++ b/.github/workflows/multiloader.yml @@ -61,12 +61,15 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + # The multiloader build uses its OWN Gradle wrapper (9.5.1), independent of + # the repo root's 9.2.1 — Architectury Loom 1.17.x requires Gradle >= 9.4. - name: Make Gradle wrapper executable - run: chmod +x ./gradlew + run: chmod +x ./multiloader/gradlew - name: Build ${{ matrix.loader }} for MC ${{ matrix.mc }} + working-directory: multiloader run: >- - ./gradlew -p multiloader :${{ matrix.loader }}:build + ./gradlew :${{ matrix.loader }}:build -Pminecraft_version=${{ matrix.mc }} --stacktrace diff --git a/.vscode/launch.json b/.vscode/launch.json index 92f3b91..1aea792 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -108,16 +108,19 @@ "shortenCommandLine": "none" }, { - "//": "===== Multiloader scaffold (multiloader/) — Architectury, loom-based =====" + "//": "\u003d\u003d\u003d\u003d\u003d Multiloader scaffold (multiloader/) — Architectury, loom-based \u003d\u003d\u003d\u003d\u003d" }, { - "//": "These are TEMPLATES shaped like loom's `genVsCodeRuns` output. Loom owns the exact classpath/vmArgs, so run the 'ML: Regenerate VS Code run configs (genVsCodeRuns)' task once (it also requires the 26.x toolchain to resolve) to materialize the authoritative entries. For a no-setup run, use the 'ML: Run ...' tasks in tasks.json instead. The MC version of these java launches follows whatever was last built; switch versions via the tasks (-Pminecraft_version)." + "//": "These are TEMPLATES shaped like loom\u0027s `genVsCodeRuns` output. Loom owns the exact classpath/vmArgs, so run the \u0027ML: Regenerate VS Code run configs (genVsCodeRuns)\u0027 task once (it also requires the 26.x toolchain to resolve) to materialize the authoritative entries. For a no-setup run, use the \u0027ML: Run ...\u0027 tasks in tasks.json instead. The MC version of these java launches follows whatever was last built; switch versions via the tasks (-Pminecraft_version)." }, { "type": "java", "request": "launch", "name": "ML: NeoForge Client", - "presentation": { "group": "Multiloader (Architectury)", "order": 0 }, + "presentation": { + "group": "Multiloader (Architectury)", + "order": 0 + }, "projectName": "neoforge", "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", @@ -128,7 +131,10 @@ "type": "java", "request": "launch", "name": "ML: NeoForge Server", - "presentation": { "group": "Multiloader (Architectury)", "order": 1 }, + "presentation": { + "group": "Multiloader (Architectury)", + "order": 1 + }, "projectName": "neoforge", "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", @@ -139,7 +145,10 @@ "type": "java", "request": "launch", "name": "ML: Fabric Client", - "presentation": { "group": "Multiloader (Architectury)", "order": 2 }, + "presentation": { + "group": "Multiloader (Architectury)", + "order": 2 + }, "projectName": "fabric", "mainClass": "net.fabricmc.devlaunchinjector.Main", "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", @@ -150,7 +159,10 @@ "type": "java", "request": "launch", "name": "ML: Fabric Server", - "presentation": { "group": "Multiloader (Architectury)", "order": 3 }, + "presentation": { + "group": "Multiloader (Architectury)", + "order": 3 + }, "projectName": "fabric", "mainClass": "net.fabricmc.devlaunchinjector.Main", "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index cc61c87..42a1da4 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,5 +1,5 @@ { - "//": "preLaunchTasks for launch.json: each runs the matching ModDevGradle 'prepare' task to (re)generate build/moddev/Args.txt before launch, so launches never fail after a clean. Cross-platform: gradlew on Linux/macOS, gradlew.bat on Windows.", + "//": "preLaunchTasks for launch.json regenerate build/moddev/Args.txt before launch. Cross-platform: gradlew on Linux/macOS, gradlew.bat on Windows.", "version": "2.0.0", "tasks": [ { @@ -38,19 +38,14 @@ "presentation": { "reveal": "silent", "panel": "shared", "clear": false }, "problemMatcher": [] }, - - { "//": "------------------------------------------------------------------" }, - { "//": "Multiloader scaffold (multiloader/) — Architectury x Stonecutter." }, - { "//": "These shell out to `gradlew -p multiloader` and pick the Minecraft" }, - { "//": "version via the mlMcVersion input. Run them from the Command Palette" }, - { "//": "-> 'Tasks: Run Task'. The matching launch.json group debugs them." }, - { "//": "------------------------------------------------------------------" }, + { "//": "Multiloader scaffold: run multiloader's OWN wrapper (Gradle 9.5.1; loom 1.17 needs >=9.4) with cwd=multiloader. Pick MC via mlMcVersion input." }, { "label": "ML: Build both loaders (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", "build", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": ["build", "-Pminecraft_version=${input:mlMcVersion}"], "group": "build", "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] @@ -58,9 +53,10 @@ { "label": "ML: Build NeoForge (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", ":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}"], "group": "build", "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] @@ -68,9 +64,10 @@ { "label": "ML: Build Fabric (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", ":fabric:build", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:build", "-Pminecraft_version=${input:mlMcVersion}"], "group": "build", "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] @@ -78,46 +75,60 @@ { "label": "ML: Run NeoForge Client (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", ":neoforge:runClient", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":neoforge:runClient", "-Pminecraft_version=${input:mlMcVersion}"], "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { "label": "ML: Run NeoForge Server (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", ":neoforge:runServer", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":neoforge:runServer", "-Pminecraft_version=${input:mlMcVersion}"], "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { "label": "ML: Run Fabric Client (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", ":fabric:runClient", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:runClient", "-Pminecraft_version=${input:mlMcVersion}"], "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { "label": "ML: Run Fabric Server (pick MC)", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", ":fabric:runServer", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:runServer", "-Pminecraft_version=${input:mlMcVersion}"], "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { "label": "ML: Regenerate VS Code run configs (genVsCodeRuns)", - "//": "Loom writes the authoritative classpath-based launch configs. Re-run after a version switch or dependency bump.", "type": "process", - "command": "${workspaceFolder}/gradlew", - "windows": { "command": "${workspaceFolder}/gradlew.bat" }, - "args": ["-p", "multiloader", "genVsCodeRuns", "-Pminecraft_version=${input:mlMcVersion}"], + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": ["genVsCodeRuns", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Refresh loom caches", + "type": "process", + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}", "--refresh-dependencies"], "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] } diff --git a/docs/MULTILOADER.md b/docs/MULTILOADER.md index a81540c..ad1ad34 100644 --- a/docs/MULTILOADER.md +++ b/docs/MULTILOADER.md @@ -6,6 +6,53 @@ Read the second question first — it changes how you structure everything. --- +## 0. Architecture decision (recommended): one codebase, no drift + +The guiding constraint for this project is **do not let the codebase drift apart** — never end up fixing the same bug twice across copies. That rules out the tempting-but-fatal pattern of a branch per loader or per Minecraft version. The decision: + +- **One repository, one source tree.** Shared game logic lives exactly once in a `common` module; only the thin per-loader entry points and platform shims are loader-specific. This is the entire point of the common-module split — there is no second copy of the mechanics to drift. +- **Stonecutter for the version axis (26.1 / 26.2).** Both versions build from the same tree, with preprocessor comments only on the handful of lines where the APIs differ. This is the purpose-built anti-drift tool; prefer it over version branches. +- **Architectury for the loader axis — when it's ready.** Architectury is the *easiest to work in*: its API gives cross-loader `DeferredRegister`, events, networking and config, so `common` does more with less hand-written glue. Adopt it as the target once architectury-loom supports de-obfuscated Minecraft 26.x (see §2.x / Troubleshooting in the scaffold README). +- **Until architectury-loom catches up, stay NeoForge-only on the existing single project.** It is the lowest-friction, zero-drift state today: one toolchain, one copy, already building and publishing. Bringing up Fabric now buys friction without a working Fabric build, because architectury-loom can't yet consume de-obfuscated 26.x mappings. +- **If Fabric is needed *before* architectury-loom is ready,** use the MultiLoader-Template layout (Fabric Loom for `:fabric`, ModDevGradle for `:neoforge`) instead — each loader's native, de-obf-ready toolchain. It still shares one `common`, so it doesn't drift; it just costs more hand-written glue than Architectury. Do **not** unblock architectury-loom by hand-feeding it a synthetic mappings file — that satisfies the config check but breaks loom's remap pipeline downstream. + +In one line: **single repo + `common` module + Stonecutter = no drift; Architectury makes that the easiest to work in; until architectury-loom supports de-obf 26.x, stay NeoForge-only rather than splitting.** + +--- + +## 0b. Field notes: how the ecosystem builds multiloader on de-obf 26.x (researched 2026-06-18) + +Hard facts gathered while trying to unblock 26.2, so the next person doesn't re-walk this: + +- **Architectury is a dead end for 26.x right now.** architectury-loom issue [#328 "26.1 Support"](https://github.com/architectury/architectury-loom/issues/328) is open (since 2026-02-01) with no fix, no PR, no assignee: it cannot consume de-obfuscated 26.x mappings. Verified directly against this repo via the gradle MCP — every mappings config fails (`officialMojangMappings()` → "Failed to find official mojang mappings"; omitted → "Configuration 'mappings' has no dependencies"; `loom.layered {}` → `NullPointerException`). Do **not** try to force it with a synthetic mappings file; that passes config but breaks loom's remap pipeline. + +- **What mods actually use instead: the MultiLoader-Template — no architectury-loom.** The canonical [jaredlll08/MultiLoader-Template](https://github.com/jaredlll08/MultiLoader-Template) (its source comments reference NeoForge's `26.2.x` branch, so it's current) wires each module to its loader's *native* toolchain: + - `common` → `net.neoforged.moddev` (ModDevGradle, via NeoForm) — no mappings step + - `fabric` → `net.fabricmc.fabric-loom` (de-obf-ready; **omits** the `mappings` line, like the official Fabric template) + - `neoforge`→ `net.neoforged.moddev` + - shared code lives once in `common`; loader modules consume it through `build-logic` convention plugins (`multiloader-common` / `multiloader-loader`). No drift, no Architectury API. + +- **Both native toolchains support de-obf 26.x; architectury-loom is the only one that doesn't** — confirmed: the root NeoForge ModDevGradle build compiles against 26.1.2, and Fabric's official template builds 26.2. + +- **26.2 Maven reality (checked 2026-06-18) — what is and isn't published.** Gradle compiles against *published artifacts*, not a git branch, so what matters is which jars are on the NeoForged/Fabric Maven: + - **NeoForm 26.2: published** (`net.neoforged:neoform`, latest `26.2-snapshot-8-1`). NeoForm is the de-obfuscated vanilla base the MultiLoader-Template's `common` (ModDevGradle) compiles against — so **`common` builds on 26.2.** + - **Fabric 26.2: published** (Fabric Loader `0.19.3` + Fabric API `0.152.1+26.2`) — so **`fabric` builds on 26.2.** + - **NeoForge loader userdev 26.2: NOT published** (`net.neoforged:neoforge` metadata still tops out at `26.1.2.76`) — so the **`neoforge` module is the one blocked cell.** + - The `neoforged/NeoForge` `26.2.x` branch *exists and is buildable*, but Gradle needs its compiled userdev jar in a resolvable repo. CI hasn't pushed it to the public releases Maven yet — so either **self-build it** (clone `26.2.x` → `./gradlew publishToMavenLocal` → add `mavenLocal()` → set `neo_version_26.2` to the published version) or wait for NeoForge's CI (likely soon, given NeoForm 26.2 is already up). So 3 of 4 cells (common + both Fabric cells) build on 26.2 today; only NeoForge-26.2 needs a self-built or awaited userdev artifact. + +- **Build-unblock ≠ mod port.** Getting a Fabric 26.2 jar to *compile* is separate from porting Nerospace's NeoForge-specific systems (capabilities/transfer, attachments, fluids, networking) to Fabric — that migration (§2) is the real effort and is unchanged by the toolchain choice. + +**Recommended migration when ready** (not yet applied — the scaffold still uses architectury-loom): replace the architectury-loom scaffold with the MultiLoader-Template layout above (ModDevGradle `common` on NeoForm + Fabric Loom `fabric` + ModDevGradle `neoforge`). On 26.1.x it works as-is. On 26.2 the `common` (NeoForm `26.2-snapshot-8-1`) and `fabric` (Fabric API `0.152.1+26.2`) cells build immediately; the **`neoforge` cell** is the only one waiting, and you can unblock even that by self-building NeoForge `26.2.x` to `mavenLocal()` rather than waiting for its CI to publish. + +### Field-notes sources + +- [architectury-loom #328 — 26.1 Support](https://github.com/architectury/architectury-loom/issues/328) +- [jaredlll08/MultiLoader-Template](https://github.com/jaredlll08/MultiLoader-Template) +- [Official Fabric example mod (de-obf, omits `mappings`)](https://github.com/FabricMC/fabric-example-mod) +- [Mojang: removing obfuscation](https://www.minecraft.net/en-us/article/removing-obfuscation-in-java-edition) · [Fabric: removing obfuscation from Fabric](https://fabricmc.net/2025/10/31/obfuscation.html) + +--- + ## 1. Can one project support Minecraft 26.1 *and* 26.2 at the same time? **A single built jar targets exactly one Minecraft version.** You cannot produce one artifact that loads on both 26.1 and 26.2. Reasons: diff --git a/multiloader/README.md b/multiloader/README.md index a22808c..06f8188 100644 --- a/multiloader/README.md +++ b/multiloader/README.md @@ -69,23 +69,32 @@ with the matching `*_version` values. ## Building -The scaffold is a nested Gradle build; drive it with the repo's wrapper: +This is a **standalone nested Gradle build with its own wrapper** (Gradle +`9.5.1`). It is *not* driven by the repo-root wrapper: Architectury Loom 1.17.x +needs Gradle ≥ 9.4 (it calls `Configuration.extendsFrom(Provider…)`, added in +9.4.0), while the root stays on 9.2.1 for NeoForge/ModDevGradle. So run it from +inside `multiloader/`: ```bash -# from the repo root -./gradlew -p multiloader build # both loaders, default MC -./gradlew -p multiloader :fabric:build -./gradlew -p multiloader :neoforge:build +cd multiloader + +./gradlew build # both loaders, default MC +./gradlew :neoforge:build +./gradlew :fabric:build # target a specific Minecraft version (the "configurations"): -./gradlew -p multiloader :neoforge:build -Pminecraft_version=26.1.2 -./gradlew -p multiloader :fabric:build -Pminecraft_version=26.2 +./gradlew :neoforge:build -Pminecraft_version=26.1.2 +./gradlew :fabric:build -Pminecraft_version=26.2 ``` Output jars land in `multiloader//build/libs/`. The default Minecraft version is `minecraft_version` in `gradle.properties`; `-Pminecraft_version` overrides it and selects the matching `*_` dependency pins. +> Do **not** use `../gradlew -p multiloader` — that runs the root's Gradle 9.2.1 +> and fails with `NoSuchMethodError: Configuration.extendsFrom`. Always use +> `multiloader/gradlew`. + ## The version matrix (26.1 and 26.2) The "different configurations" are the cross product of loader × Minecraft @@ -93,12 +102,14 @@ version: | | MC 26.1.2 | MC 26.2 | | --- | --- | --- | -| **NeoForge** | NeoForge `26.1.2.76` — real | pending (no 26.2 NeoForge yet) | -| **Fabric** | pending (Fabric newest stable is 26.1.1) | pending (Fabric API for 26.2 not out) | +| **NeoForge** | NeoForge `26.1.2.76` — exists | NeoForge `26.2` not published yet | +| **Fabric** | Loader/API exist | Loader `0.19.3` + API `0.152.1+26.2` exist | -MC 26.2 ("Chaos Cubed") released 2026-06-16, so its modding toolchains haven't -shipped. The matrix is wired now and each cell lights up automatically when its -`*_` pin in `gradle.properties` resolves — no structural changes needed. +Upstream artifacts now exist for most cells (Fabric shipped 26.2 — see the +official template). **But the loom toolchain blocks the build:** architectury-loom +1.17.x can't handle de-obfuscated Minecraft 26.x mappings yet, so *no* cell +compiles regardless of versions (see Troubleshooting). This is the gating issue, +not artifact availability. A build targets one version at a time (one MC version per jar — see [`docs/MULTILOADER.md`](../docs/MULTILOADER.md) §1). Stonecutter is only needed @@ -110,12 +121,14 @@ declared-and-ready (see "Finishing the version axis"). `.github/workflows/multiloader.yml` builds the full matrix on pushes/PRs that touch `multiloader/**` (plus manual `workflow_dispatch`), independent of the -root build (`build.yml`). It runs -`./gradlew -p multiloader ::build -Pminecraft_version=` per cell and -uploads the jars. Every cell is `experimental: true` (continue-on-error) while -the 26.x toolchains are pending — flip a cell's `experimental` to `false` once -it builds for real so a regression fails the workflow (start with -NeoForge @ 26.1.2). +root build (`build.yml`). Each cell runs (with `working-directory: multiloader`, +so it uses the 9.5.1 wrapper) `./gradlew ::build +-Pminecraft_version=` and uploads the jars. Cells are `experimental: true` +(continue-on-error) because **all of them are currently blocked at loom's +mappings step** (architectury-loom can't do de-obfuscated 26.x — see +Troubleshooting), so none compile yet. Once architectury-loom ships de-obf 26.x +support, flip a cell's `experimental` to `false` when it builds so regressions +fail the workflow. ## VS Code @@ -124,12 +137,63 @@ existing NeoForge ones: - **`tasks.json`** — `ML: …` tasks for build / runClient / runServer per loader, plus `genVsCodeRuns`. They prompt for the Minecraft version (`26.1.2` / `26.2`) - and shell out to `gradlew -p multiloader …`. This is the no-setup way to run - the configurations. + and run `multiloader/gradlew` with `cwd=multiloader` (its 9.5.1 wrapper). This + is the no-setup way to run the configurations. - **`launch.json`** — a "Multiloader (Architectury)" group of debug launches. These are templates matching loom's `genVsCodeRuns` output; run the - `ML: Regenerate VS Code run configs` task once (the toolchain must resolve - first) to populate the authoritative classpaths/args. + `ML: Regenerate VS Code run configs` task once to populate the authoritative + classpaths/args. + +## Troubleshooting + +Three walls were found while wiring 26.x support (all verified against the +gradle MCP, 2026-06-18). The first two are fixed; the third is the gating +upstream limitation. None are scaffold bugs. + +**FIXED — `NoSuchMethodError: Configuration.extendsFrom(Provider[])`** (applying +loom in `subprojects`). That Gradle API landed in **Gradle 9.4.0**; +architectury-loom 1.17.x needs it, and the repo root is on 9.2.1 (kept for +NeoForge/ModDevGradle). Fix applied: this build has **its own Gradle 9.5.1 +wrapper**. Always run `multiloader/gradlew` from inside `multiloader/`, never +`../gradlew -p multiloader` (that uses the root's 9.2.1 and fails here). + +**FIXED — stale version pins.** Minecraft 26.2 ("Chaos Cubed", 2026-06-16) *does* +have a Fabric toolchain: Loader `0.19.3`, Fabric API `0.152.1+26.2` (per the +official Fabric template). Those are now pinned in `gradle.properties`. (NeoForge +26.2 still isn't published.) + +**GATING — `Failed to find official mojang mappings` (and friends).** **Minecraft +26.x is de-obfuscated** (Mojang [removed obfuscation](https://www.minecraft.net/en-us/article/removing-obfuscation-in-java-edition) +after 1.21.11; the game ships official names, no proguard file, no Intermediary — +see [Fabric's writeup](https://fabricmc.net/2025/10/31/obfuscation.html)). +Fabric's *own* loom handles this by **omitting the `mappings` line** (the +official template has none). **architectury-loom 1.17.x does not** — every +mappings config fails at `Configure project :common`: + +| `mappings` config | result on architectury-loom 1.17.x | +| --- | --- | +| *(omitted)* | `Configuration 'mappings' has no dependencies` | +| `loom.officialMojangMappings()` | `Failed to find official mojang mappings for 26.x` (no proguard file) | +| `loom.layered {}` (empty) | `NullPointerException: srcNamespace is null` | + +So architectury-loom (the fork that adds NeoForge/Forge support) still assumes an +obfuscated game with a mappings layer, and hasn't yet ported the de-obf handling +Fabric's loom has. **This blocks every cell of the multiloader** — no version pin +or mappings tweak in this scaffold can work around it. Plugin wiring is otherwise +correct (Stonecutter 0.9.2 + Architectury Plugin 3.4.164 + Loom 1.17.483 all +resolve and configure up to this step). + +What this means in practice: + +- **A pure-Fabric 26.2 mod builds today** using Fabric's own loom + (`net.fabricmc.fabric-loom`, omit `mappings`) — but that's single-loader, not + this shared-`common` Architectury setup. +- **NeoForge 26.x builds today** via the repo-root ModDevGradle build (handles + de-obf natively). That's your shippable path now. +- **The Architectury multiloader is blocked** until architectury-loom ships + de-obf 26.x support. When it does, update the `mappings` line in `build.gradle` + per its docs (and bump `architectury_loom_version`), and the cells should + compile. Track: . ## Finishing the version axis (Stonecutter) diff --git a/multiloader/build.gradle b/multiloader/build.gradle index 154e021..a0458f8 100644 --- a/multiloader/build.gradle +++ b/multiloader/build.gradle @@ -8,7 +8,13 @@ // ===================================================================== plugins { id 'architectury-plugin' version '3.4-SNAPSHOT' - id 'dev.architectury.loom' version '1.11-SNAPSHOT' apply false + // architectury-loom 1.17.x (current line; needs Gradle >= 9.4, hence this + // build's own 9.5.1 wrapper). NOTE: 1.17.x does NOT yet support + // de-obfuscated Minecraft 26.x — see the README "Troubleshooting" for the + // three mappings configs tested, all of which fail. Bump this when + // architectury-loom ships de-obf 26.x support. Keep in sync with + // architectury_loom_version in gradle.properties. + id 'dev.architectury.loom' version '1.17.483' apply false } architectury { @@ -42,8 +48,14 @@ subprojects { dependencies { minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" - // 26.x is de-obfuscated; use official Mojang names (matches the - // root project's mappings convention — no Parchment). + // Minecraft 26.x is DE-OBFUSCATED (official names, no proguard mappings / + // Intermediary). Fabric's OWN loom supports this by OMITTING `mappings`, + // but architectury-loom 1.17.x does not yet — every option fails: + // * (omitted) -> "Configuration 'mappings' has no dependencies" + // * loom.officialMojangMappings() -> "Failed to find official mojang mappings" + // * loom.layered {} -> NPE: srcNamespace is null + // Left as the conventional line; this whole module is blocked until + // architectury-loom adds de-obf 26.x support. See README "Troubleshooting". mappings loom.officialMojangMappings() } diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties index 75fb454..ce18583 100644 --- a/multiloader/gradle.properties +++ b/multiloader/gradle.properties @@ -56,18 +56,24 @@ neo_version_26.1.2=26.1.2.76 # PENDING: no NeoForge build for MC 26.2 yet (released 2026-06-16). neo_version_26.2=26.2.0.1 -# Fabric Loader (version-agnostic; pin a known-good release). VERIFY latest. -fabric_loader_version=0.18.4 +# Fabric Loader (version-agnostic). 0.19.3 per the official 26.2 template. +fabric_loader_version=0.19.3 -# Fabric API (per MC version). PENDING for both: Fabric's newest stable is -# 26.1.1 — neither 26.1.2 nor 26.2 is published yet. -fabric_api_version_26.1.2=0.130.0+26.1.2 -fabric_api_version_26.2=0.131.0+26.2 +# Fabric API (per MC version). 26.2 confirmed from the official Fabric template. +# VERIFY the 26.1.2 value at https://fabricmc.net/develop (placeholder for now). +fabric_api_version_26.1.2=0.150.0+26.1.2 +fabric_api_version_26.2=0.152.1+26.2 ## Architectury ------------------------------------------------------------- # architectury-loom is the Gradle plugin; architectury-api is the runtime lib. -architectury_loom_version=1.11-SNAPSHOT +# Loom 1.17.x requires Gradle >= 9.4 (hence this build's own 9.5.1 wrapper). +# KNOWN BLOCKER: architectury-loom 1.17.x does NOT yet support de-obfuscated MC +# 26.x mappings (Fabric's own loom does, but the architectury fork lags). See +# build.gradle dependencies + README "Troubleshooting". The plugin version is +# also pinned in build.gradle's plugins {} block — keep the two synced. +architectury_loom_version=1.17.483 architectury_plugin_version=3.4-SNAPSHOT -# PENDING for both MC versions until Architectury ports. -architectury_api_version_26.1.2=18.0.0 -architectury_api_version_26.2=18.1.0 +# architectury-api: 21.x is the current line; 21.0.2 is latest seen. VERIFY the +# version that targets each MC release at https://maven.architectury.dev. +architectury_api_version_26.1.2=21.0.2 +architectury_api_version_26.2=21.0.2 diff --git a/multiloader/gradle/wrapper/gradle-wrapper.jar b/multiloader/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8= 9.4. Pinned to the current 9.5.1. +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/multiloader/gradlew b/multiloader/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/multiloader/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/multiloader/gradlew.bat b/multiloader/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/multiloader/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 3058d7d0996b0f7d1f6d446b5ac8a231076234b6 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:55:56 +0800 Subject: [PATCH 06/82] Migrate multiloader to MultiLoader-Template Replace the Architectury-based multiloader scaffold with the MultiLoader-Template layout (ModDevGradle NeoForm/common + Fabric Loom/fabric + ModDevGradle/neoforge). Remove architectury-loom/plugin usage and related DeferredRegister wiring; share common sources via root ext properties and update module build scripts (common, fabric, neoforge) to use net.neoforged.moddev and net.fabricmc.fabric-loom. Update CI to mark fabric@26.2 verified, adjust gradle.properties pins (default MC 26.2, NeoForm/Fabric/NeoForge notes) and add guidance for self-building NeoForge to mavenLocal. Docs and README updated to explain rationale (architectury-loom lacks de-obf 26.x support), build instructions, and migration status; ModRegistries and NerospaceCommon adjusted to reflect the new registration seam. --- .github/workflows/multiloader.yml | 21 +- docs/MULTILOADER.md | 11 +- multiloader/README.md | 302 +++++------------- multiloader/build.gradle | 80 ++--- multiloader/common/build.gradle | 31 +- .../neroland/nerospace/NerospaceCommon.java | 9 +- .../nerospace/registry/ModRegistries.java | 48 +-- multiloader/fabric/build.gradle | 56 ++-- multiloader/gradle.properties | 79 ++--- multiloader/neoforge/build.gradle | 65 ++-- multiloader/settings.gradle | 61 +--- 11 files changed, 249 insertions(+), 514 deletions(-) diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml index 7d8782f..6f29b8a 100644 --- a/.github/workflows/multiloader.yml +++ b/.github/workflows/multiloader.yml @@ -3,11 +3,12 @@ name: Multiloader Build # Builds the multiloader scaffold (multiloader/) across the loader x Minecraft # version matrix. Independent of the root single-loader build (build.yml). # -# Every combo is currently `experimental: true` (continue-on-error) because the -# Architectury / Fabric / NeoForge toolchains for MC 26.1.2 and 26.2 are not all -# published yet (see multiloader/gradle.properties "UPSTREAM AVAILABILITY"). -# As each combo becomes buildable, flip its `experimental` to false so a -# regression turns the workflow red. Suggested first: neoforge @ 26.1.2. +# Layout: MultiLoader-Template (ModDevGradle common+neoforge, Fabric Loom fabric) +# — no architectury-loom (it can't do de-obf 26.x). fabric @ 26.2 is verified +# green (Fabric Loom 1.17 + fabric-api 0.152.1+26.2, NeoForm 26.2-1). The +# remaining combos stay `experimental: true` until their pins are confirmed / +# NeoForge 26.2 userdev resolves (beta on Maven, or self-built to mavenLocal — +# see multiloader/README.md). Flip a combo's `experimental` to false once green. on: push: @@ -34,16 +35,16 @@ jobs: include: - loader: neoforge mc: "26.1.2" - experimental: true # flip to false once Architectury ships for 26.1.2 + experimental: true # ModDevGradle + NeoForge 26.1.2.76 (verify in CI, then flip) - loader: fabric mc: "26.1.2" - experimental: true # Fabric API for 26.1.2 pending (newest stable is 26.1.1) + experimental: true # confirm fabric_api_version_26.1.2 at fabricmc.net/develop - loader: neoforge mc: "26.2" - experimental: true # NeoForge 26.2 pending (MC 26.2 released 2026-06-16) + experimental: true # needs NeoForge 26.2 userdev (beta on Maven or self-built) - loader: fabric mc: "26.2" - experimental: true # Fabric API for 26.2 pending + experimental: false # VERIFIED green 2026-06-18 (Fabric Loom 1.17 + fabric-api 0.152.1+26.2) steps: - name: Checkout repository @@ -62,7 +63,7 @@ jobs: uses: gradle/actions/setup-gradle@v4 # The multiloader build uses its OWN Gradle wrapper (9.5.1), independent of - # the repo root's 9.2.1 — Architectury Loom 1.17.x requires Gradle >= 9.4. + # the repo root's 9.2.1 — Fabric Loom 1.17 requires Gradle >= 9.4. - name: Make Gradle wrapper executable run: chmod +x ./multiloader/gradlew diff --git a/docs/MULTILOADER.md b/docs/MULTILOADER.md index ad1ad34..ab52c59 100644 --- a/docs/MULTILOADER.md +++ b/docs/MULTILOADER.md @@ -42,7 +42,16 @@ Hard facts gathered while trying to unblock 26.2, so the next person doesn't re- - **Build-unblock ≠ mod port.** Getting a Fabric 26.2 jar to *compile* is separate from porting Nerospace's NeoForge-specific systems (capabilities/transfer, attachments, fluids, networking) to Fabric — that migration (§2) is the real effort and is unchanged by the toolchain choice. -**Recommended migration when ready** (not yet applied — the scaffold still uses architectury-loom): replace the architectury-loom scaffold with the MultiLoader-Template layout above (ModDevGradle `common` on NeoForm + Fabric Loom `fabric` + ModDevGradle `neoforge`). On 26.1.x it works as-is. On 26.2 the `common` (NeoForm `26.2-snapshot-8-1`) and `fabric` (Fabric API `0.152.1+26.2`) cells build immediately; the **`neoforge` cell** is the only one waiting, and you can unblock even that by self-building NeoForge `26.2.x` to `mavenLocal()` rather than waiting for its CI to publish. +**Status: IMPLEMENTED and verified (2026-06-18).** The `multiloader/` scaffold now +uses the MultiLoader-Template layout (ModDevGradle `common` on NeoForm + Fabric +Loom `fabric` + ModDevGradle `neoforge`) — architectury-loom is gone. +`./gradlew :common:build :fabric:build -Pminecraft_version=26.2` is **BUILD +SUCCESSFUL** on this machine: `common` against NeoForm `26.2-1`, `fabric` against +Fabric Loom `1.17.11` + Fabric API `0.152.1+26.2` (no `mappings`). The **`neoforge` +cell** is the only one still pending — it needs the NeoForge 26.2 userdev +(pinned to the `26.2.0.1-beta` Maven default, or self-build `26.2.x` to +`mavenLocal()`); swapping to the official jar when it lands is a one-line version +change. See `multiloader/README.md` for the per-cell status and the self-build step. ### Field-notes sources diff --git a/multiloader/README.md b/multiloader/README.md index 06f8188..8dffadc 100644 --- a/multiloader/README.md +++ b/multiloader/README.md @@ -1,255 +1,113 @@ -# Nerospace multiloader scaffold +# Nerospace multiloader scaffold (MultiLoader-Template) A self-contained skeleton for building Nerospace on **both NeoForge and Fabric** -(loader axis, via **Architectury**) and across **multiple Minecraft versions** -(version axis, via **Stonecutter**). +from one shared codebase, on the **de-obfuscated Minecraft 26.x** toolchain. -> **This does not touch the working build.** The single-loader NeoForge mod at -> the repo root is unchanged and still builds normally. This directory is a -> parallel, reviewable scaffold you promote to the root only when you're ready -> to commit to the migration. See [`docs/MULTILOADER.md`](../docs/MULTILOADER.md) -> for the full subsystem-by-subsystem migration plan. +> **Does not affect the working build.** The single-loader NeoForge mod at the +> repo root is untouched. This is a parallel scaffold you promote to the root +> when ready. Full migration plan: [`docs/MULTILOADER.md`](../docs/MULTILOADER.md). -## What is and isn't here +## Status (verified 2026-06-18) -This is a **skeleton**, not a port. It contains: +Built via the gradle MCP on this machine: -- the Gradle wiring for a 3-module Architectury project (`common` / `fabric` / `neoforge`); -- loader entry points that delegate to a shared `NerospaceCommon.init()`; -- a dependency-free platform abstraction (`platform/Services` + `IPlatformHelper`) - with a Fabric and a NeoForge implementation; -- one example cross-loader registration (`registry/ModRegistries`) proving the path; -- mod metadata for both loaders (`neoforge.mods.toml`, `fabric.mod.json`); -- Stonecutter declared and ready to activate for the version axis. +| Cell | Toolchain | 26.2 | 26.1.2 | +| --- | --- | --- | --- | +| `common` | ModDevGradle (NeoForm) | ✅ builds (`26.2-1`) | NeoForm `26.1.2-1` | +| `fabric` | Fabric Loom `1.17.11` | ✅ **builds** (`fabric-api 0.152.1+26.2`) | confirm API pin | +| `neoforge` | ModDevGradle (NeoForge) | ⏳ needs NeoForge 26.2 userdev (beta on Maven / self-build) | NeoForge `26.1.2.76` | -It does **not** contain the migrated mod. None of the ~200 existing source files -have been moved — that is the actual migration work, sequenced in -[`docs/MULTILOADER.md`](../docs/MULTILOADER.md). +`./gradlew :common:build :fabric:build -Pminecraft_version=26.2` → **BUILD SUCCESSFUL**. -## Layout +## Why this layout (not Architectury) -``` -multiloader/ -├── settings.gradle loader split (active) + Stonecutter (ready) -├── build.gradle Architectury root + shared subproject config -├── gradle.properties all version pins (per-MC-version) -├── stonecutter.gradle version-axis controller (stub until activated) -├── common/ vanilla-only + cross-loader abstractions -│ ├── NerospaceCommon shared entry point -│ ├── platform/Services ServiceLoader resolver -│ ├── platform/IPlatformHelper the loader seam (grow this during migration) -│ └── registry/ModRegistries Architectury DeferredRegister example -├── fabric/ Fabric entry points + platform impl + fabric.mod.json -└── neoforge/ NeoForge @Mod entry + platform impl + neoforge.mods.toml -``` +architectury-loom **cannot consume de-obfuscated Minecraft 26.x mappings** +(issue [#328](https://github.com/architectury/architectury-loom/issues/328), +open, no fix). So this scaffold uses the **MultiLoader-Template** approach — each +module on its loader's *native, de-obf-ready* toolchain: -## ⚠️ Before the first build — pin real versions +- `common` → `net.neoforged.moddev` in **NeoForm-only** mode (de-obfuscated vanilla) +- `fabric` → `net.fabricmc.fabric-loom` (de-obf: **no `mappings` line**) +- `neoforge` → `net.neoforged.moddev` (full NeoForge userdev) -The Fabric and Architectury artifacts in `gradle.properties` are -**placeholders shaped like real coordinates**, not confirmed-resolvable values. -Bleeding-edge Minecraft versions often ship NeoForge first, with Fabric API and -Architectury following later. Confirm and update, for **each** version in -`mc_versions`: +Shared game logic lives **once** in `common`; its source is pulled into `fabric` +and `neoforge` (single copy → no drift). No Architectury API. -| Property | Where to confirm | -| --- | --- | -| `fabric_loader_version`, `fabric_api_version_` | | -| `architectury_loom_version`, `architectury_plugin_version`, `architectury_api_version_` | / [maven.architectury.dev](https://maven.architectury.dev) | -| `neo_version_` | | - -If Fabric/Architectury have **not** ported to your target Minecraft version yet, -the `fabric` and `neoforge` configuration will fail to resolve dependencies. -That is an ecosystem-availability limit, not a scaffold defect — the NeoForge -side of the matrix can proceed independently in the meantime. +## Layout -The plugin versions in `settings.gradle` (Stonecutter) and `build.gradle` -(`architectury-plugin`, `dev.architectury.loom`) are pinned literally because -the Gradle `plugins {}` block can't read `gradle.properties`. Keep them in sync -with the matching `*_version` values. +```text +multiloader/ +├── gradlew(.bat) + gradle/ own wrapper, Gradle 9.5.1 (Loom 1.17 needs >= 9.4) +├── settings.gradle plugin versions + includes common/fabric/neoforge +├── build.gradle shared subproject config (Java 25, repos, token expand) +├── gradle.properties per-version pins (neo_form / neo_version / fabric_api) +├── common/ net.neoforged.moddev (NeoForm) — shared source +│ └── src/main/java/.../NerospaceCommon, platform/{Services,IPlatformHelper}, registry/ +├── fabric/ net.fabricmc.fabric-loom + fabric.mod.json + platform impl +└── neoforge/ net.neoforged.moddev + neoforge.mods.toml + platform impl +``` ## Building -This is a **standalone nested Gradle build with its own wrapper** (Gradle -`9.5.1`). It is *not* driven by the repo-root wrapper: Architectury Loom 1.17.x -needs Gradle ≥ 9.4 (it calls `Configuration.extendsFrom(Provider…)`, added in -9.4.0), while the root stays on 9.2.1 for NeoForge/ModDevGradle. So run it from -inside `multiloader/`: +It's a standalone build with its **own Gradle 9.5.1 wrapper** — run from inside +`multiloader/` (never `../gradlew -p multiloader`, which uses the root's 9.2.1): ```bash cd multiloader -./gradlew build # both loaders, default MC -./gradlew :neoforge:build -./gradlew :fabric:build +./gradlew :common:build :fabric:build -Pminecraft_version=26.2 # verified green +./gradlew :neoforge:build -Pminecraft_version=26.1.2 # NeoForge on 26.1.x -# target a specific Minecraft version (the "configurations"): -./gradlew :neoforge:build -Pminecraft_version=26.1.2 -./gradlew :fabric:build -Pminecraft_version=26.2 +# default version is gradle.properties -> minecraft_version (26.2); +# -Pminecraft_version selects the matching *_ pins. ``` -Output jars land in `multiloader//build/libs/`. The default Minecraft -version is `minecraft_version` in `gradle.properties`; `-Pminecraft_version` -overrides it and selects the matching `*_` dependency pins. - -> Do **not** use `../gradlew -p multiloader` — that runs the root's Gradle 9.2.1 -> and fails with `NoSuchMethodError: Configuration.extendsFrom`. Always use -> `multiloader/gradlew`. - -## The version matrix (26.1 and 26.2) - -The "different configurations" are the cross product of loader × Minecraft -version: - -| | MC 26.1.2 | MC 26.2 | -| --- | --- | --- | -| **NeoForge** | NeoForge `26.1.2.76` — exists | NeoForge `26.2` not published yet | -| **Fabric** | Loader/API exist | Loader `0.19.3` + API `0.152.1+26.2` exist | - -Upstream artifacts now exist for most cells (Fabric shipped 26.2 — see the -official template). **But the loom toolchain blocks the build:** architectury-loom -1.17.x can't handle de-obfuscated Minecraft 26.x mappings yet, so *no* cell -compiles regardless of versions (see Troubleshooting). This is the gating issue, -not artifact availability. - -A build targets one version at a time (one MC version per jar — see -[`docs/MULTILOADER.md`](../docs/MULTILOADER.md) §1). Stonecutter is only needed -once the *source* diverges between 26.1 and 26.2 APIs; until then the -property-driven matrix above is the version mechanism, and Stonecutter stays -declared-and-ready (see "Finishing the version axis"). - -## CI/CD - -`.github/workflows/multiloader.yml` builds the full matrix on pushes/PRs that -touch `multiloader/**` (plus manual `workflow_dispatch`), independent of the -root build (`build.yml`). Each cell runs (with `working-directory: multiloader`, -so it uses the 9.5.1 wrapper) `./gradlew ::build --Pminecraft_version=` and uploads the jars. Cells are `experimental: true` -(continue-on-error) because **all of them are currently blocked at loom's -mappings step** (architectury-loom can't do de-obfuscated 26.x — see -Troubleshooting), so none compile yet. Once architectury-loom ships de-obf 26.x -support, flip a cell's `experimental` to `false` when it builds so regressions -fail the workflow. - -## VS Code - -Run configs live in the repo-root `.vscode/` so they show up alongside the -existing NeoForge ones: - -- **`tasks.json`** — `ML: …` tasks for build / runClient / runServer per loader, - plus `genVsCodeRuns`. They prompt for the Minecraft version (`26.1.2` / `26.2`) - and run `multiloader/gradlew` with `cwd=multiloader` (its 9.5.1 wrapper). This - is the no-setup way to run the configurations. -- **`launch.json`** — a "Multiloader (Architectury)" group of debug launches. - These are templates matching loom's `genVsCodeRuns` output; run the - `ML: Regenerate VS Code run configs` task once to populate the authoritative - classpaths/args. - -## Troubleshooting - -Three walls were found while wiring 26.x support (all verified against the -gradle MCP, 2026-06-18). The first two are fixed; the third is the gating -upstream limitation. None are scaffold bugs. - -**FIXED — `NoSuchMethodError: Configuration.extendsFrom(Provider[])`** (applying -loom in `subprojects`). That Gradle API landed in **Gradle 9.4.0**; -architectury-loom 1.17.x needs it, and the repo root is on 9.2.1 (kept for -NeoForge/ModDevGradle). Fix applied: this build has **its own Gradle 9.5.1 -wrapper**. Always run `multiloader/gradlew` from inside `multiloader/`, never -`../gradlew -p multiloader` (that uses the root's 9.2.1 and fails here). - -**FIXED — stale version pins.** Minecraft 26.2 ("Chaos Cubed", 2026-06-16) *does* -have a Fabric toolchain: Loader `0.19.3`, Fabric API `0.152.1+26.2` (per the -official Fabric template). Those are now pinned in `gradle.properties`. (NeoForge -26.2 still isn't published.) - -**GATING — `Failed to find official mojang mappings` (and friends).** **Minecraft -26.x is de-obfuscated** (Mojang [removed obfuscation](https://www.minecraft.net/en-us/article/removing-obfuscation-in-java-edition) -after 1.21.11; the game ships official names, no proguard file, no Intermediary — -see [Fabric's writeup](https://fabricmc.net/2025/10/31/obfuscation.html)). -Fabric's *own* loom handles this by **omitting the `mappings` line** (the -official template has none). **architectury-loom 1.17.x does not** — every -mappings config fails at `Configure project :common`: - -| `mappings` config | result on architectury-loom 1.17.x | -| --- | --- | -| *(omitted)* | `Configuration 'mappings' has no dependencies` | -| `loom.officialMojangMappings()` | `Failed to find official mojang mappings for 26.x` (no proguard file) | -| `loom.layered {}` (empty) | `NullPointerException: srcNamespace is null` | - -So architectury-loom (the fork that adds NeoForge/Forge support) still assumes an -obfuscated game with a mappings layer, and hasn't yet ported the de-obf handling -Fabric's loom has. **This blocks every cell of the multiloader** — no version pin -or mappings tweak in this scaffold can work around it. Plugin wiring is otherwise -correct (Stonecutter 0.9.2 + Architectury Plugin 3.4.164 + Loom 1.17.483 all -resolve and configure up to this step). - -What this means in practice: - -- **A pure-Fabric 26.2 mod builds today** using Fabric's own loom - (`net.fabricmc.fabric-loom`, omit `mappings`) — but that's single-loader, not - this shared-`common` Architectury setup. -- **NeoForge 26.x builds today** via the repo-root ModDevGradle build (handles - de-obf natively). That's your shippable path now. -- **The Architectury multiloader is blocked** until architectury-loom ships - de-obf 26.x support. When it does, update the `mappings` line in `build.gradle` - per its docs (and bump `architectury_loom_version`), and the cells should - compile. Track: . - -## Finishing the version axis (Stonecutter) - -The loader axis (Architectury) is active. The version axis is **declared but not -wired into the build**, so the skeleton configures cleanly on its own. To build -the full 2×2 matrix (versions × loaders), pick one: - -**A — Stonecraft (recommended, turnkey).** [Stonecraft](https://stonecraft.meza.gg/) -is a settings plugin that wires Stonecutter to Architectury automatically from -the `mc_versions` list. Swap the `dev.kikugie.stonecutter` plugin in -`settings.gradle` for `dev.meza.stonecraft` and follow its quick-start. Least -hand-wiring. - -**B — Hand-wired Stonecutter.** Uncomment the `stonecutter { create(rootProject) }` -block in `settings.gradle`, flesh out `stonecutter.gradle` (the stub shows the -shape), and use version comments in source to absorb signature drift between -Minecraft versions: - -```java -//? if >=26.2 { -/*newSignature(); -*///?} else { -oldSignature(); -//?} +Jars land in `multiloader//build/libs/`. + +## NeoForge on 26.2 (the one pending cell) + +NeoForge's own loader userdev for 26.2 may already be on Maven as a beta +(`neo_version_26.2=26.2.0.1-beta` is pinned — the official MultiLoader-Template's +default). If it doesn't resolve yet, **self-build it** until it publishes: + +```bash +git clone https://github.com/neoforged/NeoForge && cd NeoForge +git checkout 26.2.x +./gradlew :neoforge:publishToMavenLocal # JDK 25; publishes net.neoforged:neoforge: ``` -Either way, keep the version list in `settings.gradle` / `stonecutter.gradle` in -sync with `mc_versions` in `gradle.properties`. +The `neoforge` module already has `mavenLocal()` in its repositories, so set +`neo_version_26.2` to the version it printed and build. When the official jar +lands on Maven, it's a **one-line change** (the version pin) — no refactor. -## The platform seam +## CI / VS Code -Common code must not import `net.neoforged.*` or `net.fabricmc.*`. Where it needs -loader behaviour, it calls `Services.PLATFORM.()`. Each loader module -provides one `IPlatformHelper` implementation, registered through its -`META-INF/services/...IPlatformHelper` file and resolved at runtime by -`ServiceLoader`. Expand `IPlatformHelper` (and add sibling service interfaces) as -you migrate capabilities, networking, config and attachments — this interface is -where every "NeoForge does X, Fabric does Y" decision is funnelled. +- **`.github/workflows/multiloader.yml`** — loader × version matrix; `fabric @ 26.2` + is marked non-experimental (verified), the rest stay `continue-on-error` until + their pins/artifacts are confirmed. +- **Root `.vscode/`** — `ML: …` tasks (build/run per loader, version picker) run + `multiloader/gradlew`. `runClient`/`runServer` come from Fabric Loom (`:fabric`) + and ModDevGradle (`:neoforge`). -## Promoting to the repo root +## Scope boundary -When the migration is far enough along to replace the single-loader build: +This unblocks the **build**. It does not port Nerospace's NeoForge-specific +systems (capabilities/transfer, attachments, fluids, networking) to Fabric — that +migration is the real effort and is tracked in +[`docs/MULTILOADER.md`](../docs/MULTILOADER.md) §2. Shared logic goes in `common`; +loader-specific behaviour goes through the `platform/Services` seam (registration +is per-loader — there is no Architectury API `DeferredRegister` here). -1. Move the existing `src/main/java/...` business logic into `common` (strip - loader imports behind `Services`); keep NeoForge-only code in `neoforge`. -2. Move `multiloader/{settings,build}.gradle` + `gradle.properties` to the repo - root (merging the JEI/datagen/tooling config from the current root build). -3. Repoint the `tools/` generators and `tools/gradle-mcp` at the new module - paths (they're loader-agnostic and otherwise unchanged). -4. Delete this `multiloader/` directory. +## Promoting to the repo root -Until then, the root build remains the source of truth. +When ready to replace the single-loader build: move the existing +`src/main/java/...` logic into `common` (loader-specific bits behind `Services`), +move these build files to the repo root (merging the root's JEI/datagen/tooling +config), repoint `tools/` at the module paths, and retire the root single-loader +build. Until then the root build remains the source of truth. -## References +## Sources -- [`docs/MULTILOADER.md`](../docs/MULTILOADER.md) — full migration plan & subsystem map -- [Architectury docs](https://docs.architectury.dev) · [architectury-loom](https://github.com/architectury/architectury-loom) -- [Stonecutter](https://stonecutter.kikugie.dev/) · [Stonecraft](https://stonecraft.meza.gg/) -- [MultiLoader-Template (illusivesoulworks)](https://github.com/illusivesoulworks/multiloader-template) +- [architectury-loom #328 — no de-obf 26.x](https://github.com/architectury/architectury-loom/issues/328) +- [jaredlll08/MultiLoader-Template](https://github.com/jaredlll08/MultiLoader-Template) · [official Fabric example (de-obf)](https://github.com/FabricMC/fabric-example-mod) +- [NeoForm](https://projects.neoforged.net/neoforged/neoform) · [NeoForge](https://projects.neoforged.net/neoforged/neoforge) · [Fabric develop](https://fabricmc.net/develop) diff --git a/multiloader/build.gradle b/multiloader/build.gradle index a0458f8..4ee1f94 100644 --- a/multiloader/build.gradle +++ b/multiloader/build.gradle @@ -1,25 +1,9 @@ // ===================================================================== -// Nerospace multiloader scaffold — ROOT build -// Loader axis via Architectury. Shared config for all sub-modules. -// -// Plugin versions are pinned here (the plugins {} block cannot read -// gradle.properties). Keep them in sync with the *_version values in -// gradle.properties. +// Nerospace multiloader — ROOT build (MultiLoader-Template style) +// Each module applies its own loader plugin (declared in settings.gradle): +// common -> net.neoforged.moddev (NeoForm) | fabric -> fabric-loom | neoforge -> moddev +// This root only holds config shared by all three. // ===================================================================== -plugins { - id 'architectury-plugin' version '3.4-SNAPSHOT' - // architectury-loom 1.17.x (current line; needs Gradle >= 9.4, hence this - // build's own 9.5.1 wrapper). NOTE: 1.17.x does NOT yet support - // de-obfuscated Minecraft 26.x — see the README "Troubleshooting" for the - // three mappings configs tested, all of which fail. Bump this when - // architectury-loom ships de-obf 26.x support. Keep in sync with - // architectury_loom_version in gradle.properties. - id 'dev.architectury.loom' version '1.17.483' apply false -} - -architectury { - minecraft = project.minecraft_version -} allprojects { group = rootProject.mod_group_id @@ -27,41 +11,9 @@ allprojects { } subprojects { - apply plugin: 'dev.architectury.loom' - apply plugin: 'architectury-plugin' - - base { - // produces e.g. nerospace-fabric-1.0.0-alpha.1.jar - archivesName = "${rootProject.mod_id}-${project.name}" - } - - repositories { - mavenCentral() - maven { url 'https://maven.fabricmc.net/' } - maven { url 'https://maven.architectury.dev/' } - maven { url 'https://maven.neoforged.net/releases/' } - maven { - name 'JEI' - url 'https://maven.blamejared.com/' - } - } - - dependencies { - minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" - // Minecraft 26.x is DE-OBFUSCATED (official names, no proguard mappings / - // Intermediary). Fabric's OWN loom supports this by OMITTING `mappings`, - // but architectury-loom 1.17.x does not yet — every option fails: - // * (omitted) -> "Configuration 'mappings' has no dependencies" - // * loom.officialMojangMappings() -> "Failed to find official mojang mappings" - // * loom.layered {} -> NPE: srcNamespace is null - // Left as the conventional line; this whole module is blocked until - // architectury-loom adds de-obf 26.x support. See README "Troubleshooting". - mappings loom.officialMojangMappings() - } + apply plugin: 'java' java { - withSourcesJar() - // Project targets Java 25 (see root CLAUDE.md). toolchain.languageVersion = JavaLanguageVersion.of(25) } @@ -70,12 +22,19 @@ subprojects { options.release = 25 } + repositories { + mavenCentral() + maven { url 'https://maven.fabricmc.net/' } + maven { url 'https://maven.neoforged.net/releases/' } + maven { name = 'BlameJared'; url = 'https://maven.blamejared.com/' } // JEI etc. (later) + } + + // Expand mod metadata tokens in the loader manifests. Range tracks the + // active MC version unless one is passed explicitly. tasks.withType(ProcessResources).configureEach { - // Range tracks the active version unless one is passed explicitly, - // so a -Pminecraft_version=26.2 build emits "[26.2,)". def mcRange = project.findProperty('minecraft_version_range') ?: "[${rootProject.minecraft_version},)" - def expand = [ + def props = [ mod_id : rootProject.mod_id, mod_name : rootProject.mod_name, mod_version : rootProject.mod_version, @@ -86,9 +45,14 @@ subprojects { minecraft_version_range: mcRange, fabric_loader_version: rootProject.fabric_loader_version, ] - inputs.properties(expand) + inputs.properties(props) filesMatching(['fabric.mod.json', 'META-INF/neoforge.mods.toml', 'pack.mcmeta']) { - expand(expand) + expand(props) } } } + +// Resolve the shared common source directories once, for the loader modules to +// pull in (single copy of the mechanics -> no drift). +ext.commonJava = "${rootProject.projectDir}/common/src/main/java" +ext.commonResources = "${rootProject.projectDir}/common/src/main/resources" diff --git a/multiloader/common/build.gradle b/multiloader/common/build.gradle index 28e9e57..46c4075 100644 --- a/multiloader/common/build.gradle +++ b/multiloader/common/build.gradle @@ -1,23 +1,20 @@ -// common: vanilla-Minecraft-only code + cross-loader abstractions. -// No loader (NeoForge/Fabric) APIs may be referenced here directly — -// reach loader behaviour through the platform Services abstraction. +// common: de-obfuscated vanilla Minecraft via NeoForm (no loader APIs here). +// ModDevGradle in "NeoForm-only" mode (neoFormVersion without a NeoForge version) +// gives official-named vanilla classes to compile shared code against. This +// module's source is also pulled into :fabric and :neoforge (one copy, no drift). -def mc = rootProject.minecraft_version -def architecturyApi = project.findProperty("architectury_api_version_${mc}") - -architectury { - // Mark this as the common module shared by both platforms. - common('fabric', 'neoforge') +plugins { + id 'net.neoforged.moddev' } -dependencies { - // Fabric Loader is pulled in here only so the @Environment annotations - // and mixin tooling are available to shared code; it does NOT make this - // module Fabric-specific. - modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" +def mc = rootProject.minecraft_version + +neoForge { + // De-obfuscated vanilla base; 26.2 NeoForm is published (26.2-1). + neoFormVersion = project.findProperty("neo_form_version_${mc}") - // Architectury API (cross-loader helpers used by common code). - if (architecturyApi != null) { - modApi "dev.architectury:architectury:${architecturyApi}" + def at = file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformers.from(at.absolutePath) } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java index 7baf8f4..1c071e2 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java @@ -3,7 +3,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import za.co.neroland.nerospace.platform.Services; -import za.co.neroland.nerospace.registry.ModRegistries; /** * Loader-agnostic entry point. @@ -32,8 +31,10 @@ public static void init() { Services.PLATFORM.getPlatformName(), Services.PLATFORM.isDevelopmentEnvironment()); - // Cross-loader content registration lives in ModRegistries and is - // realised per-loader by each module's bootstrap (see register()). - ModRegistries.register(); + // Content registration is wired per loader (NeoForge DeferredRegister / + // Fabric Registry.register) from each module's entry point. Without + // Architectury API there is no shared DeferredRegister; the migration + // (docs/MULTILOADER.md §2) introduces a small registration service on + // top of the platform Services seam when content is ported. } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index 282cee5..5078d78 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -1,49 +1,21 @@ package za.co.neroland.nerospace.registry; -import dev.architectury.registry.registries.DeferredRegister; -import dev.architectury.registry.registries.RegistrySupplier; -import net.minecraft.core.registries.Registries; -import net.minecraft.world.item.BlockItem; -import net.minecraft.world.item.Item; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockBehaviour; -import za.co.neroland.nerospace.NerospaceCommon; - /** - * Cross-loader content registration, the Architectury way. + * Placeholder for cross-loader content registration. * - *

{@link DeferredRegister} works identically on Fabric and NeoForge, so - * this single file replaces the root project's per-type NeoForge - * {@code DeferredRegister.Blocks/Items/...} classes. During migration, each - * existing registry class collapses into entries here. + *

The MultiLoader-Template approach (no Architectury API) does not provide a + * shared {@code DeferredRegister}. Registration is performed per loader from the + * loader entry points — NeoForge via {@code DeferredRegister}/{@code Registry} + * events, Fabric via {@code Registry.register} — typically funnelled through a + * small registration service added on top of the platform + * {@link za.co.neroland.nerospace.platform.Services} seam. * - *

The one example below proves the registration path end to end. Note - * that vanilla constructor signatures (e.g. {@code Block} / {@code Item} - * properties needing a registry key) drift between Minecraft versions — that - * is exactly what Stonecutter's version comments handle on the version axis. + *

This class is intentionally empty in the scaffold; it is the seam where the + * migration (see {@code docs/MULTILOADER.md} §2) will introduce that service as + * content is ported off the root project's NeoForge {@code DeferredRegister}s. */ public final class ModRegistries { - public static final DeferredRegister BLOCKS = - DeferredRegister.create(NerospaceCommon.MOD_ID, Registries.BLOCK); - public static final DeferredRegister ITEMS = - DeferredRegister.create(NerospaceCommon.MOD_ID, Registries.ITEM); - - // --- example content ------------------------------------------------- - public static final RegistrySupplier NEROSIUM_BLOCK = - BLOCKS.register("nerosium_block", - () -> new Block(BlockBehaviour.Properties.of().strength(3.0F))); - - public static final RegistrySupplier NEROSIUM_BLOCK_ITEM = - ITEMS.register("nerosium_block", - () -> new BlockItem(NEROSIUM_BLOCK.get(), new Item.Properties())); - private ModRegistries() { } - - /** Realises every DeferredRegister. Called from {@link NerospaceCommon#init()}. */ - public static void register() { - BLOCKS.register(); - ITEMS.register(); - } } diff --git a/multiloader/fabric/build.gradle b/multiloader/fabric/build.gradle index b50d285..d0fa3b4 100644 --- a/multiloader/fabric/build.gradle +++ b/multiloader/fabric/build.gradle @@ -1,46 +1,38 @@ -// fabric: Fabric entry point + platform service implementations. -// Depends on :common and bundles its transformed classes into the jar. +// fabric: Fabric Loom. On de-obfuscated 26.x the game ships official names, so +// there is NO `mappings` line (matches the official Fabric template). Shares the +// common module's source directly (single copy -> no drift). plugins { - id 'com.gradleup.shadow' version '8.3.5' + id 'net.fabricmc.fabric-loom' } def mc = rootProject.minecraft_version def fabricApi = project.findProperty("fabric_api_version_${mc}") -def architecturyApi = project.findProperty("architectury_api_version_${mc}") -architectury { - platformSetupLoomIde() - fabric() -} - -configurations { - common - shadowBundle - compileClasspath.extendsFrom common - runtimeClasspath.extendsFrom common - developmentFabric.extendsFrom common -} +// Pull in the shared common source + resources. +sourceSets.main.java.srcDir rootProject.ext.commonJava +sourceSets.main.resources.srcDir rootProject.ext.commonResources dependencies { - modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" - + minecraft "com.mojang:minecraft:${mc}" + // NOTE: no `mappings` — de-obfuscated 26.x needs none (Fabric Loom 1.17+). + implementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" if (fabricApi != null) { - modApi "net.fabricmc.fabric-api:fabric-api:${fabricApi}" - } - if (architecturyApi != null) { - modApi "dev.architectury:architectury-fabric:${architecturyApi}" + implementation "net.fabricmc.fabric-api:fabric-api:${fabricApi}" } - - common(project(path: ':common', configuration: 'namedElements')) { transitive false } - shadowBundle(project(path: ':common', configuration: 'transformProductionFabric')) -} - -shadowJar { - configurations = [project.configurations.shadowBundle] - archiveClassifier = 'dev-shadow' } -remapJar { - inputFile.set shadowJar.archiveFile +loom { + runs { + client { + client() + ideConfigGenerated true + runDir 'runs/client' + } + server { + server() + ideConfigGenerated true + runDir 'runs/server' + } + } } diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties index ce18583..3eaa3ce 100644 --- a/multiloader/gradle.properties +++ b/multiloader/gradle.properties @@ -1,34 +1,23 @@ # ---------------------------------------------------------------------------- -# Nerospace multiloader scaffold — version variables +# Nerospace multiloader scaffold — MultiLoader-Template (ModDevGradle + Fabric Loom) # -# This is a SELF-CONTAINED scaffold. It does not affect the working -# single-loader build at the repo root. See multiloader/README.md. +# Self-contained; does NOT affect the working single-loader build at the repo +# root. Run from inside multiloader/ with its own Gradle 9.5.1 wrapper. # -# Two axes: -# * Loaders -> Architectury (common / fabric / neoforge sub-modules) -# * Versions -> the mc_versions matrix below. A build targets ONE version, -# selected with -Pminecraft_version= (CI iterates the -# matrix; see .github/workflows/multiloader.yml). Stonecutter -# layers on top once 26.1/26.2 source actually diverges. +# Layout (no architectury-loom — see README "Troubleshooting"): +# common -> net.neoforged.moddev, NeoForm-only (de-obfuscated vanilla) +# fabric -> net.fabricmc.fabric-loom (omits `mappings` for de-obf 26.x) +# neoforge -> net.neoforged.moddev (full NeoForge userdev) +# common's source is shared into fabric/neoforge (one copy -> no drift). # -# UPSTREAM AVAILABILITY (checked 2026-06-17): -# * 26.1 line: NeoForge 26.1.2.76 is REAL. Fabric API/Architectury for -# 26.1.2 are NOT published yet (Fabric's newest stable is 26.1.1) — the -# Fabric column is "ready, pending upstream". -# * 26.2 line: Minecraft 26.2 released 2026-06-16; NO modding toolchain -# (NeoForge, Fabric API, Architectury) has shipped for it yet. The whole -# 26.2 column is "ready, pending upstream". -# CI marks the pending combos continue-on-error so they light up -# automatically once the artifacts below resolve. VERIFY + update every -# value tagged PENDING when upstream publishes: -# https://fabricmc.net/develop https://docs.architectury.dev -# https://projects.neoforged.net/neoforged/neoforge +# Version axis: a build targets ONE Minecraft version, chosen with +# -Pminecraft_version=; the module scripts pick the matching *_ pins. # ---------------------------------------------------------------------------- org.gradle.jvmargs=-Xmx3G org.gradle.daemon=true org.gradle.parallel=true -# Architectury Loom + configuration cache do not always agree; leave OFF here. +# ModDevGradle/Loom do not always agree with the configuration cache; off here. org.gradle.configuration-cache=false ## Mod metadata (shared by both loaders) ------------------------------------- @@ -39,41 +28,27 @@ mod_group_id=za.co.neroland.nerospace mod_license=All Rights Reserved (modpacks allowed - see LICENSE) mod_authors=Neroland -## Active Minecraft version -------------------------------------------------- -# Default target; override per build with -Pminecraft_version=26.2. -# The module build scripts pick the matching *_ pins below, and -# minecraft_version_range is derived from this value (see build.gradle). -minecraft_version=26.1.2 +## Active Minecraft version (override with -Pminecraft_version=26.1.2) -------- +minecraft_version=26.2 -## Version matrix (the "different configurations" CI iterates) --------------- -# Comma-separated Minecraft versions to ship. Keep in sync with the CI matrix -# and (when activated) the Stonecutter version list. +## Version matrix (CI iterates these) --------------------------------------- mc_versions=26.1.2,26.2 -## Per-version dependency pins ---------------------------------------------- -# NeoForge artifact (encodes the MC version: 26.1.2.x is ONLY for MC 26.1.2). +## NeoForm — the de-obfuscated vanilla base the `common` module compiles against +## (https://projects.neoforged.net/neoforged/neoform). NeoForm 26.2 IS published. +neo_form_version_26.1.2=26.1.2-1 +neo_form_version_26.2=26.2-1 + +## NeoForge userdev — the `neoforge` module +## (https://projects.neoforged.net/neoforged/neoforge). neo_version_26.1.2=26.1.2.76 -# PENDING: no NeoForge build for MC 26.2 yet (released 2026-06-16). -neo_version_26.2=26.2.0.1 +# 26.2 beta is on Maven per the official MultiLoader-Template default. If it ever +# fails to resolve, self-build the 26.2.x branch to mavenLocal() (see README) and +# set this to the version it publishes — that is the ONLY change needed. +neo_version_26.2=26.2.0.1-beta -# Fabric Loader (version-agnostic). 0.19.3 per the official 26.2 template. +## Fabric (https://fabricmc.net/develop) ------------------------------------ fabric_loader_version=0.19.3 - -# Fabric API (per MC version). 26.2 confirmed from the official Fabric template. -# VERIFY the 26.1.2 value at https://fabricmc.net/develop (placeholder for now). +# VERIFY the 26.1.2 value at fabricmc.net/develop (placeholder until confirmed). fabric_api_version_26.1.2=0.150.0+26.1.2 fabric_api_version_26.2=0.152.1+26.2 - -## Architectury ------------------------------------------------------------- -# architectury-loom is the Gradle plugin; architectury-api is the runtime lib. -# Loom 1.17.x requires Gradle >= 9.4 (hence this build's own 9.5.1 wrapper). -# KNOWN BLOCKER: architectury-loom 1.17.x does NOT yet support de-obfuscated MC -# 26.x mappings (Fabric's own loom does, but the architectury fork lags). See -# build.gradle dependencies + README "Troubleshooting". The plugin version is -# also pinned in build.gradle's plugins {} block — keep the two synced. -architectury_loom_version=1.17.483 -architectury_plugin_version=3.4-SNAPSHOT -# architectury-api: 21.x is the current line; 21.0.2 is latest seen. VERIFY the -# version that targets each MC release at https://maven.architectury.dev. -architectury_api_version_26.1.2=21.0.2 -architectury_api_version_26.2=21.0.2 diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle index fdce02d..9e6e2af 100644 --- a/multiloader/neoforge/build.gradle +++ b/multiloader/neoforge/build.gradle @@ -1,47 +1,48 @@ -// neoforge: NeoForge entry point + platform service implementations. -// Depends on :common and bundles its transformed classes into the jar. +// neoforge: full NeoForge userdev via ModDevGradle. Shares the common module's +// source directly (single copy -> no drift). +// +// 26.2 note: until NeoForge 26.2 is on the public Maven, self-build the 26.2.x +// branch (`./gradlew :neoforge:publishToMavenLocal`) and set neo_version_26.2 to +// the version it prints — mavenLocal() below resolves it. The official MultiLoader +// -Template ships `26.2.0.1-beta` as a working default, so it may already resolve. plugins { - id 'com.gradleup.shadow' version '8.3.5' + id 'net.neoforged.moddev' } def mc = rootProject.minecraft_version def neoVersion = project.findProperty("neo_version_${mc}") -def architecturyApi = project.findProperty("architectury_api_version_${mc}") -architectury { - platformSetupLoomIde() - neoForge() +repositories { + mavenLocal() // resolves a self-built NeoForge 26.2 until the official jar drops } -configurations { - common - shadowBundle - compileClasspath.extendsFrom common - runtimeClasspath.extendsFrom common - developmentNeoForge.extendsFrom common -} - -dependencies { - neoForge "net.neoforged:neoforge:${neoVersion}" +// Pull in the shared common source + resources. +sourceSets.main.java.srcDir rootProject.ext.commonJava +sourceSets.main.resources.srcDir rootProject.ext.commonResources - common(project(path: ':common', configuration: 'namedElements')) { transitive false } - shadowBundle(project(path: ':common', configuration: 'transformProductionNeoForge')) +neoForge { + version = neoVersion - if (architecturyApi != null) { - modApi "dev.architectury:architectury-neoforge:${architecturyApi}" + def at = project(':common').file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + accessTransformers.from(at.absolutePath) } -} - -processResources { - // mod metadata token expansion is configured in the root build.gradle -} -shadowJar { - configurations = [project.configurations.shadowBundle] - archiveClassifier = 'dev-shadow' -} + runs { + client { + client() + gameDirectory = file('runs/client') + } + server { + server() + gameDirectory = file('runs/server') + } + } -remapJar { - inputFile.set shadowJar.archiveFile + mods { + "${rootProject.mod_id}" { + sourceSet sourceSets.main + } + } } diff --git a/multiloader/settings.gradle b/multiloader/settings.gradle index 035d661..8029ace 100644 --- a/multiloader/settings.gradle +++ b/multiloader/settings.gradle @@ -1,63 +1,28 @@ pluginManagement { repositories { - mavenCentral() gradlePluginPortal() + mavenCentral() maven { url 'https://maven.fabricmc.net/' } // Fabric Loom, Loader, API - maven { url 'https://maven.architectury.dev/' } // Architectury plugin + loom + API - maven { url 'https://maven.neoforged.net/releases/' } // NeoForge - maven { url 'https://maven.minecraftforge.net/' } // (transitive, Loom) - maven { url 'https://maven.kikugie.dev/releases' } // Stonecutter - maven { url 'https://maven.kikugie.dev/snapshots' } - maven { url 'https://maven.parchmentmc.org' } + maven { url 'https://maven.neoforged.net/releases/' } // NeoForge / NeoForm / ModDevGradle + } + // Plugin versions are declared here so each module applies them by id only. + plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' + id 'net.neoforged.moddev' version '2.0.141' // common (NeoForm) + neoforge + id 'net.fabricmc.fabric-loom' version '1.17-SNAPSHOT' // fabric — 1.17 handles de-obf 26.x (omit mappings) } } plugins { - id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0' - // Stonecutter: the multi-version manager. Available to the whole build. - // The active version axis is configured in the `stonecutter { }` block - // below — see the activation notes there and in README.md. - id 'dev.kikugie.stonecutter' version '0.9.2' + id 'org.gradle.toolchains.foojay-resolver-convention' } rootProject.name = 'nerospace-multiloader' -// ===================================================================== -// LOADER AXIS — Architectury multiloader split (ACTIVE) -// --------------------------------------------------------------------- -// `common` : vanilla-Minecraft-only code + cross-loader abstractions. -// `fabric` : Fabric entry point + platform service implementations. -// `neoforge` : NeoForge entry point + platform service implementations. -// A normal build of this scaffold produces one fabric jar and one -// neoforge jar for the ACTIVE Minecraft version (gradle.properties -> -// minecraft_version). -// ===================================================================== +// MultiLoader-Template layout (no architectury-loom — it can't do de-obf 26.x): +// common -> net.neoforged.moddev (NeoForm) : de-obfuscated vanilla, shared source +// fabric -> net.fabricmc.fabric-loom : de-obf-ready, omits `mappings` +// neoforge -> net.neoforged.moddev : full NeoForge userdev include 'common' include 'fabric' include 'neoforge' - -// ===================================================================== -// VERSION AXIS — Stonecutter (SCAFFOLDED / READY TO ACTIVATE) -// --------------------------------------------------------------------- -// Combining the Architectury module split (above) with Stonecutter's -// version nodes is the "Stonecraft" pattern. Two supported ways to -// finish wiring the 2x2 (versions x loaders) matrix — pick one, see -// README.md "Finishing the version axis": -// -// A) Stonecraft (turnkey): replace this block + the plugin above with -// the `dev.meza.stonecraft` settings plugin, which wires Stonecutter -// to Architectury automatically from the `mc_versions` property. -// -// B) Hand-wired Stonecutter: enable the block below. It declares one -// node per Minecraft version; each node rebuilds the common/fabric/ -// neoforge set against that version's dependency pins. -// -// Left commented so the Architectury skeleton configures cleanly on its -// own. Uncomment (and choose path A or B) when you adopt the version axis. -// -// stonecutter { -// create(rootProject) { -// versions '26.1.2', '26.2' // keep in sync with gradle.properties mc_versions -// vcsVersion = '26.1.2' // the version your source is authored against -// } -// } From c7cb2f7cd29ef3a46997af9c3215e28ef82110fd Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:13:19 +0800 Subject: [PATCH 07/82] Update multiloader CI, VSCode tasks & NeoForge Refactor multiloader CI/workflow and developer tasks for the nested multiloader build. - Harden multiloader GitHub Actions: remove continue-on-error, clarify matrix cells as verified or pending, and add strict failure/docs in comments. - Update editor integration: add nerospace.code-workspace to import the multiloader build; neutralize multiloader/.vscode/launch.json; update root .vscode/launch.json to include build/resources/main in -Dfml.modFolders so processed resources are found. - Overhaul .vscode/tasks.json with explicit ML run/build tasks (fabric/neoforge), use :fabric:vscode to generate Fabric run configs, add runServer/runClient targets, and set default MC to 26.2. - Add ignores for runs/ and multiloader/fabric/runs in .gitignore. - Java API adjustments for NeoForge: use FMLEnvironment.isProduction() and FMLEnvironment.getDist() to match 26.1.x API. - Small resource tidy: normalize hyphenation in fabric.mod.json and neoforge.mods.toml and remove the 'architectury' suggestion from fabric.mod.json. These changes make CI expectations explicit, provide convenient run/build tasks for the multiloader, and update code to match the newer FMLEnvironment API. --- .github/workflows/multiloader.yml | 35 ++++----- .gitignore | 2 + .vscode/launch.json | 74 ++----------------- .vscode/tasks.json | 72 ++++++++++++------ multiloader/.vscode/launch.json | 5 ++ .../fabric/src/main/resources/fabric.mod.json | 5 +- .../platform/NeoForgePlatformHelper.java | 6 +- .../resources/META-INF/neoforge.mods.toml | 2 +- nerospace.code-workspace | 10 +++ 9 files changed, 93 insertions(+), 118 deletions(-) create mode 100644 multiloader/.vscode/launch.json create mode 100644 nerospace.code-workspace diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml index 6f29b8a..5c5f83e 100644 --- a/.github/workflows/multiloader.yml +++ b/.github/workflows/multiloader.yml @@ -1,14 +1,14 @@ name: Multiloader Build -# Builds the multiloader scaffold (multiloader/) across the loader x Minecraft -# version matrix. Independent of the root single-loader build (build.yml). +# Builds the multiloader scaffold (multiloader/) — MultiLoader-Template layout +# (ModDevGradle common+neoforge, Fabric Loom fabric; no architectury-loom). +# Independent of the root single-loader build (build.yml). # -# Layout: MultiLoader-Template (ModDevGradle common+neoforge, Fabric Loom fabric) -# — no architectury-loom (it can't do de-obf 26.x). fabric @ 26.2 is verified -# green (Fabric Loom 1.17 + fabric-api 0.152.1+26.2, NeoForm 26.2-1). The -# remaining combos stay `experimental: true` until their pins are confirmed / -# NeoForge 26.2 userdev resolves (beta on Maven, or self-built to mavenLocal — -# see multiloader/README.md). Flip a combo's `experimental` to false once green. +# STRICT: there is no continue-on-error, so ANY matrix cell that fails to build +# fails the whole workflow. The matrix therefore lists ONLY cells that are +# expected to build on a clean runner (both verified green via the gradle MCP on +# 2026-06-18). Cells that can't build on CI yet are listed (commented) below with +# the reason — re-add them here once they're unblocked. on: push: @@ -28,23 +28,18 @@ jobs: build: name: ${{ matrix.loader }} @ MC ${{ matrix.mc }} runs-on: ubuntu-latest - continue-on-error: ${{ matrix.experimental }} + # No continue-on-error: any failed cell fails the workflow. strategy: - fail-fast: false + fail-fast: false # still run every cell so one failure doesn't mask others matrix: include: - loader: neoforge - mc: "26.1.2" - experimental: true # ModDevGradle + NeoForge 26.1.2.76 (verify in CI, then flip) + mc: "26.1.2" # ModDevGradle + NeoForge 26.1.2.76 — verified green - loader: fabric - mc: "26.1.2" - experimental: true # confirm fabric_api_version_26.1.2 at fabricmc.net/develop - - loader: neoforge - mc: "26.2" - experimental: true # needs NeoForge 26.2 userdev (beta on Maven or self-built) - - loader: fabric - mc: "26.2" - experimental: false # VERIFIED green 2026-06-18 (Fabric Loom 1.17 + fabric-api 0.152.1+26.2) + mc: "26.2" # Fabric Loom 1.17 + fabric-api 0.152.1+26.2 (de-obf, no mappings) — verified green + # PENDING — not buildable on a clean CI runner; re-add when unblocked: + # - { loader: fabric, mc: "26.1.2" } # Fabric never shipped MC 26.1.2 (26.1 -> 26.1.1 -> 26.2) + # - { loader: neoforge, mc: "26.2" } # needs NeoForge 26.2 userdev on Maven (or self-built to mavenLocal — see multiloader/README.md) steps: - name: Checkout repository diff --git a/.gitignore b/.gitignore index 0f5f083..cba881b 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ repo/ !**/src/**/repo/ PROJECT_PLAN.md /tools/__pycache__ +runs/ +/multiloader/fabric/runs \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 1aea792..6143e61 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,7 @@ { - "//": "Generic, committable run configs for Nerospace. Paths use ${workspaceFolder} so they work on any machine, and each config\u0027s preLaunchTask (see tasks.json) regenerates build/moddev/\u003crun\u003eArgs.txt before launch so it never fails after a clean. ModDevGradle no longer overwrites this file: build.gradle sets disableIdeRun() on every run, so this file is the source of truth.", + "//": "Run/debug configs for the ROOT single-loader NeoForge build (project \u0027nerospace\u0027). modFolders points at BOTH the IDE class output (bin/main) AND build/resources/main, because the generated neoforge.mods.toml lives in build/resources/main/META-INF (Gradle builds it from src/main/templates; the Java builder does NOT copy it into bin/main). Without the resources dir, FML reports \u0027bin/main is not a valid mod file\u0027. Each preLaunchTask (tasks.json) runs the matching ModDevGradle prepare* task, which regenerates the argfiles and the processed resources.", "version": "0.2.0", - "//classPaths": "Each config launches from build/moddev/devlaunchClasspath.jar, a gradle-generated pathing jar whose MANIFEST Class-Path lists the exact NeoForge/Minecraft/DevLaunch dev jars for the version in gradle.properties (built by the devLaunchPathingJar task, which finalizes every prepare*Run preLaunchTask). With no $Auto entry, classPaths REPLACES the IDE-resolved runtime classpath, so a neo_version bump can never again launch a stale NeoForge and crash FML with \u0027version is null\u0027. projectName is kept only for source/breakpoint mapping; the mod loads via -Dfml.modFolders, not the classpath. Verified end-to-end: launching with only this jar boots GameTestServer and passes all gametests.", + "//multiloader": "The multiloader/ build is a SEPARATE nested Gradle build, so VS Code\u0027s Java extension (which imports the repo root) does NOT know its :fabric/:neoforge projects. To RUN multiloader on either MC version use the \u0027ML: Run ...\u0027 tasks (tasks.json). To DEBUG with breakpoints, open nerospace.code-workspace, run \u0027ML: Generate Fabric VS Code run configs\u0027, and use the entries Loom writes into multiloader/.vscode/launch.json.", "configurations": [ { "type": "java", @@ -22,7 +22,7 @@ ], "vmArgs": [ "@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" ], "cwd": "${workspaceFolder}/run", "env": {}, @@ -48,7 +48,7 @@ ], "vmArgs": [ "@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" ], "cwd": "${workspaceFolder}/run", "env": {}, @@ -74,7 +74,7 @@ ], "vmArgs": [ "@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" ], "cwd": "${workspaceFolder}/run", "env": {}, @@ -100,74 +100,12 @@ ], "vmArgs": [ "@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" ], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", "shortenCommandLine": "none" - }, - { - "//": "\u003d\u003d\u003d\u003d\u003d Multiloader scaffold (multiloader/) — Architectury, loom-based \u003d\u003d\u003d\u003d\u003d" - }, - { - "//": "These are TEMPLATES shaped like loom\u0027s `genVsCodeRuns` output. Loom owns the exact classpath/vmArgs, so run the \u0027ML: Regenerate VS Code run configs (genVsCodeRuns)\u0027 task once (it also requires the 26.x toolchain to resolve) to materialize the authoritative entries. For a no-setup run, use the \u0027ML: Run ...\u0027 tasks in tasks.json instead. The MC version of these java launches follows whatever was last built; switch versions via the tasks (-Pminecraft_version)." - }, - { - "type": "java", - "request": "launch", - "name": "ML: NeoForge Client", - "presentation": { - "group": "Multiloader (Architectury)", - "order": 0 - }, - "projectName": "neoforge", - "mainClass": "net.neoforged.devlaunch.Main", - "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", - "cwd": "${workspaceFolder}/multiloader/neoforge/run", - "console": "internalConsole" - }, - { - "type": "java", - "request": "launch", - "name": "ML: NeoForge Server", - "presentation": { - "group": "Multiloader (Architectury)", - "order": 1 - }, - "projectName": "neoforge", - "mainClass": "net.neoforged.devlaunch.Main", - "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", - "cwd": "${workspaceFolder}/multiloader/neoforge/run", - "console": "internalConsole" - }, - { - "type": "java", - "request": "launch", - "name": "ML: Fabric Client", - "presentation": { - "group": "Multiloader (Architectury)", - "order": 2 - }, - "projectName": "fabric", - "mainClass": "net.fabricmc.devlaunchinjector.Main", - "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", - "cwd": "${workspaceFolder}/multiloader/fabric/run", - "console": "internalConsole" - }, - { - "type": "java", - "request": "launch", - "name": "ML: Fabric Server", - "presentation": { - "group": "Multiloader (Architectury)", - "order": 3 - }, - "projectName": "fabric", - "mainClass": "net.fabricmc.devlaunchinjector.Main", - "preLaunchTask": "ML: Regenerate VS Code run configs (genVsCodeRuns)", - "cwd": "${workspaceFolder}/multiloader/fabric/run", - "console": "internalConsole" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 42a1da4..5ce6efb 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,5 +1,5 @@ { - "//": "preLaunchTasks for launch.json regenerate build/moddev/Args.txt before launch. Cross-platform: gradlew on Linux/macOS, gradlew.bat on Windows.", + "//": "Root single-loader prepareRun tasks (preLaunchTasks for the root launch.json) + Multiloader (multiloader/) build/run tasks. The multiloader tasks shell out to multiloader's OWN wrapper (Gradle 9.5.1) with cwd=multiloader; they do NOT need the multiloader Gradle build to be imported by the Java extension, so they work from the repo-root window.", "version": "2.0.0", "tasks": [ { @@ -38,38 +38,46 @@ "presentation": { "reveal": "silent", "panel": "shared", "clear": false }, "problemMatcher": [] }, - { "//": "Multiloader scaffold: run multiloader's OWN wrapper (Gradle 9.5.1; loom 1.17 needs >=9.4) with cwd=multiloader. Pick MC via mlMcVersion input." }, + + { "//": "===== Multiloader: RUN both versions. Terminal -> Run Task. These launch the game directly via Gradle (no debugger). For breakpoint debugging, open nerospace.code-workspace (imports the multiloader build) and use its generated Run & Debug entries. =====" }, { - "label": "ML: Build both loaders (pick MC)", + "label": "ML: Run Fabric Client - 26.2", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": ["build", "-Pminecraft_version=${input:mlMcVersion}"], - "group": "build", - "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "args": [":fabric:runClient", "-Pminecraft_version=26.2"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { - "label": "ML: Build NeoForge (pick MC)", + "label": "ML: Run NeoForge Client - 26.1.2", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": [":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}"], - "group": "build", - "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "args": [":neoforge:runClient", "-Pminecraft_version=26.1.2"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { - "label": "ML: Build Fabric (pick MC)", + "label": "ML: Run Fabric Client (pick MC)", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": [":fabric:build", "-Pminecraft_version=${input:mlMcVersion}"], - "group": "build", - "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "args": [":fabric:runClient", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Run Fabric Server (pick MC)", + "type": "process", + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:runServer", "-Pminecraft_version=${input:mlMcVersion}"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, { @@ -92,33 +100,49 @@ "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, "problemMatcher": [] }, + + { "//": "===== Multiloader: BUILD =====" }, { - "label": "ML: Run Fabric Client (pick MC)", + "label": "ML: Build both loaders (pick MC)", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": [":fabric:runClient", "-Pminecraft_version=${input:mlMcVersion}"], - "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "args": ["build", "-Pminecraft_version=${input:mlMcVersion}"], + "group": "build", + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] }, { - "label": "ML: Run Fabric Server (pick MC)", + "label": "ML: Build NeoForge (pick MC)", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": [":fabric:runServer", "-Pminecraft_version=${input:mlMcVersion}"], - "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "args": [":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}"], + "group": "build", + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, + "problemMatcher": [] + }, + { + "label": "ML: Build Fabric (pick MC)", + "type": "process", + "command": "${workspaceFolder}/multiloader/gradlew", + "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:build", "-Pminecraft_version=${input:mlMcVersion}"], + "group": "build", + "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] }, { - "label": "ML: Regenerate VS Code run configs (genVsCodeRuns)", + "label": "ML: Generate Fabric VS Code run configs (pick MC)", + "//": "Loom's real task is 'vscode' (not genVsCodeRuns). Writes multiloader/.vscode/launch.json; use it from nerospace.code-workspace.", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": ["genVsCodeRuns", "-Pminecraft_version=${input:mlMcVersion}"], + "args": [":fabric:vscode", "-Pminecraft_version=${input:mlMcVersion}"], "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] }, @@ -128,7 +152,7 @@ "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, "options": { "cwd": "${workspaceFolder}/multiloader" }, - "args": [":neoforge:build", "-Pminecraft_version=${input:mlMcVersion}", "--refresh-dependencies"], + "args": [":fabric:build", "-Pminecraft_version=${input:mlMcVersion}", "--refresh-dependencies"], "presentation": { "reveal": "always", "panel": "shared", "clear": true }, "problemMatcher": [] } @@ -139,7 +163,7 @@ "type": "pickString", "description": "Minecraft version for the multiloader build", "options": ["26.1.2", "26.2"], - "default": "26.1.2" + "default": "26.2" } ] } diff --git a/multiloader/.vscode/launch.json b/multiloader/.vscode/launch.json new file mode 100644 index 0000000..3f5f940 --- /dev/null +++ b/multiloader/.vscode/launch.json @@ -0,0 +1,5 @@ +{ + "//": "NEUTRALIZED ON PURPOSE. Loom/ModDevGradle's generated Run & Debug entries point at bin/main (VS Code's RAW Java output, where ${mod_id}/${mod_version} are NOT expanded), which breaks token-templated manifests. Do NOT use Run & Debug for the multiloader. RUN via the 'ML: Run Fabric/NeoForge Client - ' tasks (repo-root .vscode/tasks.json -> Terminal: Run Task) — those use Gradle's processed build/resources and are verified to load 'nerospace 1.0.0-alpha.1'. This file is gitignored; if 'gradlew :fabric:vscode' regenerates the bin/main entries, empty configurations again.", + "version": "0.2.0", + "configurations": [] +} diff --git a/multiloader/fabric/src/main/resources/fabric.mod.json b/multiloader/fabric/src/main/resources/fabric.mod.json index 3528f37..c663a0a 100644 --- a/multiloader/fabric/src/main/resources/fabric.mod.json +++ b/multiloader/fabric/src/main/resources/fabric.mod.json @@ -3,7 +3,7 @@ "id": "${mod_id}", "version": "${mod_version}", "name": "${mod_name}", - "description": "Nerospace — multiloader build (Fabric).", + "description": "Nerospace - multiloader build (Fabric).", "authors": ["${mod_authors}"], "license": "${mod_license}", "environment": "*", @@ -21,7 +21,6 @@ "java": ">=21" }, "suggests": { - "fabric-api": "*", - "architectury": "*" + "fabric-api": "*" } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java index 8ec1137..fc069f5 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -17,7 +17,9 @@ public String getPlatformName() { @Override public boolean isDevelopmentEnvironment() { - return !FMLEnvironment.production; + // 26.1.x exposes these as methods (the old `FMLEnvironment.production` + // / `.dist` fields were removed) — matches the root project's usage. + return !FMLEnvironment.isProduction(); } @Override @@ -27,6 +29,6 @@ public boolean isModLoaded(String modId) { @Override public boolean isClient() { - return FMLEnvironment.dist == Dist.CLIENT; + return FMLEnvironment.getDist() == Dist.CLIENT; } } diff --git a/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml index a3300f4..5cec1e2 100644 --- a/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -8,7 +8,7 @@ version = "${mod_version}" displayName = "${mod_name}" authors = "${mod_authors}" description = ''' -Nerospace — multiloader build (NeoForge). +Nerospace - multiloader build (NeoForge). ''' [[mixins]] diff --git a/nerospace.code-workspace b/nerospace.code-workspace new file mode 100644 index 0000000..d497938 --- /dev/null +++ b/nerospace.code-workspace @@ -0,0 +1,10 @@ +{ + "//": "Multi-root workspace. Adds multiloader/ as a second folder so VS Code's Gradle/Java extension imports the nested multiloader build — only then can its :fabric/:neoforge runs appear in Run & Debug. Open this file (File > Open Workspace from File) instead of the plain folder when you want to debug the multiloader. Generate the Fabric run config first with the 'ML: Generate Fabric VS Code run configs' task; it writes multiloader/.vscode/launch.json.", + "folders": [ + { "name": "nerospace (root, NeoForge)", "path": "." }, + { "name": "multiloader (Fabric + NeoForge)", "path": "multiloader" } + ], + "settings": { + "java.import.gradle.enabled": true + } +} From f0d9f0c41743fe40af7e66c0334844b1283331db Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:32:35 +0800 Subject: [PATCH 08/82] Generate mod metadata & add VS Code configs Add committed, relative VS Code run/debug configs and tasks for the multiloader and update the root launch config. Move loader manifest templates from src/main/resources to src/main/templates and add generateModMetadata ProcessResources tasks (fabric & neoforge) wired into sourceSets/IDE sync so manifests are expanded into a generated resources dir. Remove the previous global resources expansion in multiloader/build.gradle since each module now handles its own metadata generation. These changes prevent the IDE from copying raw tokenized manifests into bin/main, ensure Run & Debug picks up expanded manifests, and provide preLaunch tasks to regenerate per-machine launch args. --- .vscode/launch.json | 77 ++------ multiloader/.vscode/launch.json | 171 +++++++++++++++++- multiloader/.vscode/tasks.json | 12 ++ multiloader/build.gradle | 26 +-- multiloader/fabric/build.gradle | 29 ++- .../{resources => templates}/fabric.mod.json | 0 multiloader/neoforge/build.gradle | 25 ++- .../META-INF/neoforge.mods.toml | 0 nerospace.code-workspace | 6 +- 9 files changed, 256 insertions(+), 90 deletions(-) create mode 100644 multiloader/.vscode/tasks.json rename multiloader/fabric/src/main/{resources => templates}/fabric.mod.json (100%) rename multiloader/neoforge/src/main/{resources => templates}/META-INF/neoforge.mods.toml (100%) diff --git a/.vscode/launch.json b/.vscode/launch.json index 6143e61..2dad935 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,29 +1,18 @@ { - "//": "Run/debug configs for the ROOT single-loader NeoForge build (project \u0027nerospace\u0027). modFolders points at BOTH the IDE class output (bin/main) AND build/resources/main, because the generated neoforge.mods.toml lives in build/resources/main/META-INF (Gradle builds it from src/main/templates; the Java builder does NOT copy it into bin/main). Without the resources dir, FML reports \u0027bin/main is not a valid mod file\u0027. Each preLaunchTask (tasks.json) runs the matching ModDevGradle prepare* task, which regenerates the argfiles and the processed resources.", + "//": "Run/debug configs for the ROOT single-loader NeoForge build (project 'nerospace'). modFolders points at bin/main (IDE classes) AND build/resources/main (where the generated neoforge.mods.toml lives). Multiloader run/debug lives in multiloader/.vscode + nerospace.code-workspace.", "version": "0.2.0", - "//multiloader": "The multiloader/ build is a SEPARATE nested Gradle build, so VS Code\u0027s Java extension (which imports the repo root) does NOT know its :fabric/:neoforge projects. To RUN multiloader on either MC version use the \u0027ML: Run ...\u0027 tasks (tasks.json). To DEBUG with breakpoints, open nerospace.code-workspace, run \u0027ML: Generate Fabric VS Code run configs\u0027, and use the entries Loom writes into multiloader/.vscode/launch.json.", "configurations": [ { "type": "java", "request": "launch", "name": "Client", - "presentation": { - "group": "Mod Development - nerospace", - "order": 0 - }, + "presentation": { "group": "Mod Development - nerospace", "order": 0 }, "projectName": "nerospace", - "classPaths": [ - "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" - ], + "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareClientRun", - "args": [ - "@${workspaceFolder}/build/moddev/clientRunProgramArgs.txt" - ], - "vmArgs": [ - "@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" - ], + "args": ["@${workspaceFolder}/build/moddev/clientRunProgramArgs.txt"], + "vmArgs": ["@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -33,23 +22,13 @@ "type": "java", "request": "launch", "name": "Data", - "presentation": { - "group": "Mod Development - nerospace", - "order": 1 - }, + "presentation": { "group": "Mod Development - nerospace", "order": 1 }, "projectName": "nerospace", - "classPaths": [ - "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" - ], + "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareDataRun", - "args": [ - "@${workspaceFolder}/build/moddev/dataRunProgramArgs.txt" - ], - "vmArgs": [ - "@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" - ], + "args": ["@${workspaceFolder}/build/moddev/dataRunProgramArgs.txt"], + "vmArgs": ["@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -59,23 +38,13 @@ "type": "java", "request": "launch", "name": "GameTestServer", - "presentation": { - "group": "Mod Development - nerospace", - "order": 2 - }, + "presentation": { "group": "Mod Development - nerospace", "order": 2 }, "projectName": "nerospace", - "classPaths": [ - "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" - ], + "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareGameTestServerRun", - "args": [ - "@${workspaceFolder}/build/moddev/gameTestServerRunProgramArgs.txt" - ], - "vmArgs": [ - "@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" - ], + "args": ["@${workspaceFolder}/build/moddev/gameTestServerRunProgramArgs.txt"], + "vmArgs": ["@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -85,27 +54,17 @@ "type": "java", "request": "launch", "name": "Server", - "presentation": { - "group": "Mod Development - nerospace", - "order": 3 - }, + "presentation": { "group": "Mod Development - nerospace", "order": 3 }, "projectName": "nerospace", - "classPaths": [ - "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" - ], + "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareServerRun", - "args": [ - "@${workspaceFolder}/build/moddev/serverRunProgramArgs.txt" - ], - "vmArgs": [ - "@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main" - ], + "args": ["@${workspaceFolder}/build/moddev/serverRunProgramArgs.txt"], + "vmArgs": ["@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", "shortenCommandLine": "none" } ] -} \ No newline at end of file +} diff --git a/multiloader/.vscode/launch.json b/multiloader/.vscode/launch.json index 3f5f940..3ac4dbc 100644 --- a/multiloader/.vscode/launch.json +++ b/multiloader/.vscode/launch.json @@ -1,5 +1,170 @@ { - "//": "NEUTRALIZED ON PURPOSE. Loom/ModDevGradle's generated Run & Debug entries point at bin/main (VS Code's RAW Java output, where ${mod_id}/${mod_version} are NOT expanded), which breaks token-templated manifests. Do NOT use Run & Debug for the multiloader. RUN via the 'ML: Run Fabric/NeoForge Client - ' tasks (repo-root .vscode/tasks.json -> Terminal: Run Task) — those use Gradle's processed build/resources and are verified to load 'nerospace 1.0.0-alpha.1'. This file is gitignored; if 'gradlew :fabric:vscode' regenerates the bin/main entries, empty configurations again.", + "//": "Committed RELATIVE multiloader run/debug configs (no machine paths). Open nerospace.code-workspace, then pick a config in Run \u0026 Debug. preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync. Today Fabric@26.2 and NeoForge@26.1.2 run; Fabric@26.1.2 (Fabric never shipped MC 26.1.2) and NeoForge@26.2 (needs userdev) will fail in preLaunch until upstream ships.", "version": "0.2.0", - "configurations": [] -} + "configurations": [ + { + "type": "java", + "request": "launch", + "name": "Fabric Client (26.2)", + "cwd": "${workspaceFolder}/fabric/runs/client", + "console": "integratedTerminal", + "mainClass": "net.fabricmc.devlaunchinjector.Main", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "args": "", + "projectName": "fabric", + "preLaunchTask": "ml-prepare-fabric-26.2" + }, + { + "type": "java", + "request": "launch", + "name": "Fabric Client (26.1.2)", + "cwd": "${workspaceFolder}/fabric/runs/client", + "console": "integratedTerminal", + "mainClass": "net.fabricmc.devlaunchinjector.Main", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "args": "", + "projectName": "fabric", + "preLaunchTask": "ml-prepare-fabric-26.1.2" + }, + { + "type": "java", + "request": "launch", + "name": "Fabric Server (26.2)", + "cwd": "${workspaceFolder}/fabric/runs/server", + "console": "integratedTerminal", + "mainClass": "net.fabricmc.devlaunchinjector.Main", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "args": "nogui", + "projectName": "fabric", + "preLaunchTask": "ml-prepare-fabric-26.2" + }, + { + "type": "java", + "request": "launch", + "name": "Fabric Server (26.1.2)", + "cwd": "${workspaceFolder}/fabric/runs/server", + "console": "integratedTerminal", + "mainClass": "net.fabricmc.devlaunchinjector.Main", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "args": "nogui", + "projectName": "fabric", + "preLaunchTask": "ml-prepare-fabric-26.1.2" + }, + { + "type": "java", + "request": "launch", + "name": "NeoForge Client (26.1.2)", + "cwd": "${workspaceFolder}/neoforge/runs/client", + "console": "internalConsole", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@${workspaceFolder}/neoforge/build/moddev/clientRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/neoforge/build/moddev/clientRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + ], + "projectName": "neoforge", + "preLaunchTask": "ml-prepare-neoforge-client-26.1.2", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "NeoForge Client (26.2)", + "cwd": "${workspaceFolder}/neoforge/runs/client", + "console": "internalConsole", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@${workspaceFolder}/neoforge/build/moddev/clientRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/neoforge/build/moddev/clientRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + ], + "projectName": "neoforge", + "preLaunchTask": "ml-prepare-neoforge-client-26.2", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "NeoForge Server (26.1.2)", + "cwd": "${workspaceFolder}/neoforge/runs/server", + "console": "internalConsole", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@${workspaceFolder}/neoforge/build/moddev/serverRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/neoforge/build/moddev/serverRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + ], + "projectName": "neoforge", + "preLaunchTask": "ml-prepare-neoforge-server-26.1.2", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "NeoForge Server (26.2)", + "cwd": "${workspaceFolder}/neoforge/runs/server", + "console": "internalConsole", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@${workspaceFolder}/neoforge/build/moddev/serverRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/neoforge/build/moddev/serverRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + ], + "projectName": "neoforge", + "preLaunchTask": "ml-prepare-neoforge-server-26.2", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "neoforge - Client", + "presentation": { + "group": "Mod Development - neoforge", + "order": 0 + }, + "projectName": "neoforge", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" + ], + "cwd": "${workspaceFolder}\\neoforge\\runs\\client", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "neoforge - Server", + "presentation": { + "group": "Mod Development - neoforge", + "order": 1 + }, + "projectName": "neoforge", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" + ], + "cwd": "${workspaceFolder}\\neoforge\\runs\\server", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + } + ] +} \ No newline at end of file diff --git a/multiloader/.vscode/tasks.json b/multiloader/.vscode/tasks.json new file mode 100644 index 0000000..01c376d --- /dev/null +++ b/multiloader/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "//": "preLaunchTasks for this folder's launch.json. Each runs multiloader's own Gradle wrapper (cwd = this folder) to regenerate per-machine launch args / loom launch.cfg for the chosen Minecraft version. Relative paths only.", + "version": "2.0.0", + "tasks": [ + { "label": "ml-prepare-fabric-26.2", "type": "process", "command": "${workspaceFolder}/gradlew", "windows": { "command": "${workspaceFolder}/gradlew.bat" }, "args": [":fabric:configureLaunch", "-Pminecraft_version=26.2"], "presentation": { "reveal": "silent", "panel": "shared" }, "problemMatcher": [] }, + { "label": "ml-prepare-fabric-26.1.2", "type": "process", "command": "${workspaceFolder}/gradlew", "windows": { "command": "${workspaceFolder}/gradlew.bat" }, "args": [":fabric:configureLaunch", "-Pminecraft_version=26.1.2"], "presentation": { "reveal": "silent", "panel": "shared" }, "problemMatcher": [] }, + { "label": "ml-prepare-neoforge-client-26.1.2", "type": "process", "command": "${workspaceFolder}/gradlew", "windows": { "command": "${workspaceFolder}/gradlew.bat" }, "args": [":neoforge:prepareClientRun", "-Pminecraft_version=26.1.2"], "presentation": { "reveal": "silent", "panel": "shared" }, "problemMatcher": [] }, + { "label": "ml-prepare-neoforge-server-26.1.2", "type": "process", "command": "${workspaceFolder}/gradlew", "windows": { "command": "${workspaceFolder}/gradlew.bat" }, "args": [":neoforge:prepareServerRun", "-Pminecraft_version=26.1.2"], "presentation": { "reveal": "silent", "panel": "shared" }, "problemMatcher": [] }, + { "label": "ml-prepare-neoforge-client-26.2", "type": "process", "command": "${workspaceFolder}/gradlew", "windows": { "command": "${workspaceFolder}/gradlew.bat" }, "args": [":neoforge:prepareClientRun", "-Pminecraft_version=26.2"], "presentation": { "reveal": "silent", "panel": "shared" }, "problemMatcher": [] }, + { "label": "ml-prepare-neoforge-server-26.2", "type": "process", "command": "${workspaceFolder}/gradlew", "windows": { "command": "${workspaceFolder}/gradlew.bat" }, "args": [":neoforge:prepareServerRun", "-Pminecraft_version=26.2"], "presentation": { "reveal": "silent", "panel": "shared" }, "problemMatcher": [] } + ] +} diff --git a/multiloader/build.gradle b/multiloader/build.gradle index 4ee1f94..3b55522 100644 --- a/multiloader/build.gradle +++ b/multiloader/build.gradle @@ -29,27 +29,11 @@ subprojects { maven { name = 'BlameJared'; url = 'https://maven.blamejared.com/' } // JEI etc. (later) } - // Expand mod metadata tokens in the loader manifests. Range tracks the - // active MC version unless one is passed explicitly. - tasks.withType(ProcessResources).configureEach { - def mcRange = project.findProperty('minecraft_version_range') - ?: "[${rootProject.minecraft_version},)" - def props = [ - mod_id : rootProject.mod_id, - mod_name : rootProject.mod_name, - mod_version : rootProject.mod_version, - mod_license : rootProject.mod_license, - mod_authors : rootProject.mod_authors, - mod_group_id : rootProject.mod_group_id, - minecraft_version : rootProject.minecraft_version, - minecraft_version_range: mcRange, - fabric_loader_version: rootProject.fabric_loader_version, - ] - inputs.properties(props) - filesMatching(['fabric.mod.json', 'META-INF/neoforge.mods.toml', 'pack.mcmeta']) { - expand(props) - } - } + // NOTE: loader manifests (fabric.mod.json / neoforge.mods.toml) are expanded + // per-module by each module's `generateModMetadata` task from src/main/templates + // (see fabric/ and neoforge/ build.gradle). They are intentionally NOT in + // src/main/resources, so the IDE never copies a raw, unexpanded ${...} manifest + // into bin/main — which is what broke Run & Debug. } // Resolve the shared common source directories once, for the loader modules to diff --git a/multiloader/fabric/build.gradle b/multiloader/fabric/build.gradle index d0fa3b4..1afc21e 100644 --- a/multiloader/fabric/build.gradle +++ b/multiloader/fabric/build.gradle @@ -1,9 +1,9 @@ -// fabric: Fabric Loom. On de-obfuscated 26.x the game ships official names, so -// there is NO `mappings` line (matches the official Fabric template). Shares the -// common module's source directly (single copy -> no drift). +// fabric: Fabric Loom. De-obfuscated 26.x: NO `mappings` line. Shares the common +// module's source directly (single copy -> no drift). plugins { id 'net.fabricmc.fabric-loom' + id 'eclipse' // for eclipse.synchronizationTasks (VS Code / Buildship import hook) } def mc = rootProject.minecraft_version @@ -22,6 +22,29 @@ dependencies { } } +// Expand fabric.mod.json from src/main/templates into a generated resources dir. +// The template is NOT under src/main/resources, so the IDE never copies a RAW +// (token-laden) fabric.mod.json into bin/main. eclipse.synchronizationTasks runs +// this on VS Code/Buildship import, so bin/main gets the EXPANDED manifest and +// Run & Debug works — while values stay dynamic (sourced from gradle.properties). +def generateModMetadata = tasks.register('generateModMetadata', ProcessResources) { + def props = [ + mod_id : rootProject.mod_id, + mod_version : rootProject.mod_version, + mod_name : rootProject.mod_name, + mod_authors : rootProject.mod_authors, + mod_license : rootProject.mod_license, + fabric_loader_version: rootProject.fabric_loader_version, + minecraft_version : mc, + ] + inputs.properties(props) + expand(props) + from 'src/main/templates' + into layout.buildDirectory.dir('generated/sources/modMetadata') +} +sourceSets.main.resources.srcDir generateModMetadata +eclipse.synchronizationTasks generateModMetadata + loom { runs { client { diff --git a/multiloader/fabric/src/main/resources/fabric.mod.json b/multiloader/fabric/src/main/templates/fabric.mod.json similarity index 100% rename from multiloader/fabric/src/main/resources/fabric.mod.json rename to multiloader/fabric/src/main/templates/fabric.mod.json diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle index 9e6e2af..613dac7 100644 --- a/multiloader/neoforge/build.gradle +++ b/multiloader/neoforge/build.gradle @@ -3,8 +3,7 @@ // // 26.2 note: until NeoForge 26.2 is on the public Maven, self-build the 26.2.x // branch (`./gradlew :neoforge:publishToMavenLocal`) and set neo_version_26.2 to -// the version it prints — mavenLocal() below resolves it. The official MultiLoader -// -Template ships `26.2.0.1-beta` as a working default, so it may already resolve. +// the version it prints — mavenLocal() below resolves it. plugins { id 'net.neoforged.moddev' @@ -21,8 +20,30 @@ repositories { sourceSets.main.java.srcDir rootProject.ext.commonJava sourceSets.main.resources.srcDir rootProject.ext.commonResources +// Expand neoforge.mods.toml from src/main/templates into a generated resources dir +// (same pattern as the repo-root build). The template is NOT under src/main/resources, +// so the IDE never copies a RAW (token-laden) mods.toml into bin/main; +// neoForge.ideSyncTask runs this on IDE sync so bin/main gets the EXPANDED manifest +// and Run & Debug works — values stay dynamic (from gradle.properties). +def generateModMetadata = tasks.register('generateModMetadata', ProcessResources) { + def props = [ + mod_id : rootProject.mod_id, + mod_version : rootProject.mod_version, + mod_name : rootProject.mod_name, + mod_authors : rootProject.mod_authors, + mod_license : rootProject.mod_license, + minecraft_version_range: "[${mc},)", + ] + inputs.properties(props) + expand(props) + from 'src/main/templates' + into layout.buildDirectory.dir('generated/sources/modMetadata') +} +sourceSets.main.resources.srcDir generateModMetadata + neoForge { version = neoVersion + ideSyncTask generateModMetadata def at = project(':common').file('src/main/resources/META-INF/accesstransformer.cfg') if (at.exists()) { diff --git a/multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/multiloader/neoforge/src/main/templates/META-INF/neoforge.mods.toml similarity index 100% rename from multiloader/neoforge/src/main/resources/META-INF/neoforge.mods.toml rename to multiloader/neoforge/src/main/templates/META-INF/neoforge.mods.toml diff --git a/nerospace.code-workspace b/nerospace.code-workspace index d497938..c44ee69 100644 --- a/nerospace.code-workspace +++ b/nerospace.code-workspace @@ -1,10 +1,12 @@ { - "//": "Multi-root workspace. Adds multiloader/ as a second folder so VS Code's Gradle/Java extension imports the nested multiloader build — only then can its :fabric/:neoforge runs appear in Run & Debug. Open this file (File > Open Workspace from File) instead of the plain folder when you want to debug the multiloader. Generate the Fabric run config first with the 'ML: Generate Fabric VS Code run configs' task; it writes multiloader/.vscode/launch.json.", + "//": "Multi-root workspace: adds multiloader/ so VS Code imports the nested :fabric/:neoforge projects and their Run & Debug entries (multiloader/.vscode/launch.json) resolve. Open via File > Open Workspace from File to run/debug the multiloader. Configs are committed + relative; preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync.", "folders": [ { "name": "nerospace (root, NeoForge)", "path": "." }, { "name": "multiloader (Fabric + NeoForge)", "path": "multiloader" } ], "settings": { - "java.import.gradle.enabled": true + "java.import.gradle.enabled": true, + "java.configuration.updateBuildConfiguration": "automatic", + "java.compile.nullAnalysis.mode": "automatic" } } From cf5aa2dc89f7f468d66ffa57b6c44682fdd76d22 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 01:38:13 +0800 Subject: [PATCH 09/82] Update VS Code launch configs for modFolders Point fml.modFolders at nefospace/bin/main only (remove the additional build/resources path) across .vscode/launch.json and multiloader/.vscode/launch.json. Normalize VM arg escaping (use = instead of escaped = sequences), remove machine-specific absolute launch entries from multiloader, and tidy nerospace.code-workspace formatting and settings (remove a redundant java.setting). This fixes incorrect use of ${pathSeparator} (which VS Code treats as a file separator) and keeps run/debug configs portable and relative. --- .vscode/launch.json | 8 ++--- multiloader/.vscode/launch.json | 64 ++++++--------------------------- nerospace.code-workspace | 15 +++++--- 3 files changed, 24 insertions(+), 63 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2dad935..1a10248 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,7 +12,7 @@ "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareClientRun", "args": ["@${workspaceFolder}/build/moddev/clientRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], + "vmArgs": ["@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -28,7 +28,7 @@ "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareDataRun", "args": ["@${workspaceFolder}/build/moddev/dataRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], + "vmArgs": ["@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -44,7 +44,7 @@ "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareGameTestServerRun", "args": ["@${workspaceFolder}/build/moddev/gameTestServerRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], + "vmArgs": ["@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -60,7 +60,7 @@ "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareServerRun", "args": ["@${workspaceFolder}/build/moddev/serverRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main${pathSeparator}${workspaceFolder}/build/resources/main"], + "vmArgs": ["@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", diff --git a/multiloader/.vscode/launch.json b/multiloader/.vscode/launch.json index 3ac4dbc..bb1a4ac 100644 --- a/multiloader/.vscode/launch.json +++ b/multiloader/.vscode/launch.json @@ -1,5 +1,5 @@ { - "//": "Committed RELATIVE multiloader run/debug configs (no machine paths). Open nerospace.code-workspace, then pick a config in Run \u0026 Debug. preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync. Today Fabric@26.2 and NeoForge@26.1.2 run; Fabric@26.1.2 (Fabric never shipped MC 26.1.2) and NeoForge@26.2 (needs userdev) will fail in preLaunch until upstream ships.", + "//": "Committed RELATIVE multiloader run/debug configs (no machine paths). Open nerospace.code-workspace, then pick a config in Run & Debug. preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync. fml.modFolders points at bin/main only (IDE compiles classes + copies the expanded manifest there); do NOT add a 2nd path with ${pathSeparator} (VS Code resolves it to the FILE separator, not the classpath separator). Today Fabric@26.2 and NeoForge@26.1.2 run; Fabric@26.1.2 (no Fabric MC 26.1.2) and NeoForge@26.2 (needs userdev) fail in preLaunch until upstream ships.", "version": "0.2.0", "configurations": [ { @@ -9,7 +9,7 @@ "cwd": "${workspaceFolder}/fabric/runs/client", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=client -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", "args": "", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.2" @@ -21,7 +21,7 @@ "cwd": "${workspaceFolder}/fabric/runs/client", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=client -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", "args": "", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.1.2" @@ -33,7 +33,7 @@ "cwd": "${workspaceFolder}/fabric/runs/server", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=server -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", "args": "nogui", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.2" @@ -45,7 +45,7 @@ "cwd": "${workspaceFolder}/fabric/runs/server", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=server -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", "args": "nogui", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.1.2" @@ -62,7 +62,7 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/clientRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-client-26.1.2", @@ -80,7 +80,7 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/clientRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-client-26.2", @@ -98,7 +98,7 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/serverRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-server-26.1.2", @@ -116,55 +116,11 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/serverRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main${pathSeparator}${workspaceFolder}/neoforge/build/resources/main" + "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-server-26.2", "shortenCommandLine": "none" - }, - { - "type": "java", - "request": "launch", - "name": "neoforge - Client", - "presentation": { - "group": "Mod Development - neoforge", - "order": 0 - }, - "projectName": "neoforge", - "mainClass": "net.neoforged.devlaunch.Main", - "args": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunProgramArgs.txt" - ], - "vmArgs": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" - ], - "cwd": "${workspaceFolder}\\neoforge\\runs\\client", - "env": {}, - "console": "internalConsole", - "shortenCommandLine": "none" - }, - { - "type": "java", - "request": "launch", - "name": "neoforge - Server", - "presentation": { - "group": "Mod Development - neoforge", - "order": 1 - }, - "projectName": "neoforge", - "mainClass": "net.neoforged.devlaunch.Main", - "args": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunProgramArgs.txt" - ], - "vmArgs": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" - ], - "cwd": "${workspaceFolder}\\neoforge\\runs\\server", - "env": {}, - "console": "internalConsole", - "shortenCommandLine": "none" } ] -} \ No newline at end of file +} diff --git a/nerospace.code-workspace b/nerospace.code-workspace index c44ee69..f927311 100644 --- a/nerospace.code-workspace +++ b/nerospace.code-workspace @@ -1,12 +1,17 @@ { - "//": "Multi-root workspace: adds multiloader/ so VS Code imports the nested :fabric/:neoforge projects and their Run & Debug entries (multiloader/.vscode/launch.json) resolve. Open via File > Open Workspace from File to run/debug the multiloader. Configs are committed + relative; preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync.", + "//": "Multi-root workspace: adds multiloader/ so VS Code imports the nested :fabric/:neoforge projects and their Run & Debug entries resolve. Open via File > Open Workspace from File to run/debug the multiloader.", "folders": [ - { "name": "nerospace (root, NeoForge)", "path": "." }, - { "name": "multiloader (Fabric + NeoForge)", "path": "multiloader" } + { + "name": "nerospace (root, NeoForge)", + "path": "." + }, + { + "name": "multiloader (Fabric + NeoForge)", + "path": "multiloader" + } ], "settings": { "java.import.gradle.enabled": true, - "java.configuration.updateBuildConfiguration": "automatic", - "java.compile.nullAnalysis.mode": "automatic" + "java.configuration.updateBuildConfiguration": "automatic" } } From cea3c82993220465e9d2c6ce8e3244b0a08d1863 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:14:20 +0800 Subject: [PATCH 10/82] Introduce registration seam and Nerosium block Add a cross-loader registration seam (RegistrationProvider) and concrete factories for Fabric and NeoForge. Introduce ModBlocks, ModItems and ModRegistries to register a new Nerosium block and its BlockItem, plus assets (blockstate, model, texture, lang). NeoForge factory wraps DeferredRegister instances and exposes registerAll to attach to the mod event bus; Fabric factory performs eager Registry.register calls. Wire ModRegistries.init() into NerospaceCommon.init(). Also apply VS Code launch/tasks formatting tweaks, add multiloader neoforge run configs, and enable Java null analysis in nerospace.code-workspace. --- .vscode/launch.json | 76 +++++++++++++----- .vscode/tasks.json | 5 +- multiloader/.vscode/launch.json | 64 ++++++++++++--- .../neroland/nerospace/NerospaceCommon.java | 26 +++--- .../nerospace/registry/ModBlocks.java | 37 +++++++++ .../neroland/nerospace/registry/ModItems.java | 30 +++++++ .../nerospace/registry/ModRegistries.java | 20 ++--- .../registry/RegistrationProvider.java | 48 +++++++++++ .../nerospace/blockstates/nerosium_block.json | 7 ++ .../nerospace/items/nerosium_block.json | 6 ++ .../assets/nerospace/lang/en_us.json | 3 + .../models/block/nerosium_block.json | 6 ++ .../textures/block/nerosium_block.png | Bin 0 -> 485 bytes .../nerospace/fabric/NerospaceFabric.java | 12 +-- .../registry/FabricRegistrationFactory.java | 58 +++++++++++++ ...pace.registry.RegistrationProvider$Factory | 1 + .../nerospace/neoforge/NerospaceNeoForge.java | 21 ++--- .../registry/NeoForgeRegistrationFactory.java | 71 ++++++++++++++++ ...pace.registry.RegistrationProvider$Factory | 1 + nerospace.code-workspace | 3 +- 20 files changed, 413 insertions(+), 82 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/RegistrationProvider.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosium_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_block.png create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/registry/FabricRegistrationFactory.java create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/registry/NeoForgeRegistrationFactory.java create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory diff --git a/.vscode/launch.json b/.vscode/launch.json index 1a10248..1565198 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,18 +1,28 @@ { - "//": "Run/debug configs for the ROOT single-loader NeoForge build (project 'nerospace'). modFolders points at bin/main (IDE classes) AND build/resources/main (where the generated neoforge.mods.toml lives). Multiloader run/debug lives in multiloader/.vscode + nerospace.code-workspace.", + "//": "Run/debug configs for the ROOT single-loader NeoForge build (project \u0027nerospace\u0027). modFolders points at bin/main (IDE classes) AND build/resources/main (where the generated neoforge.mods.toml lives). Multiloader run/debug lives in multiloader/.vscode + nerospace.code-workspace.", "version": "0.2.0", "configurations": [ { "type": "java", "request": "launch", "name": "Client", - "presentation": { "group": "Mod Development - nerospace", "order": 0 }, + "presentation": { + "group": "Mod Development - nerospace", + "order": 0 + }, "projectName": "nerospace", - "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], + "classPaths": [ + "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" + ], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareClientRun", - "args": ["@${workspaceFolder}/build/moddev/clientRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], + "args": [ + "@${workspaceFolder}/build/moddev/clientRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/build/moddev/clientRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + ], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -22,13 +32,23 @@ "type": "java", "request": "launch", "name": "Data", - "presentation": { "group": "Mod Development - nerospace", "order": 1 }, + "presentation": { + "group": "Mod Development - nerospace", + "order": 1 + }, "projectName": "nerospace", - "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], + "classPaths": [ + "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" + ], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareDataRun", - "args": ["@${workspaceFolder}/build/moddev/dataRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], + "args": [ + "@${workspaceFolder}/build/moddev/dataRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/build/moddev/dataRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + ], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -38,13 +58,23 @@ "type": "java", "request": "launch", "name": "GameTestServer", - "presentation": { "group": "Mod Development - nerospace", "order": 2 }, + "presentation": { + "group": "Mod Development - nerospace", + "order": 2 + }, "projectName": "nerospace", - "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], + "classPaths": [ + "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" + ], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareGameTestServerRun", - "args": ["@${workspaceFolder}/build/moddev/gameTestServerRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], + "args": [ + "@${workspaceFolder}/build/moddev/gameTestServerRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/build/moddev/gameTestServerRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + ], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", @@ -54,17 +84,27 @@ "type": "java", "request": "launch", "name": "Server", - "presentation": { "group": "Mod Development - nerospace", "order": 3 }, + "presentation": { + "group": "Mod Development - nerospace", + "order": 3 + }, "projectName": "nerospace", - "classPaths": ["${workspaceFolder}/build/moddev/devlaunchClasspath.jar"], + "classPaths": [ + "${workspaceFolder}/build/moddev/devlaunchClasspath.jar" + ], "mainClass": "net.neoforged.devlaunch.Main", "preLaunchTask": "prepareServerRun", - "args": ["@${workspaceFolder}/build/moddev/serverRunProgramArgs.txt"], - "vmArgs": ["@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", "-Dfml.modFolders=nerospace%%${workspaceFolder}/bin/main"], + "args": [ + "@${workspaceFolder}/build/moddev/serverRunProgramArgs.txt" + ], + "vmArgs": [ + "@${workspaceFolder}/build/moddev/serverRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/bin/main" + ], "cwd": "${workspaceFolder}/run", "env": {}, "console": "internalConsole", "shortenCommandLine": "none" } ] -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5ce6efb..3deea39 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,9 +39,9 @@ "problemMatcher": [] }, - { "//": "===== Multiloader: RUN both versions. Terminal -> Run Task. These launch the game directly via Gradle (no debugger). For breakpoint debugging, open nerospace.code-workspace (imports the multiloader build) and use its generated Run & Debug entries. =====" }, { "label": "ML: Run Fabric Client - 26.2", + "detail": "Multiloader RUN tasks launch the game directly via Gradle (no debugger). For breakpoint debugging, open nerospace.code-workspace and use its Run & Debug entries.", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, @@ -101,7 +101,6 @@ "problemMatcher": [] }, - { "//": "===== Multiloader: BUILD =====" }, { "label": "ML: Build both loaders (pick MC)", "type": "process", @@ -137,7 +136,7 @@ }, { "label": "ML: Generate Fabric VS Code run configs (pick MC)", - "//": "Loom's real task is 'vscode' (not genVsCodeRuns). Writes multiloader/.vscode/launch.json; use it from nerospace.code-workspace.", + "detail": "Loom's real task is 'vscode' (not genVsCodeRuns). Writes multiloader/.vscode/launch.json; use it from nerospace.code-workspace.", "type": "process", "command": "${workspaceFolder}/multiloader/gradlew", "windows": { "command": "${workspaceFolder}/multiloader/gradlew.bat" }, diff --git a/multiloader/.vscode/launch.json b/multiloader/.vscode/launch.json index bb1a4ac..cea1058 100644 --- a/multiloader/.vscode/launch.json +++ b/multiloader/.vscode/launch.json @@ -1,5 +1,5 @@ { - "//": "Committed RELATIVE multiloader run/debug configs (no machine paths). Open nerospace.code-workspace, then pick a config in Run & Debug. preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync. fml.modFolders points at bin/main only (IDE compiles classes + copies the expanded manifest there); do NOT add a 2nd path with ${pathSeparator} (VS Code resolves it to the FILE separator, not the classpath separator). Today Fabric@26.2 and NeoForge@26.1.2 run; Fabric@26.1.2 (no Fabric MC 26.1.2) and NeoForge@26.2 (needs userdev) fail in preLaunch until upstream ships.", + "//": "Committed RELATIVE multiloader run/debug configs (no machine paths). Open nerospace.code-workspace, then pick a config in Run \u0026 Debug. preLaunchTasks regenerate per-machine launch args; manifests expand into bin/main on Gradle/IDE sync. fml.modFolders points at bin/main only (IDE compiles classes + copies the expanded manifest there); do NOT add a 2nd path with ${pathSeparator} (VS Code resolves it to the FILE separator, not the classpath separator). Today Fabric@26.2 and NeoForge@26.1.2 run; Fabric@26.1.2 (no Fabric MC 26.1.2) and NeoForge@26.2 (needs userdev) fail in preLaunch until upstream ships.", "version": "0.2.0", "configurations": [ { @@ -9,7 +9,7 @@ "cwd": "${workspaceFolder}/fabric/runs/client", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=client -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", "args": "", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.2" @@ -21,7 +21,7 @@ "cwd": "${workspaceFolder}/fabric/runs/client", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=client -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dclient -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotClient --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", "args": "", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.1.2" @@ -33,7 +33,7 @@ "cwd": "${workspaceFolder}/fabric/runs/server", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=server -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", "args": "nogui", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.2" @@ -45,7 +45,7 @@ "cwd": "${workspaceFolder}/fabric/runs/server", "console": "integratedTerminal", "mainClass": "net.fabricmc.devlaunchinjector.Main", - "vmArgs": "-Dfabric.dli.config=${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env=server -Dfabric.dli.main=net.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access=allow --enable-native-access=ALL-UNNAMED", + "vmArgs": "-Dfabric.dli.config\u003d${workspaceFolder}/.gradle/loom-cache/projects/fabric/launch.cfg -Dfabric.dli.env\u003dserver -Dfabric.dli.main\u003dnet.fabricmc.loader.impl.launch.knot.KnotServer --sun-misc-unsafe-memory-access\u003dallow --enable-native-access\u003dALL-UNNAMED", "args": "nogui", "projectName": "fabric", "preLaunchTask": "ml-prepare-fabric-26.1.2" @@ -62,7 +62,7 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/clientRunVmArgs.txt", - "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-client-26.1.2", @@ -80,7 +80,7 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/clientRunVmArgs.txt", - "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-client-26.2", @@ -98,7 +98,7 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/serverRunVmArgs.txt", - "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-server-26.1.2", @@ -116,11 +116,55 @@ ], "vmArgs": [ "@${workspaceFolder}/neoforge/build/moddev/serverRunVmArgs.txt", - "-Dfml.modFolders=nerospace%%${workspaceFolder}/neoforge/bin/main" + "-Dfml.modFolders\u003dnerospace%%${workspaceFolder}/neoforge/bin/main" ], "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-server-26.2", "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "neoforge - Client", + "presentation": { + "group": "Mod Development - neoforge", + "order": 0 + }, + "projectName": "neoforge", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" + ], + "cwd": "${workspaceFolder}\\neoforge\\runs\\client", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" + }, + { + "type": "java", + "request": "launch", + "name": "neoforge - Server", + "presentation": { + "group": "Mod Development - neoforge", + "order": 1 + }, + "projectName": "neoforge", + "mainClass": "net.neoforged.devlaunch.Main", + "args": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunProgramArgs.txt" + ], + "vmArgs": [ + "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunVmArgs.txt", + "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" + ], + "cwd": "${workspaceFolder}\\neoforge\\runs\\server", + "env": {}, + "console": "internalConsole", + "shortenCommandLine": "none" } ] -} +} \ No newline at end of file diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java index 1c071e2..0515f07 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java @@ -2,20 +2,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import za.co.neroland.nerospace.platform.Services; +import za.co.neroland.nerospace.registry.ModRegistries; /** - * Loader-agnostic entry point. - * - *

Both {@code NerospaceFabric} and {@code NerospaceNeoForge} call - * {@link #init()} from their own loader entry points. All shared setup - * (registration, config wiring, common event hooks) belongs here or in - * the packages it touches — never in a loader module. - * - *

Anything that must reach loader-specific behaviour goes through - * {@link Services} (a Java {@link java.util.ServiceLoader} abstraction), - * keeping this module free of {@code net.neoforged.*} and - * {@code net.fabricmc.*} imports. + * Loader-agnostic entry point. Both {@code NerospaceFabric} and + * {@code NerospaceNeoForge} call {@link #init()} during mod construction. + * Loader-specific behaviour is reached only through {@link Services}, keeping + * this module free of {@code net.neoforged.*} / {@code net.fabricmc.*} imports. */ public final class NerospaceCommon { @@ -31,10 +26,9 @@ public static void init() { Services.PLATFORM.getPlatformName(), Services.PLATFORM.isDevelopmentEnvironment()); - // Content registration is wired per loader (NeoForge DeferredRegister / - // Fabric Registry.register) from each module's entry point. Without - // Architectury API there is no shared DeferredRegister; the migration - // (docs/MULTILOADER.md §2) introduces a small registration service on - // top of the platform Services seam when content is ported. + // Shared content registration via the RegistrationProvider seam. On + // NeoForge this builds DeferredRegisters (the loader entry point then + // attaches them to the mod bus); on Fabric it registers eagerly. + ModRegistries.init(); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java new file mode 100644 index 0000000..4504788 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.MapColor; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Block registrations shared by both loaders. First migrated slice: a single + * storage block, registered through {@link RegistrationProvider} so the exact + * same definition drives Fabric and NeoForge. + */ +public final class ModBlocks { + + public static final RegistrationProvider BLOCKS = + RegistrationProvider.get(Registries.BLOCK, NerospaceCommon.MOD_ID); + + public static final RegistryEntry NEROSIUM_BLOCK = BLOCKS.register( + "nerosium_block", + key -> new Block(BlockBehaviour.Properties.of() + .setId(key) + .mapColor(MapColor.COLOR_LIGHT_BLUE) + .strength(5.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL))); + + private ModBlocks() { + } + + /** Touch to force class-init (and thus registration). */ + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java new file mode 100644 index 0000000..63b6ac4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -0,0 +1,30 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Item; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Item registrations shared by both loaders. The block item for + * {@link ModBlocks#NEROSIUM_BLOCK} — proves a second registry flows through the + * same abstraction and that cross-entry references resolve in registry order. + */ +public final class ModItems { + + public static final RegistrationProvider ITEMS = + RegistrationProvider.get(Registries.ITEM, NerospaceCommon.MOD_ID); + + public static final RegistryEntry NEROSIUM_BLOCK_ITEM = ITEMS.register( + "nerosium_block", + key -> new BlockItem(ModBlocks.NEROSIUM_BLOCK.get(), new Item.Properties().setId(key))); + + private ModItems() { + } + + /** Touch to force class-init (and thus registration). */ + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index 5078d78..dcf55c1 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -1,21 +1,17 @@ package za.co.neroland.nerospace.registry; /** - * Placeholder for cross-loader content registration. - * - *

The MultiLoader-Template approach (no Architectury API) does not provide a - * shared {@code DeferredRegister}. Registration is performed per loader from the - * loader entry points — NeoForge via {@code DeferredRegister}/{@code Registry} - * events, Fabric via {@code Registry.register} — typically funnelled through a - * small registration service added on top of the platform - * {@link za.co.neroland.nerospace.platform.Services} seam. - * - *

This class is intentionally empty in the scaffold; it is the seam where the - * migration (see {@code docs/MULTILOADER.md} §2) will introduce that service as - * content is ported off the root project's NeoForge {@code DeferredRegister}s. + * Aggregates the cross-loader content registries. Called once from + * {@link za.co.neroland.nerospace.NerospaceCommon#init()}. Order matters: + * blocks before items (the block item references its block). */ public final class ModRegistries { private ModRegistries() { } + + public static void init() { + ModBlocks.init(); + ModItems.init(); + } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/RegistrationProvider.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/RegistrationProvider.java new file mode 100644 index 0000000..ad4616a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/RegistrationProvider.java @@ -0,0 +1,48 @@ +package za.co.neroland.nerospace.registry; + +import java.util.function.Function; +import java.util.function.Supplier; + +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; + +import za.co.neroland.nerospace.platform.Services; + +/** + * Cross-loader registration seam (the MultiLoader-Template alternative to + * Architectury's {@code DeferredRegister}, which has not ported to 26.x yet). + * + *

Common code obtains a provider for a vanilla registry and registers + * factories that receive the entry's {@link ResourceKey} — so the value can set + * its own id ({@code Properties.setId(key)}, mandatory since 1.21.2). Each loader + * ships one {@link Factory} implementation, resolved via {@link Services} + * ({@link java.util.ServiceLoader}): + *

    + *
  • NeoForge wraps a {@code DeferredRegister} (attached to the mod bus);
  • + *
  • Fabric calls {@code Registry.register} eagerly.
  • + *
+ */ +public interface RegistrationProvider { + + static RegistrationProvider get(ResourceKey> registryKey, String modId) { + return Factory.INSTANCE.create(registryKey, modId); + } + + RegistryEntry register(String name, Function, I> factory); + + /** A registered entry: a {@link Supplier} of the value plus its id. */ + interface RegistryEntry extends Supplier { + @Override + R get(); + + Identifier id(); + } + + /** Loader-provided bridge to the platform registry. */ + interface Factory { + Factory INSTANCE = Services.load(Factory.class); + + RegistrationProvider create(ResourceKey> registryKey, String modId); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_block.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_block.json new file mode 100644 index 0000000..c39a18c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/nerosium_block" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_block.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_block.json new file mode 100644 index 0000000..65abc5e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_block.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/nerosium_block" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json new file mode 100644 index 0000000..e97f6fb --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -0,0 +1,3 @@ +{ + "block.nerospace.nerosium_block": "Block of Nerosium" +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_block.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_block.json new file mode 100644 index 0000000..955d579 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/nerosium_block" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_block.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_block.png new file mode 100644 index 0000000000000000000000000000000000000000..6c0f037fdd10fb9055fa169ff091b57aba3b3970 GIT binary patch literal 485 zcmVB>m@JZaKN?|I964s7`vtl1q00L)Hn7Ly*MbVcmeX&j=+Gt)pX>*UxUi*);!CO=+9^3t4bS%DME6~I1Q za$w6-!(;kbQ^R9-9Pr6YXnME5f@H7G4v~6b=i-k!_tUOwcgyPKJ7$gc{*|6i zj{R{(rp@3AAa?8Qvn89XAa?6?(tGyVlHGB@?6d}8G3l|%ii@G?iVwpE05!==xgNIv b`EULSM0DT%6S3Ao00000NkvXXu0mjflbq(g literal 0 HcmV?d00001 diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index c3dcf38..c6859cc 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -1,17 +1,13 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ModInitializer; + import za.co.neroland.nerospace.NerospaceCommon; /** - * Fabric common-side entry point. Delegates all shared setup to - * {@link NerospaceCommon#init()}. - * - *

As the migration proceeds, Fabric-specific wiring goes here: - * registering networking via {@code PayloadTypeRegistry} + - * {@code ServerPlayNetworking}, event callbacks ({@code ServerTickEvents}, - * {@code ServerPlayConnectionEvents}, ...), capability/storage providers via - * the Fabric Transfer API, and Fabric data generation. + * Fabric entry point. Shared init registers content eagerly (Fabric needs no + * deferred bus). Creative-tab insertion (which needs the Fabric API + * item-group module) is wired in the next step alongside the Fabric API setup. */ public final class NerospaceFabric implements ModInitializer { diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/registry/FabricRegistrationFactory.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/registry/FabricRegistrationFactory.java new file mode 100644 index 0000000..8173223 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/registry/FabricRegistrationFactory.java @@ -0,0 +1,58 @@ +package za.co.neroland.nerospace.registry; + +import java.util.function.Function; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; + +/** + * Fabric {@link RegistrationProvider.Factory}: registers eagerly via + * {@code Registry.register}. The factory supplies the entry's {@link ResourceKey} + * so the value can set its own id before registration. + * + *

Registered via {@code META-INF/services/ + * za.co.neroland.nerospace.registry.RegistrationProvider$Factory}. + */ +public final class FabricRegistrationFactory implements RegistrationProvider.Factory { + + @Override + @SuppressWarnings("unchecked") + public RegistrationProvider create(ResourceKey> registryKey, String modId) { + Registry registry = (Registry) BuiltInRegistries.REGISTRY.getValue(registryKey.identifier()); + return new Provider<>(registry, registryKey, modId); + } + + private static final class Provider implements RegistrationProvider { + + private final Registry registry; + private final ResourceKey> registryKey; + private final String modId; + + Provider(Registry registry, ResourceKey> registryKey, String modId) { + this.registry = registry; + this.registryKey = registryKey; + this.modId = modId; + } + + @Override + public RegistryEntry register(String name, Function, I> factory) { + Identifier id = Identifier.fromNamespaceAndPath(modId, name); + ResourceKey key = ResourceKey.create(registryKey, id); + I value = factory.apply(key); + Registry.register(registry, key, value); + return new RegistryEntry<>() { + @Override + public I get() { + return value; + } + + @Override + public Identifier id() { + return id; + } + }; + } + } +} diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory new file mode 100644 index 0000000..d606404 --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory @@ -0,0 +1 @@ +za.co.neroland.nerospace.registry.FabricRegistrationFactory diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 7e27502..7e5bcb4 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -3,29 +3,22 @@ import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; + import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; /** - * NeoForge entry point. Mirrors the root project's {@code Nerospace} class - * but does nothing loader-specific beyond construction — all shared setup - * is delegated to {@link NerospaceCommon#init()}. - * - *

As the migration proceeds, the per-loader registration wiring (binding - * Architectury DeferredRegisters, capability providers via - * {@code RegisterCapabilitiesEvent}, payload registration, etc.) lives here - * and in sibling classes in this module. + * NeoForge entry point. Runs shared init (which builds the DeferredRegisters via + * the RegistrationProvider seam), then attaches those registers to the mod bus. + * Creative-tab insertion is added in the next step (with the Fabric side, for + * symmetry). */ @Mod(NerospaceCommon.MOD_ID) public final class NerospaceNeoForge { public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NerospaceCommon.LOGGER.info("[Nerospace] NeoForge bootstrap"); - // Shared, loader-agnostic init. NerospaceCommon.init(); - - // TODO (migration): wire NeoForge-specific listeners on modEventBus, - // e.g. RegisterCapabilitiesEvent, RegisterPayloadHandlersEvent, - // EntityAttributeCreationEvent, GatherDataEvent, client renderer - // registration (under a Dist.CLIENT guard). + NeoForgeRegistrationFactory.registerAll(modEventBus); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/registry/NeoForgeRegistrationFactory.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/registry/NeoForgeRegistrationFactory.java new file mode 100644 index 0000000..5435c2f --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/registry/NeoForgeRegistrationFactory.java @@ -0,0 +1,71 @@ +package za.co.neroland.nerospace.registry; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import net.minecraft.core.Registry; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +/** + * NeoForge {@link RegistrationProvider.Factory}: each provider wraps a + * {@link DeferredRegister}. The registers are collected as they are created + * (during {@code NerospaceCommon.init()}) and attached to the mod event bus by + * the loader entry point via {@link #registerAll(IEventBus)}. + * + *

Registered via {@code META-INF/services/ + * za.co.neroland.nerospace.registry.RegistrationProvider$Factory}. + */ +public final class NeoForgeRegistrationFactory implements RegistrationProvider.Factory { + + private static final List> REGISTERS = new ArrayList<>(); + + /** Attach every DeferredRegister created so far to the mod event bus. */ + public static void registerAll(IEventBus modEventBus) { + REGISTERS.forEach(register -> register.register(modEventBus)); + } + + @Override + public RegistrationProvider create(ResourceKey> registryKey, String modId) { + DeferredRegister register = DeferredRegister.create(registryKey, modId); + REGISTERS.add(register); + return new Provider<>(register, registryKey, modId); + } + + private static final class Provider implements RegistrationProvider { + + private final DeferredRegister register; + private final ResourceKey> registryKey; + private final String modId; + + Provider(DeferredRegister register, ResourceKey> registryKey, String modId) { + this.register = register; + this.registryKey = registryKey; + this.modId = modId; + } + + @Override + public RegistryEntry register(String name, Function, I> factory) { + Identifier id = Identifier.fromNamespaceAndPath(modId, name); + ResourceKey key = ResourceKey.create(registryKey, id); + Supplier supplier = () -> factory.apply(key); + DeferredHolder holder = register.register(name, supplier); + return new RegistryEntry<>() { + @Override + public I get() { + return holder.get(); + } + + @Override + public Identifier id() { + return id; + } + }; + } + } +} diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory new file mode 100644 index 0000000..8c821ad --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.registry.RegistrationProvider$Factory @@ -0,0 +1 @@ +za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory diff --git a/nerospace.code-workspace b/nerospace.code-workspace index f927311..c0f3548 100644 --- a/nerospace.code-workspace +++ b/nerospace.code-workspace @@ -12,6 +12,7 @@ ], "settings": { "java.import.gradle.enabled": true, - "java.configuration.updateBuildConfiguration": "automatic" + "java.configuration.updateBuildConfiguration": "automatic", + "java.compile.nullAnalysis.mode": "automatic" } } From 92070a8ea72998fc7276791a9d26ad5e2f73eabe Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:22:31 +0800 Subject: [PATCH 11/82] Add ore/material blocks, items, and assets Introduce multiple ore/material families and the associated assets, and centralise common registrations. ModBlocks was refactored to register many blocks (nerosium, deepslate_nerosium, nerosteel, xertz_quartz, cindrite, glacite, raw blocks, etc.) via a helper that applies common properties (map color, hardness/resistance, requiresCorrectToolForDrops, sound). ModItems now registers block items and material items (ingots, raw materials, quartz, gems) and exposes creativeTabItems() to group items into vanilla creative tabs. Adds all required resource files (blockstates, models, item models, textures, language entries, data tags, loot tables and recipes) to support the new content. Also updates platform entry points/templates and the workspace to reflect the expanded registrations. --- .../nerospace/registry/ModBlocks.java | 54 +++++++++++---- .../neroland/nerospace/registry/ModItems.java | 65 ++++++++++++++++-- .../nerospace/blockstates/cindrite_block.json | 7 ++ .../nerospace/blockstates/cindrite_ore.json | 7 ++ .../blockstates/deepslate_nerosium_ore.json | 7 ++ .../nerospace/blockstates/glacite_block.json | 7 ++ .../nerospace/blockstates/glacite_ore.json | 7 ++ .../nerospace/blockstates/nerosium_ore.json | 7 ++ .../blockstates/nerosteel_block.json | 7 ++ .../nerospace/blockstates/nerosteel_ore.json | 7 ++ .../blockstates/raw_nerosium_block.json | 7 ++ .../blockstates/xertz_quartz_ore.json | 7 ++ .../assets/nerospace/items/cindrite.json | 6 ++ .../nerospace/items/cindrite_block.json | 6 ++ .../assets/nerospace/items/cindrite_ore.json | 6 ++ .../items/deepslate_nerosium_ore.json | 6 ++ .../assets/nerospace/items/glacite.json | 6 ++ .../assets/nerospace/items/glacite_block.json | 6 ++ .../assets/nerospace/items/glacite_ore.json | 6 ++ .../nerospace/items/nerosium_ingot.json | 6 ++ .../assets/nerospace/items/nerosium_ore.json | 6 ++ .../nerospace/items/nerosteel_block.json | 6 ++ .../nerospace/items/nerosteel_ingot.json | 6 ++ .../assets/nerospace/items/nerosteel_ore.json | 6 ++ .../assets/nerospace/items/raw_nerosium.json | 6 ++ .../nerospace/items/raw_nerosium_block.json | 6 ++ .../assets/nerospace/items/raw_nerosteel.json | 6 ++ .../assets/nerospace/items/xertz_quartz.json | 6 ++ .../nerospace/items/xertz_quartz_ore.json | 6 ++ .../assets/nerospace/lang/en_us.json | 19 ++++- .../models/block/cindrite_block.json | 6 ++ .../nerospace/models/block/cindrite_ore.json | 6 ++ .../models/block/deepslate_nerosium_ore.json | 6 ++ .../nerospace/models/block/glacite_block.json | 6 ++ .../nerospace/models/block/glacite_ore.json | 6 ++ .../nerospace/models/block/nerosium_ore.json | 6 ++ .../models/block/nerosteel_block.json | 6 ++ .../nerospace/models/block/nerosteel_ore.json | 6 ++ .../models/block/raw_nerosium_block.json | 6 ++ .../models/block/xertz_quartz_ore.json | 6 ++ .../nerospace/models/item/cindrite.json | 6 ++ .../assets/nerospace/models/item/glacite.json | 6 ++ .../nerospace/models/item/nerosium_ingot.json | 6 ++ .../models/item/nerosteel_ingot.json | 6 ++ .../nerospace/models/item/raw_nerosium.json | 6 ++ .../nerospace/models/item/raw_nerosteel.json | 6 ++ .../nerospace/models/item/xertz_quartz.json | 6 ++ .../textures/block/cindrite_block.png | Bin 0 -> 402 bytes .../nerospace/textures/block/cindrite_ore.png | Bin 0 -> 498 bytes .../textures/block/deepslate_nerosium_ore.png | Bin 0 -> 506 bytes .../textures/block/glacite_block.png | Bin 0 -> 375 bytes .../nerospace/textures/block/glacite_ore.png | Bin 0 -> 507 bytes .../nerospace/textures/block/nerosium_ore.png | Bin 0 -> 541 bytes .../textures/block/nerosteel_block.png | Bin 0 -> 481 bytes .../textures/block/nerosteel_ore.png | Bin 0 -> 500 bytes .../textures/block/raw_nerosium_block.png | Bin 0 -> 481 bytes .../textures/block/xertz_quartz_ore.png | Bin 0 -> 491 bytes .../nerospace/textures/item/cindrite.png | Bin 0 -> 160 bytes .../nerospace/textures/item/glacite.png | Bin 0 -> 194 bytes .../textures/item/nerosium_ingot.png | Bin 0 -> 212 bytes .../textures/item/nerosteel_ingot.png | Bin 0 -> 204 bytes .../nerospace/textures/item/raw_nerosium.png | Bin 0 -> 261 bytes .../nerospace/textures/item/raw_nerosteel.png | Bin 0 -> 235 bytes .../nerospace/textures/item/xertz_quartz.png | Bin 0 -> 195 bytes .../data/c/tags/block/ores/cindrite.json | 5 ++ .../data/c/tags/block/ores/glacite.json | 5 ++ .../data/c/tags/block/ores/nerosteel.json | 5 ++ .../data/c/tags/block/ores/xertz_quartz.json | 5 ++ .../c/tags/block/storage_blocks/cindrite.json | 5 ++ .../c/tags/block/storage_blocks/glacite.json | 5 ++ .../tags/block/storage_blocks/nerosteel.json | 5 ++ .../data/c/tags/item/gems/cindrite.json | 5 ++ .../data/c/tags/item/gems/glacite.json | 5 ++ .../data/c/tags/item/gems/xertz_quartz.json | 5 ++ .../data/c/tags/item/ingots/nerosium.json | 5 ++ .../data/c/tags/item/ingots/nerosteel.json | 5 ++ .../data/c/tags/item/ores/cindrite.json | 5 ++ .../data/c/tags/item/ores/glacite.json | 5 ++ .../data/c/tags/item/ores/nerosteel.json | 5 ++ .../data/c/tags/item/ores/xertz_quartz.json | 5 ++ .../c/tags/item/raw_materials/nerosium.json | 5 ++ .../c/tags/item/raw_materials/nerosteel.json | 5 ++ .../c/tags/item/storage_blocks/cindrite.json | 5 ++ .../c/tags/item/storage_blocks/glacite.json | 5 ++ .../c/tags/item/storage_blocks/nerosteel.json | 5 ++ .../tags/block/mineable/pickaxe.json | 15 ++++ .../minecraft/tags/block/needs_iron_tool.json | 14 ++++ .../loot_table/blocks/cindrite_block.json | 21 ++++++ .../loot_table/blocks/cindrite_ore.json | 52 ++++++++++++++ .../blocks/deepslate_nerosium_ore.json | 52 ++++++++++++++ .../loot_table/blocks/glacite_block.json | 21 ++++++ .../loot_table/blocks/glacite_ore.json | 52 ++++++++++++++ .../loot_table/blocks/nerosium_block.json | 21 ++++++ .../loot_table/blocks/nerosium_ore.json | 52 ++++++++++++++ .../loot_table/blocks/nerosteel_block.json | 21 ++++++ .../loot_table/blocks/nerosteel_ore.json | 52 ++++++++++++++ .../loot_table/blocks/raw_nerosium_block.json | 21 ++++++ .../loot_table/blocks/xertz_quartz_ore.json | 52 ++++++++++++++ .../data/nerospace/recipe/cindrite_block.json | 15 ++++ .../data/nerospace/recipe/glacite_block.json | 15 ++++ .../data/nerospace/recipe/nerosium_block.json | 15 ++++ ...sium_ingot_from_blasting_raw_nerosium.json | 10 +++ ...sium_ingot_from_smelting_raw_nerosium.json | 10 +++ .../nerospace/recipe/nerosteel_block.json | 15 ++++ ...eel_ingot_from_blasting_raw_nerosteel.json | 10 +++ ...eel_ingot_from_smelting_raw_nerosteel.json | 10 +++ .../nerospace/recipe/raw_nerosium_block.json | 15 ++++ .../nerospace/fabric/NerospaceFabric.java | 10 ++- .../fabric/src/main/templates/fabric.mod.json | 4 +- .../nerospace/neoforge/NerospaceNeoForge.java | 20 ++++-- nerospace.code-workspace | 3 +- 111 files changed, 1084 insertions(+), 31 deletions(-) create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/deepslate_nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/raw_nerosium_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/xertz_quartz_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/cindrite.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/cindrite_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/cindrite_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/deepslate_nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/glacite.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/glacite_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/glacite_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ingot.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ingot.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosteel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/deepslate_nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/raw_nerosium_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/xertz_quartz_ore.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/cindrite.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/glacite.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_ingot.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/nerosteel_ingot.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosium.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosteel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_quartz.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_block.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_ore.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/deepslate_nerosium_ore.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_block.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_ore.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_ore.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_block.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_ore.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/raw_nerosium_block.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/xertz_quartz_ore.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/cindrite.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/glacite.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_ingot.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosteel_ingot.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosium.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosteel.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_quartz.png create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/ores/cindrite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/ores/glacite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/ores/nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/ores/xertz_quartz.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/cindrite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/glacite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/gems/cindrite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/gems/glacite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/gems/xertz_quartz.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosium.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/ores/cindrite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/ores/glacite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/ores/nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/ores/xertz_quartz.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosium.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/cindrite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/glacite.json create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json create mode 100644 multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/deepslate_nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/raw_nerosium_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/xertz_quartz_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/cindrite_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/glacite_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_blasting_raw_nerosium.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_smelting_raw_nerosium.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_blasting_raw_nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_smelting_raw_nerosteel.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/raw_nerosium_block.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 4504788..a44b472 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -10,28 +10,58 @@ import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** - * Block registrations shared by both loaders. First migrated slice: a single - * storage block, registered through {@link RegistrationProvider} so the exact - * same definition drives Fabric and NeoForge. + * Block registrations shared by both loaders (ore / material families), + * registered through {@link RegistrationProvider}. All entries are plain + * {@link Block}s with {@code requiresCorrectToolForDrops} (see the block tags in + * data/minecraft/tags/block for the mining tier). */ public final class ModBlocks { public static final RegistrationProvider BLOCKS = RegistrationProvider.get(Registries.BLOCK, NerospaceCommon.MOD_ID); - public static final RegistryEntry NEROSIUM_BLOCK = BLOCKS.register( - "nerosium_block", - key -> new Block(BlockBehaviour.Properties.of() - .setId(key) - .mapColor(MapColor.COLOR_LIGHT_BLUE) - .strength(5.0F, 6.0F) - .requiresCorrectToolForDrops() - .sound(SoundType.METAL))); + // Nerosium + public static final RegistryEntry NEROSIUM_ORE = + simple("nerosium_ore", MapColor.STONE, 3.0F, 3.0F, SoundType.STONE); + public static final RegistryEntry DEEPSLATE_NEROSIUM_ORE = + simple("deepslate_nerosium_ore", MapColor.DEEPSLATE, 4.5F, 3.0F, SoundType.DEEPSLATE); + public static final RegistryEntry NEROSIUM_BLOCK = + simple("nerosium_block", MapColor.COLOR_LIGHT_BLUE, 5.0F, 6.0F, SoundType.METAL); + public static final RegistryEntry RAW_NEROSIUM_BLOCK = + simple("raw_nerosium_block", MapColor.COLOR_LIGHT_BLUE, 5.0F, 6.0F, SoundType.METAL); + + // Greenxertz (nerosteel + xertz quartz) + public static final RegistryEntry NEROSTEEL_ORE = + simple("nerosteel_ore", MapColor.STONE, 3.0F, 3.0F, SoundType.STONE); + public static final RegistryEntry XERTZ_QUARTZ_ORE = + simple("xertz_quartz_ore", MapColor.STONE, 3.0F, 3.0F, SoundType.NETHER_ORE); + public static final RegistryEntry NEROSTEEL_BLOCK = + simple("nerosteel_block", MapColor.COLOR_GRAY, 5.0F, 6.0F, SoundType.METAL); + + // Cindara + public static final RegistryEntry CINDRITE_ORE = + simple("cindrite_ore", MapColor.COLOR_BLACK, 3.5F, 3.0F, SoundType.STONE); + public static final RegistryEntry CINDRITE_BLOCK = + simple("cindrite_block", MapColor.COLOR_RED, 5.0F, 6.0F, SoundType.METAL); + + // Glacira + public static final RegistryEntry GLACITE_ORE = + simple("glacite_ore", MapColor.ICE, 3.5F, 3.0F, SoundType.STONE); + public static final RegistryEntry GLACITE_BLOCK = + simple("glacite_block", MapColor.COLOR_LIGHT_BLUE, 5.0F, 6.0F, SoundType.METAL); + + private static RegistryEntry simple(String name, MapColor color, float hardness, float resistance, SoundType sound) { + return BLOCKS.register(name, key -> new Block(BlockBehaviour.Properties.of() + .setId(key) + .mapColor(color) + .strength(hardness, resistance) + .requiresCorrectToolForDrops() + .sound(sound))); + } private ModBlocks() { } - /** Touch to force class-init (and thus registration). */ public static void init() { } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 63b6ac4..35cc504 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -1,30 +1,81 @@ package za.co.neroland.nerospace.registry; +import java.util.List; +import java.util.Map; + import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.Item; +import net.minecraft.world.level.ItemLike; +import net.minecraft.world.level.block.Block; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** - * Item registrations shared by both loaders. The block item for - * {@link ModBlocks#NEROSIUM_BLOCK} — proves a second registry flows through the - * same abstraction and that cross-entry references resolve in registry order. + * Item registrations shared by both loaders, plus the creative-tab grouping the + * loader entry points consume (tab placement defined once, in common). */ public final class ModItems { public static final RegistrationProvider ITEMS = RegistrationProvider.get(Registries.ITEM, NerospaceCommon.MOD_ID); - public static final RegistryEntry NEROSIUM_BLOCK_ITEM = ITEMS.register( - "nerosium_block", - key -> new BlockItem(ModBlocks.NEROSIUM_BLOCK.get(), new Item.Properties().setId(key))); + // Block items + public static final RegistryEntry NEROSIUM_ORE_ITEM = blockItem("nerosium_ore", ModBlocks.NEROSIUM_ORE); + public static final RegistryEntry DEEPSLATE_NEROSIUM_ORE_ITEM = blockItem("deepslate_nerosium_ore", ModBlocks.DEEPSLATE_NEROSIUM_ORE); + public static final RegistryEntry NEROSIUM_BLOCK_ITEM = blockItem("nerosium_block", ModBlocks.NEROSIUM_BLOCK); + public static final RegistryEntry RAW_NEROSIUM_BLOCK_ITEM = blockItem("raw_nerosium_block", ModBlocks.RAW_NEROSIUM_BLOCK); + public static final RegistryEntry NEROSTEEL_ORE_ITEM = blockItem("nerosteel_ore", ModBlocks.NEROSTEEL_ORE); + public static final RegistryEntry XERTZ_QUARTZ_ORE_ITEM = blockItem("xertz_quartz_ore", ModBlocks.XERTZ_QUARTZ_ORE); + public static final RegistryEntry NEROSTEEL_BLOCK_ITEM = blockItem("nerosteel_block", ModBlocks.NEROSTEEL_BLOCK); + public static final RegistryEntry CINDRITE_ORE_ITEM = blockItem("cindrite_ore", ModBlocks.CINDRITE_ORE); + public static final RegistryEntry CINDRITE_BLOCK_ITEM = blockItem("cindrite_block", ModBlocks.CINDRITE_BLOCK); + public static final RegistryEntry GLACITE_ORE_ITEM = blockItem("glacite_ore", ModBlocks.GLACITE_ORE); + public static final RegistryEntry GLACITE_BLOCK_ITEM = blockItem("glacite_block", ModBlocks.GLACITE_BLOCK); + + // Materials + public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); + public static final RegistryEntry NEROSIUM_INGOT = item("nerosium_ingot"); + public static final RegistryEntry RAW_NEROSTEEL = item("raw_nerosteel"); + public static final RegistryEntry NEROSTEEL_INGOT = item("nerosteel_ingot"); + public static final RegistryEntry XERTZ_QUARTZ = item("xertz_quartz"); + public static final RegistryEntry CINDRITE = item("cindrite"); + public static final RegistryEntry GLACITE = item("glacite"); + + private static RegistryEntry item(String name) { + return ITEMS.register(name, key -> new Item(new Item.Properties().setId(key))); + } + + private static RegistryEntry blockItem(String name, RegistryEntry block) { + return ITEMS.register(name, key -> new BlockItem(block.get(), new Item.Properties().setId(key))); + } + + /** Items grouped by the vanilla creative tab they should appear in. */ + public static Map, List> creativeTabItems() { + return Map.of( + CreativeModeTabs.NATURAL_BLOCKS, + List.of( + NEROSIUM_ORE_ITEM.get(), DEEPSLATE_NEROSIUM_ORE_ITEM.get(), + NEROSTEEL_ORE_ITEM.get(), XERTZ_QUARTZ_ORE_ITEM.get(), + CINDRITE_ORE_ITEM.get(), GLACITE_ORE_ITEM.get()), + CreativeModeTabs.BUILDING_BLOCKS, + List.of( + NEROSIUM_BLOCK_ITEM.get(), RAW_NEROSIUM_BLOCK_ITEM.get(), + NEROSTEEL_BLOCK_ITEM.get(), CINDRITE_BLOCK_ITEM.get(), GLACITE_BLOCK_ITEM.get()), + CreativeModeTabs.INGREDIENTS, + List.of( + RAW_NEROSIUM.get(), NEROSIUM_INGOT.get(), + RAW_NEROSTEEL.get(), NEROSTEEL_INGOT.get(), + XERTZ_QUARTZ.get(), CINDRITE.get(), GLACITE.get())); + } private ModItems() { } - /** Touch to force class-init (and thus registration). */ public static void init() { } } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_block.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_block.json new file mode 100644 index 0000000..41bb715 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/cindrite_block" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_ore.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_ore.json new file mode 100644 index 0000000..0ee45d7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/cindrite_ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/cindrite_ore" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/deepslate_nerosium_ore.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/deepslate_nerosium_ore.json new file mode 100644 index 0000000..b0ac2ed --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/deepslate_nerosium_ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/deepslate_nerosium_ore" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_block.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_block.json new file mode 100644 index 0000000..d0423be --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/glacite_block" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_ore.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_ore.json new file mode 100644 index 0000000..c1b6d4b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/glacite_ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/glacite_ore" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_ore.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_ore.json new file mode 100644 index 0000000..0a38ace --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/nerosium_ore" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_block.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_block.json new file mode 100644 index 0000000..2dcf9c3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/nerosteel_block" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_ore.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_ore.json new file mode 100644 index 0000000..420e5ff --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosteel_ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/nerosteel_ore" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/raw_nerosium_block.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/raw_nerosium_block.json new file mode 100644 index 0000000..aab2121 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/raw_nerosium_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/raw_nerosium_block" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/xertz_quartz_ore.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/xertz_quartz_ore.json new file mode 100644 index 0000000..b84aa66 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/xertz_quartz_ore.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/xertz_quartz_ore" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/cindrite.json b/multiloader/common/src/main/resources/assets/nerospace/items/cindrite.json new file mode 100644 index 0000000..3606259 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/cindrite.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/cindrite" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/cindrite_block.json b/multiloader/common/src/main/resources/assets/nerospace/items/cindrite_block.json new file mode 100644 index 0000000..c061092 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/cindrite_block.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/cindrite_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/cindrite_ore.json b/multiloader/common/src/main/resources/assets/nerospace/items/cindrite_ore.json new file mode 100644 index 0000000..4a559eb --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/cindrite_ore.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/cindrite_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/deepslate_nerosium_ore.json b/multiloader/common/src/main/resources/assets/nerospace/items/deepslate_nerosium_ore.json new file mode 100644 index 0000000..7e7c382 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/deepslate_nerosium_ore.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/deepslate_nerosium_ore" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/glacite.json b/multiloader/common/src/main/resources/assets/nerospace/items/glacite.json new file mode 100644 index 0000000..47e28d8 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/glacite.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/glacite" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/glacite_block.json b/multiloader/common/src/main/resources/assets/nerospace/items/glacite_block.json new file mode 100644 index 0000000..254d1e0 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/glacite_block.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/glacite_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/glacite_ore.json b/multiloader/common/src/main/resources/assets/nerospace/items/glacite_ore.json new file mode 100644 index 0000000..4ed11cc --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/glacite_ore.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/glacite_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ingot.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ingot.json new file mode 100644 index 0000000..af83406 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ingot.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/nerosium_ingot" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ore.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ore.json new file mode 100644 index 0000000..5f01bed --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_ore.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/nerosium_ore" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_block.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_block.json new file mode 100644 index 0000000..455301f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_block.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/nerosteel_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ingot.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ingot.json new file mode 100644 index 0000000..11e41b4 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ingot.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/nerosteel_ingot" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ore.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ore.json new file mode 100644 index 0000000..ee28480 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosteel_ore.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/nerosteel_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium.json b/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium.json new file mode 100644 index 0000000..dad7aa9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/raw_nerosium" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium_block.json b/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium_block.json new file mode 100644 index 0000000..69c3bde --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosium_block.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/raw_nerosium_block" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosteel.json b/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosteel.json new file mode 100644 index 0000000..c8d8587 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/raw_nerosteel.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/raw_nerosteel" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz.json b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz.json new file mode 100644 index 0000000..06b86ec --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/xertz_quartz" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz_ore.json b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz_ore.json new file mode 100644 index 0000000..c538e41 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_quartz_ore.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/xertz_quartz_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index e97f6fb..8fc8426 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -1,3 +1,20 @@ { - "block.nerospace.nerosium_block": "Block of Nerosium" + "block.nerospace.nerosium_ore": "Nerosium Ore", + "block.nerospace.deepslate_nerosium_ore": "Deepslate Nerosium Ore", + "block.nerospace.nerosium_block": "Block of Nerosium", + "block.nerospace.raw_nerosium_block": "Block of Raw Nerosium", + "item.nerospace.raw_nerosium": "Raw Nerosium", + "item.nerospace.nerosium_ingot": "Nerosium Ingot", + "block.nerospace.nerosteel_ore": "Nerosteel Ore", + "block.nerospace.nerosteel_block": "Block of Nerosteel", + "block.nerospace.xertz_quartz_ore": "Xertz Quartz Ore", + "block.nerospace.cindrite_ore": "Cindrite Ore", + "block.nerospace.cindrite_block": "Block of Cindrite", + "block.nerospace.glacite_ore": "Glacite Ore", + "block.nerospace.glacite_block": "Block of Glacite", + "item.nerospace.raw_nerosteel": "Raw Nerosteel", + "item.nerospace.nerosteel_ingot": "Nerosteel Ingot", + "item.nerospace.xertz_quartz": "Xertz Quartz", + "item.nerospace.cindrite": "Cindrite", + "item.nerospace.glacite": "Glacite" } diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_block.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_block.json new file mode 100644 index 0000000..6ce7e6e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/cindrite_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_ore.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_ore.json new file mode 100644 index 0000000..3663110 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/cindrite_ore.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/cindrite_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/deepslate_nerosium_ore.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/deepslate_nerosium_ore.json new file mode 100644 index 0000000..e395a92 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/deepslate_nerosium_ore.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/deepslate_nerosium_ore" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_block.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_block.json new file mode 100644 index 0000000..27d1341 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/glacite_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_ore.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_ore.json new file mode 100644 index 0000000..ef5a365 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/glacite_ore.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/glacite_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_ore.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_ore.json new file mode 100644 index 0000000..0ec0392 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_ore.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/nerosium_ore" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_block.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_block.json new file mode 100644 index 0000000..259591e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/nerosteel_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_ore.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_ore.json new file mode 100644 index 0000000..4d4320c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosteel_ore.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/nerosteel_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/raw_nerosium_block.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/raw_nerosium_block.json new file mode 100644 index 0000000..a0a704f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/raw_nerosium_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/raw_nerosium_block" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/xertz_quartz_ore.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/xertz_quartz_ore.json new file mode 100644 index 0000000..f9e7a45 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/xertz_quartz_ore.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/xertz_quartz_ore" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/cindrite.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/cindrite.json new file mode 100644 index 0000000..05bcc41 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/cindrite.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/cindrite" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/glacite.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/glacite.json new file mode 100644 index 0000000..40d5d46 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/glacite.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/glacite" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_ingot.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_ingot.json new file mode 100644 index 0000000..d748731 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_ingot.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/nerosium_ingot" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosteel_ingot.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosteel_ingot.json new file mode 100644 index 0000000..caaaf98 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosteel_ingot.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/nerosteel_ingot" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosium.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosium.json new file mode 100644 index 0000000..6f08816 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosium.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/raw_nerosium" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosteel.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosteel.json new file mode 100644 index 0000000..0a974e8 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/raw_nerosteel.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/raw_nerosteel" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_quartz.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_quartz.json new file mode 100644 index 0000000..d89cfa3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_quartz.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/xertz_quartz" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_block.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_block.png new file mode 100644 index 0000000000000000000000000000000000000000..a27a36ce9cd05208e027525dfc8c6bea09c389b5 GIT binary patch literal 402 zcmV;D0d4+?P) zY-);VXKwa56r=WI-kUdXW-+$w5&wuK0GwTaq3^sr*2V$=`p)C!@))aa2LLFG9JR6N zJCE8}c%q5G6TvzMJ_jNInBQUo<^o(W<4-?>$|6_bU4%FYh;xNNhnCM#7CEeQLZa1V ze}O_A5>o!FjZM^K+5-laMJ}yq$z_p?0?ti5U{HtyLLBgPbA+lf__%Kn;vm(vM~kst zkKfPTz7-PHW&^xE;84zL+f8FUz5A3tLmWh+bq+0VQ8fnVk454>tIb%lCrXS1fOb9Y zz{$m{fUI+g2TR=#08zmb->=McKd=KrprbYx^-MI4H;S=ZI=^LHRWo~{WcEB(y+Fwr w`QnLW&N~IWMdKXp)vp%xXCT6g_A}D>0pqLDi1O2W>;M1&07*qoM6N<$g4!>uH2?qr literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_ore.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_ore.png new file mode 100644 index 0000000000000000000000000000000000000000..3d6c631af15a85e6b72ed83ef6d1cba9869e47f5 GIT binary patch literal 498 zcmVmAmJ)LIdd-pI@t z%&d3!_0980&Y4n5&zl({LX5HVQdNc+BPpd0$Jh4{e(~hT8LEn^0uVwV=S&CzGb4n+ z;1Zqc>)&T^br{5>q&0uGN@w#7`DTisgQ zzXdZxRr|1bfr!A>IjY(Zd!3n4O4-|}s+3YlDa~g7(ZwfDzTfH6oq?)$hX!ZhokfJ! o8Y04%`;SQ}5o7E%Q@A3+Ut!cItvK-;1ONa407*qoM6N<$f*kPbi2wiq literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/deepslate_nerosium_ore.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/deepslate_nerosium_ore.png new file mode 100644 index 0000000000000000000000000000000000000000..f49f53a831311b375c9654dc2b289305ccdefb0e GIT binary patch literal 506 zcmVhMjH{a3N}%z*j<}o zEf^6&xZNWh`>?s2G@eDA$2)GC<;~yk`)0ONRf8q}Erb>kAe z73lUh_~tWIB0`q(I-JfJzy6GK1x-^ELW}ohq-01^iyFN6`IzqR4vc59(BP^nPrnWb zp=C4~Mv6^S)9Ls3mL{O8GQM$>`{&bGY`PpyXLKLD8%e5m4p(S&!hzDX_Rh8%4Z#dfAT0yf|%n6}gKV|D?_aGvdE4}rE zqs^_@^!VimMw21O`>z;HhL>BYs=+e-iLCplKXbafJKX*LZ3TX+iT5QN7PL)R!ak6n wPw~E7(ZJSc%`%&wQb`k=rj8U$Xrl@64>lSWvz6-rPyhe`07*qoM6N<$g2bHZT>t<8 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_block.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_block.png new file mode 100644 index 0000000000000000000000000000000000000000..8671021dd03c359a27dd6264c839da2cd825d48d GIT binary patch literal 375 zcmV--0f_#IP)hZZ)grAVa1lv-}r@cT!yU%{0COn{(!AyStP|)7Qr&g7J&r|A&??iEL&M`u}H8?Evc;c zC&*M`7%b#6jjlz!V_uwUc<E`<_}WW`?SgQfmJJRJGkl zA_73I6%oPA$T@Qino?@tQc8pn$T>6O=%{MDJByhSV{Am;9Al*O;+!*m-;+{eGoG>; zPX~atR%)%S)=wuQs45|ZR=*#Ol?J-uJD8Bd$`zVDk95g~+t zne72mN)5G?!h^wXGH>i3@3wq@Ipb`;XkF-* zpY!VU3D4x8i7vS-r4T}(l!BRY*{z8&a@nm1+P~f`29x;|C#%)!5olW72M^M5a6DKc z1b}@GJZbKM7mLVXWyeIoUVu*8`gb--0AtG!&I_~)H(BK_!KKq5L z^8NIH%i}xznSQ0U)>n9NyvL%Q0B~~jp*ylMpUd_@x0a=DWz+I zv3)LF&Y2h^Ap}$vGwW7+v6(TrKqAsD0ysa~W=!Ts=1!Oys(LM=>)@7(2(NbDv8Wdv zdwf*xfT}Wt5c>I!&v@(Z9^8AuqFzu+q1MXc;hgDupVjV+oHHWA#&Ws5@}0&}N@1m! zyxF~lQ`TCEF*056vz~srCK&HnO2N!9Gj6Q!b0`x^DP2gX`}pMNzdLZdTpYi$$*^{y;M5b3Qd# zW8p!OyaWK=8$e!a-T@?oKIzk3vex^ z6fBxCz?-)(U%y|!-~g_=qsb@OWd+t)RIJ2L27Q3`3o2G#kOW^Wi~Mw21dSiyNcD0L-O!zl3lde*?BmF~MqsaTPhy6r(d>r(dpA0Ox3v!Ryz z24)HiP+oM9ytGvv<*wcT-oWFP`YtP&-i_I11%^ukCz?3d}?@q`+-td?6N{M zU+M~Ig;wecYb>R%NT24yR&=oaytytAzL>fT1n~Urm*9HPZq>hL{v2B;2h+XJj~?z7 X^pEa&L&KI800000NkvXXu0mjfbWPdv literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_ore.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_ore.png new file mode 100644 index 0000000000000000000000000000000000000000..4a7e31c8c978ea2ff9fa3c0a22fc0f71e788c274 GIT binary patch literal 500 zcmVJ0GY$UQ=u!zBN zkr;F!OVhz|7L5oRtkuqFB}4c0t9tMC>l^F!`j?pjFbo4;r4#_nj2I&+B~%pv5h0}n zKqn%E5C|bmfW8hPP)Z@jNX{96loByULI`x@Nhu|q&A7~rTI+<7<9Y{}86gC|{oHf0 zzvR}BS!cCp`pWCg z+cpfKccVmvQVO#(*sQk9{;(2}c6h#8zI53DYHsjoRZyZ8sS9o{6n1ra5LI*St954(6r4&Sj>y_}~QO3RU^Eu}OgYlhu zpj<+&mF3Y1y5m2OJQJO&qNeQ|g^PXFKP qovOPkB2BM@RJG;8<*F*B6n+D6%M+n!m_OqH0000`+1`4SzI!f zG-UA5Nn|KSco2tCgdiE5_j1>c)pmF9&+mP|r&nhOpC0*-F97&a{G@JU?_ph6q}@i( zg$?yAvFo~`Bad=%w0D9D%72n2v9KZQy21n{z^;HK&-*!fdZ9^$4XN9h#6$p^ly)0c z(UFUfZ%%m^f93W5dlrM9r@$R&-p{Gq*jG^4klAL!s{G8u?H$?gKPp=q6gFfz%ovqZ zk5$JF8kJMt-fRF!OhjTLOi)s{F=@Bq!}l9iBqIb845dolb+4WaqX0#AVqP5U2ppHR z6rJ~TmcvZDx=cI*nbFzq&6H|M?r*RsP^%fsy*hpSfSly6i{Sq?K6gC6sK{!&2fl2OLR zNR!;+7x5R}ljHlb&!DiO{sY&|xq6+Lh`Nng*A+K0V?|}exyZvn)z-%*d;&yLO XuZ8NxEpKH300000NkvXXu0mjfd>Gqg literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/xertz_quartz_ore.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/xertz_quartz_ore.png new file mode 100644 index 0000000000000000000000000000000000000000..8f7a9dc1dceed88c66d48c55b2eb1da0816793ba GIT binary patch literal 491 zcmVtNm*jhtXd3aL+NGVZD zq4yrlUX2shTA5Nxj4@`0vnA`#mr5{ znAxuH!`SEL2+ZUFRR!SY>UWsum3ch$M?{39r=5uhtJUhoA3iVV^YC#8Dy4)J2l;Oi znLY52qbK2A%dfy@7Op#ypRF|(-gAGTs^N64H4?eRT5EI%nCHNq_HgizUzfc9@-Y(i z-eX==b(SC^(GpK$W}JRMpFx{#Fsdr$;yAn-W3ccFT5B;Je?P#uy5rBdjUDh=IDdNr zpNN((RF!=na?a!iNfWC2x&znu++OU!z&r=7wb(e9<5Ef~kw8`Hn{Dh1{NKi$b8OuH h82jlmeT4V7^&i9%Anl?$ojCvi002ovPDHLkV1kN$=ivYV literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/cindrite.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/cindrite.png new file mode 100644 index 0000000000000000000000000000000000000000..30a0dc522d6929a5d72e5a64c487f94acc60494b GIT binary patch literal 160 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`>7Fi*Ar*6y6D00E_%F8o|Nref z{vKc6#yR~+J6G?2`F@+E1Ou)Fi$s<7%K~fIYF1o0aG~L}01xwq%wy>se)FVdQ I&MBb@0B6lTNB{r; literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/glacite.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/glacite.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd942212e6338158820cb4f4ae2eebc0b674d54 GIT binary patch literal 194 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`ot`d^Ar*6y6D00EII;Xc7|73; zQi~C)EdTc>Js~0C$Diqq2N%3%^mE=I#c+AgtE=%lB=npOcp5U(fU+FI$!-%5r*Zh{ zFWAi>ejuA+7k7tI!Xz$kWi#4rSzV_ n&g%l3OpOf;3<8!^zF=UuCT+#N^ zwE55XNZjb%mY8u(U}nQYvBL~A+PZg_8NP6jJ^YGm51T^vjvYSS>(kiSavCk(9<2YV zWWT0MQbIyv!r%U@3~WFkD7aE?j+%ji!Jf*`Ygp$0@nB}SA7Hh}@W<}^KxZ;|y85}S Ib4q9e0K+j+u>b%7 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosteel_ingot.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosteel_ingot.png new file mode 100644 index 0000000000000000000000000000000000000000..71a3ba3cdb7fb792720fa6da0a730df37c4eb19b GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`lRaG=Ln`JZCrGf0FuRpB#{TdB zbA0*1-ivIm{M<(A_qIj;51*o+P*7Z){37wzkpgTe~DWM4f D1F=s| literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosium.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosium.png new file mode 100644 index 0000000000000000000000000000000000000000..5b9541b31f8fe83387c0c59d03125692f45806d9 GIT binary patch literal 261 zcmV+g0s8)lP)2Rg_M!=CZ06=*@#a7xY`oK+>Xtk!; zN=bP>anmJfXT(icD;fVbZu=Ynusli>RfLc|v3P7d7q$eDI{?6Ja&AObgoHB`RiqUe zU1z-Oi(mi%N7l%v2}Buu&1ePE&PccIu1>AI_6wp6jF82DhX)#7j+{q{ZU?)a00000 LNkvXXu0mjfH%DhI literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosteel.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosteel.png new file mode 100644 index 0000000000000000000000000000000000000000..77eba6522b501ce17eb79bbe5aeeee5abf6788a3 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`n><|{Ln`JZCrGebI0i+Y`XBqh z|IhQw6My#m%LY*{s`dro{8ee13W2kH41pL4y|7*as&X zgL%w&czCjsjCi(x;uF|1K`^$#-~CyA$8`3?s{QgjRn`^~dUJOkT9Cfz@P@S8d~86_ z<{v&U;l;sMymqpZJ2w9;N%=S1B2qw}&)}@V{=+wJTuJI>onid7tiWJ}Z&3mWSUld+ fvf;A31_ML)5C4fVS29-sUC-d@>gTe~DWM4f&URa3 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_quartz.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_quartz.png new file mode 100644 index 0000000000000000000000000000000000000000..740802b5cf5863e867bdc1e8f9c0e5f6fb7b5614 GIT binary patch literal 195 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`U7jwEAr*6y6D00EIJ>_5KM2Ur zmr{!nVvm#geg43K10TM9uWrla4}WOFvYOH0Z1}yq4c!loSPnDHNMi#6Sw&tE%bd0c zUzm1r7clN&Td=!9gIPvpjl{;C53YPm=8622*z@95VH2}{-{1PrukZdh-+%9~|BS!} mbFGh3PTRm{beZtWGBEsQcSv@s|L_IqDh5wiKbLh*2~7Zm8%(1B literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/c/tags/block/ores/cindrite.json b/multiloader/common/src/main/resources/data/c/tags/block/ores/cindrite.json new file mode 100644 index 0000000..27604ec --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/ores/cindrite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:cindrite_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/block/ores/glacite.json b/multiloader/common/src/main/resources/data/c/tags/block/ores/glacite.json new file mode 100644 index 0000000..89e34ef --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/ores/glacite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:glacite_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/block/ores/nerosteel.json b/multiloader/common/src/main/resources/data/c/tags/block/ores/nerosteel.json new file mode 100644 index 0000000..1f88731 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/ores/nerosteel.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosteel_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/block/ores/xertz_quartz.json b/multiloader/common/src/main/resources/data/c/tags/block/ores/xertz_quartz.json new file mode 100644 index 0000000..9ac0056 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/ores/xertz_quartz.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:xertz_quartz_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/cindrite.json b/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/cindrite.json new file mode 100644 index 0000000..ee40409 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/cindrite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:cindrite_block" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/glacite.json b/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/glacite.json new file mode 100644 index 0000000..aa44305 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/glacite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:glacite_block" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/nerosteel.json b/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/nerosteel.json new file mode 100644 index 0000000..c9dd8d2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/block/storage_blocks/nerosteel.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosteel_block" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/gems/cindrite.json b/multiloader/common/src/main/resources/data/c/tags/item/gems/cindrite.json new file mode 100644 index 0000000..57e652f --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/gems/cindrite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:cindrite" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/gems/glacite.json b/multiloader/common/src/main/resources/data/c/tags/item/gems/glacite.json new file mode 100644 index 0000000..f77cc2f --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/gems/glacite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:glacite" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/gems/xertz_quartz.json b/multiloader/common/src/main/resources/data/c/tags/item/gems/xertz_quartz.json new file mode 100644 index 0000000..8fc9d3c --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/gems/xertz_quartz.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:xertz_quartz" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosium.json b/multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosium.json new file mode 100644 index 0000000..9d28b75 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosium.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosium_ingot" + ] +} diff --git a/multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosteel.json b/multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosteel.json new file mode 100644 index 0000000..69459bc --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/ingots/nerosteel.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosteel_ingot" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/ores/cindrite.json b/multiloader/common/src/main/resources/data/c/tags/item/ores/cindrite.json new file mode 100644 index 0000000..27604ec --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/ores/cindrite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:cindrite_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/ores/glacite.json b/multiloader/common/src/main/resources/data/c/tags/item/ores/glacite.json new file mode 100644 index 0000000..89e34ef --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/ores/glacite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:glacite_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/ores/nerosteel.json b/multiloader/common/src/main/resources/data/c/tags/item/ores/nerosteel.json new file mode 100644 index 0000000..1f88731 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/ores/nerosteel.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosteel_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/ores/xertz_quartz.json b/multiloader/common/src/main/resources/data/c/tags/item/ores/xertz_quartz.json new file mode 100644 index 0000000..9ac0056 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/ores/xertz_quartz.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:xertz_quartz_ore" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosium.json b/multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosium.json new file mode 100644 index 0000000..b4d6c62 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosium.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:raw_nerosium" + ] +} diff --git a/multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosteel.json b/multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosteel.json new file mode 100644 index 0000000..880fdc9 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/raw_materials/nerosteel.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:raw_nerosteel" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/cindrite.json b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/cindrite.json new file mode 100644 index 0000000..ee40409 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/cindrite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:cindrite_block" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/glacite.json b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/glacite.json new file mode 100644 index 0000000..aa44305 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/glacite.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:glacite_block" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosteel.json b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosteel.json new file mode 100644 index 0000000..c9dd8d2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosteel.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosteel_block" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json new file mode 100644 index 0000000..6515cf1 --- /dev/null +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -0,0 +1,15 @@ +{ + "values": [ + "nerospace:nerosium_ore", + "nerospace:deepslate_nerosium_ore", + "nerospace:nerosium_block", + "nerospace:raw_nerosium_block", + "nerospace:nerosteel_ore", + "nerospace:xertz_quartz_ore", + "nerospace:nerosteel_block", + "nerospace:cindrite_ore", + "nerospace:cindrite_block", + "nerospace:glacite_ore", + "nerospace:glacite_block" + ] +} diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json new file mode 100644 index 0000000..ff61e80 --- /dev/null +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json @@ -0,0 +1,14 @@ +{ + "values": [ + "nerospace:nerosium_ore", + "nerospace:deepslate_nerosium_ore", + "nerospace:nerosium_block", + "nerospace:raw_nerosium_block", + "nerospace:nerosteel_ore", + "nerospace:nerosteel_block", + "nerospace:cindrite_ore", + "nerospace:cindrite_block", + "nerospace:glacite_ore", + "nerospace:glacite_block" + ] +} diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_block.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_block.json new file mode 100644 index 0000000..32076a8 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:cindrite_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/cindrite_block" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_ore.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_ore.json new file mode 100644 index 0000000..b6dfda5 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cindrite_ore.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:alternatives", + "children": [ + { + "type": "minecraft:item", + "conditions": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "predicates": { + "minecraft:enchantments": [ + { + "enchantments": "minecraft:silk_touch", + "levels": { + "min": 1 + } + } + ] + } + } + } + ], + "name": "nerospace:cindrite_ore" + }, + { + "type": "minecraft:item", + "functions": [ + { + "enchantment": "minecraft:fortune", + "formula": "minecraft:ore_drops", + "function": "minecraft:apply_bonus" + }, + { + "function": "minecraft:explosion_decay" + } + ], + "name": "nerospace:cindrite" + } + ] + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/cindrite_ore" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/deepslate_nerosium_ore.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/deepslate_nerosium_ore.json new file mode 100644 index 0000000..fe54edc --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/deepslate_nerosium_ore.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:alternatives", + "children": [ + { + "type": "minecraft:item", + "conditions": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "predicates": { + "minecraft:enchantments": [ + { + "enchantments": "minecraft:silk_touch", + "levels": { + "min": 1 + } + } + ] + } + } + } + ], + "name": "nerospace:deepslate_nerosium_ore" + }, + { + "type": "minecraft:item", + "functions": [ + { + "enchantment": "minecraft:fortune", + "formula": "minecraft:ore_drops", + "function": "minecraft:apply_bonus" + }, + { + "function": "minecraft:explosion_decay" + } + ], + "name": "nerospace:raw_nerosium" + } + ] + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/deepslate_nerosium_ore" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_block.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_block.json new file mode 100644 index 0000000..2ddd72c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:glacite_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/glacite_block" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_ore.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_ore.json new file mode 100644 index 0000000..7ea9ad9 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/glacite_ore.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:alternatives", + "children": [ + { + "type": "minecraft:item", + "conditions": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "predicates": { + "minecraft:enchantments": [ + { + "enchantments": "minecraft:silk_touch", + "levels": { + "min": 1 + } + } + ] + } + } + } + ], + "name": "nerospace:glacite_ore" + }, + { + "type": "minecraft:item", + "functions": [ + { + "enchantment": "minecraft:fortune", + "formula": "minecraft:ore_drops", + "function": "minecraft:apply_bonus" + }, + { + "function": "minecraft:explosion_decay" + } + ], + "name": "nerospace:glacite" + } + ] + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/glacite_ore" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_block.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_block.json new file mode 100644 index 0000000..49e2386 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:nerosium_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/nerosium_block" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_ore.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_ore.json new file mode 100644 index 0000000..c502d1a --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_ore.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:alternatives", + "children": [ + { + "type": "minecraft:item", + "conditions": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "predicates": { + "minecraft:enchantments": [ + { + "enchantments": "minecraft:silk_touch", + "levels": { + "min": 1 + } + } + ] + } + } + } + ], + "name": "nerospace:nerosium_ore" + }, + { + "type": "minecraft:item", + "functions": [ + { + "enchantment": "minecraft:fortune", + "formula": "minecraft:ore_drops", + "function": "minecraft:apply_bonus" + }, + { + "function": "minecraft:explosion_decay" + } + ], + "name": "nerospace:raw_nerosium" + } + ] + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/nerosium_ore" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_block.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_block.json new file mode 100644 index 0000000..3fcf891 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:nerosteel_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/nerosteel_block" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_ore.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_ore.json new file mode 100644 index 0000000..9420511 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosteel_ore.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:alternatives", + "children": [ + { + "type": "minecraft:item", + "conditions": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "predicates": { + "minecraft:enchantments": [ + { + "enchantments": "minecraft:silk_touch", + "levels": { + "min": 1 + } + } + ] + } + } + } + ], + "name": "nerospace:nerosteel_ore" + }, + { + "type": "minecraft:item", + "functions": [ + { + "enchantment": "minecraft:fortune", + "formula": "minecraft:ore_drops", + "function": "minecraft:apply_bonus" + }, + { + "function": "minecraft:explosion_decay" + } + ], + "name": "nerospace:raw_nerosteel" + } + ] + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/nerosteel_ore" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/raw_nerosium_block.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/raw_nerosium_block.json new file mode 100644 index 0000000..194b66c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/raw_nerosium_block.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:raw_nerosium_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/raw_nerosium_block" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/xertz_quartz_ore.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/xertz_quartz_ore.json new file mode 100644 index 0000000..6510fd4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/xertz_quartz_ore.json @@ -0,0 +1,52 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "entries": [ + { + "type": "minecraft:alternatives", + "children": [ + { + "type": "minecraft:item", + "conditions": [ + { + "condition": "minecraft:match_tool", + "predicate": { + "predicates": { + "minecraft:enchantments": [ + { + "enchantments": "minecraft:silk_touch", + "levels": { + "min": 1 + } + } + ] + } + } + } + ], + "name": "nerospace:xertz_quartz_ore" + }, + { + "type": "minecraft:item", + "functions": [ + { + "enchantment": "minecraft:fortune", + "formula": "minecraft:ore_drops", + "function": "minecraft:apply_bonus" + }, + { + "function": "minecraft:explosion_decay" + } + ], + "name": "nerospace:xertz_quartz" + } + ] + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/xertz_quartz_ore" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/cindrite_block.json b/multiloader/common/src/main/resources/data/nerospace/recipe/cindrite_block.json new file mode 100644 index 0000000..4464903 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/cindrite_block.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:gems/cindrite" + }, + "pattern": [ + "###", + "###", + "###" + ], + "result": { + "id": "nerospace:cindrite_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/glacite_block.json b/multiloader/common/src/main/resources/data/nerospace/recipe/glacite_block.json new file mode 100644 index 0000000..db1177a --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/glacite_block.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:gems/glacite" + }, + "pattern": [ + "###", + "###", + "###" + ], + "result": { + "id": "nerospace:glacite_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_block.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_block.json new file mode 100644 index 0000000..b6d007a --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_block.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:ingots/nerosium" + }, + "pattern": [ + "###", + "###", + "###" + ], + "result": { + "id": "nerospace:nerosium_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_blasting_raw_nerosium.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_blasting_raw_nerosium.json new file mode 100644 index 0000000..9d2dc9b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_blasting_raw_nerosium.json @@ -0,0 +1,10 @@ +{ + "type": "minecraft:blasting", + "category": "misc", + "cookingtime": 100, + "experience": 0.7, + "ingredient": "nerospace:raw_nerosium", + "result": { + "id": "nerospace:nerosium_ingot" + } +} diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_smelting_raw_nerosium.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_smelting_raw_nerosium.json new file mode 100644 index 0000000..00466c0 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_ingot_from_smelting_raw_nerosium.json @@ -0,0 +1,10 @@ +{ + "type": "minecraft:smelting", + "category": "misc", + "cookingtime": 200, + "experience": 0.7, + "ingredient": "nerospace:raw_nerosium", + "result": { + "id": "nerospace:nerosium_ingot" + } +} diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_block.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_block.json new file mode 100644 index 0000000..64b3bc3 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_block.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:ingots/nerosteel" + }, + "pattern": [ + "###", + "###", + "###" + ], + "result": { + "id": "nerospace:nerosteel_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_blasting_raw_nerosteel.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_blasting_raw_nerosteel.json new file mode 100644 index 0000000..6bcb642 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_blasting_raw_nerosteel.json @@ -0,0 +1,10 @@ +{ + "type": "minecraft:blasting", + "category": "misc", + "cookingtime": 100, + "experience": 0.7, + "ingredient": "nerospace:raw_nerosteel", + "result": { + "id": "nerospace:nerosteel_ingot" + } +} diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_smelting_raw_nerosteel.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_smelting_raw_nerosteel.json new file mode 100644 index 0000000..adb8045 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosteel_ingot_from_smelting_raw_nerosteel.json @@ -0,0 +1,10 @@ +{ + "type": "minecraft:smelting", + "category": "misc", + "cookingtime": 200, + "experience": 0.7, + "ingredient": "nerospace:raw_nerosteel", + "result": { + "id": "nerospace:nerosteel_ingot" + } +} diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/raw_nerosium_block.json b/multiloader/common/src/main/resources/data/nerospace/recipe/raw_nerosium_block.json new file mode 100644 index 0000000..f9dcee6 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/raw_nerosium_block.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:raw_materials/nerosium" + }, + "pattern": [ + "###", + "###", + "###" + ], + "result": { + "id": "nerospace:raw_nerosium_block" + } +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index c6859cc..592df59 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -1,13 +1,14 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.ModItems; /** - * Fabric entry point. Shared init registers content eagerly (Fabric needs no - * deferred bus). Creative-tab insertion (which needs the Fabric API - * item-group module) is wired in the next step alongside the Fabric API setup. + * Fabric entry point. Shared init registers content eagerly, then fills creative + * tabs from the common grouping via the Fabric API creative-tab module. */ public final class NerospaceFabric implements ModInitializer { @@ -15,5 +16,8 @@ public final class NerospaceFabric implements ModInitializer { public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); NerospaceCommon.init(); + ModItems.creativeTabItems().forEach((tab, items) -> + CreativeModeTabEvents.modifyOutputEvent(tab) + .register(output -> items.forEach(output::accept))); } } diff --git a/multiloader/fabric/src/main/templates/fabric.mod.json b/multiloader/fabric/src/main/templates/fabric.mod.json index c663a0a..9293afa 100644 --- a/multiloader/fabric/src/main/templates/fabric.mod.json +++ b/multiloader/fabric/src/main/templates/fabric.mod.json @@ -18,9 +18,7 @@ "depends": { "fabricloader": ">=${fabric_loader_version}", "minecraft": ">=${minecraft_version}", - "java": ">=21" - }, - "suggests": { + "java": ">=21", "fabric-api": "*" } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 7e5bcb4..b088b39 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -1,17 +1,21 @@ package za.co.neroland.nerospace.neoforge; +import java.util.List; + +import net.minecraft.world.level.ItemLike; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.ModItems; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; /** - * NeoForge entry point. Runs shared init (which builds the DeferredRegisters via - * the RegistrationProvider seam), then attaches those registers to the mod bus. - * Creative-tab insertion is added in the next step (with the Fabric side, for - * symmetry). + * NeoForge entry point. Runs shared init (building the DeferredRegisters via the + * RegistrationProvider seam), attaches them to the mod bus, then fills creative + * tabs from the common grouping. */ @Mod(NerospaceCommon.MOD_ID) public final class NerospaceNeoForge { @@ -20,5 +24,13 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NerospaceCommon.LOGGER.info("[Nerospace] NeoForge bootstrap"); NerospaceCommon.init(); NeoForgeRegistrationFactory.registerAll(modEventBus); + modEventBus.addListener(this::onBuildCreativeTabs); + } + + private void onBuildCreativeTabs(BuildCreativeModeTabContentsEvent event) { + List items = ModItems.creativeTabItems().get(event.getTabKey()); + if (items != null) { + items.forEach(event::accept); + } } } diff --git a/nerospace.code-workspace b/nerospace.code-workspace index c0f3548..03d2032 100644 --- a/nerospace.code-workspace +++ b/nerospace.code-workspace @@ -13,6 +13,7 @@ "settings": { "java.import.gradle.enabled": true, "java.configuration.updateBuildConfiguration": "automatic", - "java.compile.nullAnalysis.mode": "automatic" + "java.compile.nullAnalysis.mode": "automatic", + "java.debug.settings.onBuildFailureProceed": true } } From ba9eca0c9fb16ef8c07fd4aba4279f0a00fc7fbb Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:35:29 +0800 Subject: [PATCH 12/82] Add station, alien and meteor blocks and assets Refactor ModBlocks to use a UnaryOperator for BlockBehaviour properties and add new blocks (station_floor, station_wall, alien_* variants, alien_lamp, alien_crystal_block, meteor_rock, plus ores/blocks like glacite). Register corresponding block items in ModItems and update creative tab groupings. Add resource assets: blockstates, block/item models, textures, loot tables, language entries, and crafting recipes for station_floor and station_wall. Update minecraft block tags (mineable/pickaxe and needs_iron_tool) to include the new blocks. --- .../nerospace/registry/ModBlocks.java | 89 ++++++++++-------- .../neroland/nerospace/registry/ModItems.java | 20 +++- .../nerospace/blockstates/alien_bricks.json | 7 ++ .../blockstates/alien_crystal_block.json | 7 ++ .../nerospace/blockstates/alien_lamp.json | 7 ++ .../nerospace/blockstates/alien_pillar.json | 7 ++ .../nerospace/blockstates/alien_tile.json | 7 ++ .../blockstates/cracked_alien_bricks.json | 7 ++ .../nerospace/blockstates/meteor_rock.json | 7 ++ .../nerospace/blockstates/station_floor.json | 7 ++ .../nerospace/blockstates/station_wall.json | 7 ++ .../assets/nerospace/items/alien_bricks.json | 6 ++ .../nerospace/items/alien_crystal_block.json | 6 ++ .../assets/nerospace/items/alien_lamp.json | 6 ++ .../assets/nerospace/items/alien_pillar.json | 6 ++ .../assets/nerospace/items/alien_tile.json | 6 ++ .../nerospace/items/cracked_alien_bricks.json | 6 ++ .../assets/nerospace/items/meteor_rock.json | 6 ++ .../assets/nerospace/items/station_floor.json | 6 ++ .../assets/nerospace/items/station_wall.json | 6 ++ .../assets/nerospace/lang/en_us.json | 13 ++- .../nerospace/models/block/alien_bricks.json | 6 ++ .../models/block/alien_crystal_block.json | 6 ++ .../nerospace/models/block/alien_lamp.json | 6 ++ .../nerospace/models/block/alien_pillar.json | 6 ++ .../nerospace/models/block/alien_tile.json | 6 ++ .../models/block/cracked_alien_bricks.json | 6 ++ .../nerospace/models/block/meteor_rock.json | 6 ++ .../nerospace/models/block/station_floor.json | 6 ++ .../nerospace/models/block/station_wall.json | 6 ++ .../nerospace/textures/block/alien_bricks.png | Bin 0 -> 190 bytes .../textures/block/alien_crystal_block.png | Bin 0 -> 341 bytes .../nerospace/textures/block/alien_lamp.png | Bin 0 -> 135 bytes .../nerospace/textures/block/alien_pillar.png | Bin 0 -> 111 bytes .../nerospace/textures/block/alien_tile.png | Bin 0 -> 121 bytes .../textures/block/cracked_alien_bricks.png | Bin 0 -> 342 bytes .../nerospace/textures/block/meteor_rock.png | Bin 0 -> 515 bytes .../textures/block/station_floor.png | Bin 0 -> 316 bytes .../nerospace/textures/block/station_wall.png | Bin 0 -> 334 bytes .../tags/block/mineable/pickaxe.json | 17 ++-- .../minecraft/tags/block/needs_iron_tool.json | 14 +-- .../loot_table/blocks/alien_bricks.json | 21 +++++ .../blocks/alien_crystal_block.json | 21 +++++ .../loot_table/blocks/alien_lamp.json | 21 +++++ .../loot_table/blocks/alien_pillar.json | 21 +++++ .../loot_table/blocks/alien_tile.json | 21 +++++ .../blocks/cracked_alien_bricks.json | 21 +++++ .../loot_table/blocks/meteor_rock.json | 21 +++++ .../loot_table/blocks/station_floor.json | 21 +++++ .../loot_table/blocks/station_wall.json | 21 +++++ .../data/nerospace/recipe/station_floor.json | 16 ++++ .../data/nerospace/recipe/station_wall.json | 17 ++++ 52 files changed, 481 insertions(+), 65 deletions(-) create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_bricks.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_crystal_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_lamp.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_pillar.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_tile.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/cracked_alien_bricks.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_rock.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/station_floor.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/station_wall.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_bricks.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_crystal_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_lamp.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_pillar.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_tile.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/cracked_alien_bricks.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/meteor_rock.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/station_floor.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/station_wall.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/alien_bricks.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/alien_crystal_block.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/alien_lamp.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/alien_pillar.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/alien_tile.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/cracked_alien_bricks.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_rock.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/station_floor.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/station_wall.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_bricks.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_crystal_block.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_lamp.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_pillar.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_tile.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/cracked_alien_bricks.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_rock.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/station_floor.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/station_wall.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_bricks.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_crystal_block.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_lamp.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_pillar.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_tile.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cracked_alien_bricks.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/meteor_rock.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_floor.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_wall.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/station_floor.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/station_wall.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index a44b472..ab46f55 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -1,5 +1,7 @@ package za.co.neroland.nerospace.registry; +import java.util.function.UnaryOperator; + import net.minecraft.core.registries.Registries; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.SoundType; @@ -10,53 +12,62 @@ import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** - * Block registrations shared by both loaders (ore / material families), - * registered through {@link RegistrationProvider}. All entries are plain - * {@link Block}s with {@code requiresCorrectToolForDrops} (see the block tags in - * data/minecraft/tags/block for the mining tier). + * Block registrations shared by both loaders, registered through + * {@link RegistrationProvider}. Properties are configured via a + * {@link UnaryOperator} (mirrors the root project's style), so per-block + * variations (light level, tool requirement) stay inline. */ public final class ModBlocks { public static final RegistrationProvider BLOCKS = RegistrationProvider.get(Registries.BLOCK, NerospaceCommon.MOD_ID); - // Nerosium - public static final RegistryEntry NEROSIUM_ORE = - simple("nerosium_ore", MapColor.STONE, 3.0F, 3.0F, SoundType.STONE); - public static final RegistryEntry DEEPSLATE_NEROSIUM_ORE = - simple("deepslate_nerosium_ore", MapColor.DEEPSLATE, 4.5F, 3.0F, SoundType.DEEPSLATE); - public static final RegistryEntry NEROSIUM_BLOCK = - simple("nerosium_block", MapColor.COLOR_LIGHT_BLUE, 5.0F, 6.0F, SoundType.METAL); - public static final RegistryEntry RAW_NEROSIUM_BLOCK = - simple("raw_nerosium_block", MapColor.COLOR_LIGHT_BLUE, 5.0F, 6.0F, SoundType.METAL); - - // Greenxertz (nerosteel + xertz quartz) - public static final RegistryEntry NEROSTEEL_ORE = - simple("nerosteel_ore", MapColor.STONE, 3.0F, 3.0F, SoundType.STONE); - public static final RegistryEntry XERTZ_QUARTZ_ORE = - simple("xertz_quartz_ore", MapColor.STONE, 3.0F, 3.0F, SoundType.NETHER_ORE); - public static final RegistryEntry NEROSTEEL_BLOCK = - simple("nerosteel_block", MapColor.COLOR_GRAY, 5.0F, 6.0F, SoundType.METAL); - - // Cindara - public static final RegistryEntry CINDRITE_ORE = - simple("cindrite_ore", MapColor.COLOR_BLACK, 3.5F, 3.0F, SoundType.STONE); - public static final RegistryEntry CINDRITE_BLOCK = - simple("cindrite_block", MapColor.COLOR_RED, 5.0F, 6.0F, SoundType.METAL); + // --- Ores / materials --------------------------------------------------- + public static final RegistryEntry NEROSIUM_ORE = block("nerosium_ore", + p -> p.mapColor(MapColor.STONE).strength(3.0F, 3.0F).requiresCorrectToolForDrops().sound(SoundType.STONE)); + public static final RegistryEntry DEEPSLATE_NEROSIUM_ORE = block("deepslate_nerosium_ore", + p -> p.mapColor(MapColor.DEEPSLATE).strength(4.5F, 3.0F).requiresCorrectToolForDrops().sound(SoundType.DEEPSLATE)); + public static final RegistryEntry NEROSIUM_BLOCK = block("nerosium_block", + p -> p.mapColor(MapColor.COLOR_LIGHT_BLUE).strength(5.0F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry RAW_NEROSIUM_BLOCK = block("raw_nerosium_block", + p -> p.mapColor(MapColor.COLOR_LIGHT_BLUE).strength(5.0F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry NEROSTEEL_ORE = block("nerosteel_ore", + p -> p.mapColor(MapColor.STONE).strength(3.0F, 3.0F).requiresCorrectToolForDrops().sound(SoundType.STONE)); + public static final RegistryEntry XERTZ_QUARTZ_ORE = block("xertz_quartz_ore", + p -> p.mapColor(MapColor.STONE).strength(3.0F, 3.0F).requiresCorrectToolForDrops().sound(SoundType.NETHER_ORE)); + public static final RegistryEntry NEROSTEEL_BLOCK = block("nerosteel_block", + p -> p.mapColor(MapColor.COLOR_GRAY).strength(5.0F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry CINDRITE_ORE = block("cindrite_ore", + p -> p.mapColor(MapColor.COLOR_BLACK).strength(3.5F, 3.0F).requiresCorrectToolForDrops().sound(SoundType.STONE)); + public static final RegistryEntry CINDRITE_BLOCK = block("cindrite_block", + p -> p.mapColor(MapColor.COLOR_RED).strength(5.0F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry GLACITE_ORE = block("glacite_ore", + p -> p.mapColor(MapColor.ICE).strength(3.5F, 3.0F).requiresCorrectToolForDrops().sound(SoundType.STONE)); + public static final RegistryEntry GLACITE_BLOCK = block("glacite_block", + p -> p.mapColor(MapColor.COLOR_LIGHT_BLUE).strength(5.0F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); - // Glacira - public static final RegistryEntry GLACITE_ORE = - simple("glacite_ore", MapColor.ICE, 3.5F, 3.0F, SoundType.STONE); - public static final RegistryEntry GLACITE_BLOCK = - simple("glacite_block", MapColor.COLOR_LIGHT_BLUE, 5.0F, 6.0F, SoundType.METAL); + // --- Station + alien decorative + meteor -------------------------------- + public static final RegistryEntry STATION_FLOOR = block("station_floor", + p -> p.mapColor(MapColor.METAL).strength(4.0F, 12.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry STATION_WALL = block("station_wall", + p -> p.mapColor(MapColor.COLOR_LIGHT_GRAY).strength(4.0F, 12.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry ALIEN_BRICKS = block("alien_bricks", + p -> p.mapColor(MapColor.COLOR_GREEN).strength(1.5F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry CRACKED_ALIEN_BRICKS = block("cracked_alien_bricks", + p -> p.mapColor(MapColor.COLOR_GREEN).strength(1.5F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry ALIEN_TILE = block("alien_tile", + p -> p.mapColor(MapColor.COLOR_GREEN).strength(1.5F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry ALIEN_PILLAR = block("alien_pillar", + p -> p.mapColor(MapColor.COLOR_GREEN).strength(1.5F, 6.0F).requiresCorrectToolForDrops().sound(SoundType.METAL)); + public static final RegistryEntry ALIEN_LAMP = block("alien_lamp", + p -> p.mapColor(MapColor.COLOR_GREEN).strength(1.5F, 6.0F).lightLevel(s -> 15).sound(SoundType.METAL)); + public static final RegistryEntry ALIEN_CRYSTAL_BLOCK = block("alien_crystal_block", + p -> p.mapColor(MapColor.EMERALD).strength(1.5F, 6.0F).lightLevel(s -> 12).sound(SoundType.AMETHYST)); + public static final RegistryEntry METEOR_ROCK = block("meteor_rock", + p -> p.mapColor(MapColor.COLOR_BLACK).strength(3.0F, 4.0F).requiresCorrectToolForDrops().lightLevel(s -> 3).sound(SoundType.STONE)); - private static RegistryEntry simple(String name, MapColor color, float hardness, float resistance, SoundType sound) { - return BLOCKS.register(name, key -> new Block(BlockBehaviour.Properties.of() - .setId(key) - .mapColor(color) - .strength(hardness, resistance) - .requiresCorrectToolForDrops() - .sound(sound))); + private static RegistryEntry block(String name, UnaryOperator props) { + return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } private ModBlocks() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 35cc504..47fd5f9 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -24,7 +24,7 @@ public final class ModItems { public static final RegistrationProvider ITEMS = RegistrationProvider.get(Registries.ITEM, NerospaceCommon.MOD_ID); - // Block items + // Ore / material block items public static final RegistryEntry NEROSIUM_ORE_ITEM = blockItem("nerosium_ore", ModBlocks.NEROSIUM_ORE); public static final RegistryEntry DEEPSLATE_NEROSIUM_ORE_ITEM = blockItem("deepslate_nerosium_ore", ModBlocks.DEEPSLATE_NEROSIUM_ORE); public static final RegistryEntry NEROSIUM_BLOCK_ITEM = blockItem("nerosium_block", ModBlocks.NEROSIUM_BLOCK); @@ -37,6 +37,17 @@ public final class ModItems { public static final RegistryEntry GLACITE_ORE_ITEM = blockItem("glacite_ore", ModBlocks.GLACITE_ORE); public static final RegistryEntry GLACITE_BLOCK_ITEM = blockItem("glacite_block", ModBlocks.GLACITE_BLOCK); + // Station / alien decorative / meteor block items + public static final RegistryEntry STATION_FLOOR_ITEM = blockItem("station_floor", ModBlocks.STATION_FLOOR); + public static final RegistryEntry STATION_WALL_ITEM = blockItem("station_wall", ModBlocks.STATION_WALL); + public static final RegistryEntry ALIEN_BRICKS_ITEM = blockItem("alien_bricks", ModBlocks.ALIEN_BRICKS); + public static final RegistryEntry CRACKED_ALIEN_BRICKS_ITEM = blockItem("cracked_alien_bricks", ModBlocks.CRACKED_ALIEN_BRICKS); + public static final RegistryEntry ALIEN_TILE_ITEM = blockItem("alien_tile", ModBlocks.ALIEN_TILE); + public static final RegistryEntry ALIEN_PILLAR_ITEM = blockItem("alien_pillar", ModBlocks.ALIEN_PILLAR); + public static final RegistryEntry ALIEN_LAMP_ITEM = blockItem("alien_lamp", ModBlocks.ALIEN_LAMP); + public static final RegistryEntry ALIEN_CRYSTAL_BLOCK_ITEM = blockItem("alien_crystal_block", ModBlocks.ALIEN_CRYSTAL_BLOCK); + public static final RegistryEntry METEOR_ROCK_ITEM = blockItem("meteor_rock", ModBlocks.METEOR_ROCK); + // Materials public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); public static final RegistryEntry NEROSIUM_INGOT = item("nerosium_ingot"); @@ -61,11 +72,14 @@ public static Map, List> creativeTabItems List.of( NEROSIUM_ORE_ITEM.get(), DEEPSLATE_NEROSIUM_ORE_ITEM.get(), NEROSTEEL_ORE_ITEM.get(), XERTZ_QUARTZ_ORE_ITEM.get(), - CINDRITE_ORE_ITEM.get(), GLACITE_ORE_ITEM.get()), + CINDRITE_ORE_ITEM.get(), GLACITE_ORE_ITEM.get(), METEOR_ROCK_ITEM.get()), CreativeModeTabs.BUILDING_BLOCKS, List.of( NEROSIUM_BLOCK_ITEM.get(), RAW_NEROSIUM_BLOCK_ITEM.get(), - NEROSTEEL_BLOCK_ITEM.get(), CINDRITE_BLOCK_ITEM.get(), GLACITE_BLOCK_ITEM.get()), + NEROSTEEL_BLOCK_ITEM.get(), CINDRITE_BLOCK_ITEM.get(), GLACITE_BLOCK_ITEM.get(), + STATION_FLOOR_ITEM.get(), STATION_WALL_ITEM.get(), + ALIEN_BRICKS_ITEM.get(), CRACKED_ALIEN_BRICKS_ITEM.get(), ALIEN_TILE_ITEM.get(), + ALIEN_PILLAR_ITEM.get(), ALIEN_LAMP_ITEM.get(), ALIEN_CRYSTAL_BLOCK_ITEM.get()), CreativeModeTabs.INGREDIENTS, List.of( RAW_NEROSIUM.get(), NEROSIUM_INGOT.get(), diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_bricks.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_bricks.json new file mode 100644 index 0000000..22a04e0 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_bricks.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/alien_bricks" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_crystal_block.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_crystal_block.json new file mode 100644 index 0000000..fa40446 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_crystal_block.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/alien_crystal_block" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_lamp.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_lamp.json new file mode 100644 index 0000000..a4568ac --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_lamp.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/alien_lamp" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_pillar.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_pillar.json new file mode 100644 index 0000000..cae7a77 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_pillar.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/alien_pillar" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_tile.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_tile.json new file mode 100644 index 0000000..589454e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/alien_tile.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/alien_tile" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/cracked_alien_bricks.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/cracked_alien_bricks.json new file mode 100644 index 0000000..10f7582 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/cracked_alien_bricks.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/cracked_alien_bricks" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_rock.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_rock.json new file mode 100644 index 0000000..d27dcf2 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_rock.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/meteor_rock" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_floor.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_floor.json new file mode 100644 index 0000000..fa3313e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_floor.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/station_floor" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_wall.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_wall.json new file mode 100644 index 0000000..3323c87 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_wall.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/station_wall" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_bricks.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_bricks.json new file mode 100644 index 0000000..3cb1b91 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_bricks.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/alien_bricks" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_crystal_block.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_crystal_block.json new file mode 100644 index 0000000..44b81a1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_crystal_block.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/alien_crystal_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_lamp.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_lamp.json new file mode 100644 index 0000000..6df16ec --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_lamp.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/alien_lamp" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_pillar.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_pillar.json new file mode 100644 index 0000000..8437383 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_pillar.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/alien_pillar" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_tile.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_tile.json new file mode 100644 index 0000000..b9cca54 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_tile.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/alien_tile" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/cracked_alien_bricks.json b/multiloader/common/src/main/resources/assets/nerospace/items/cracked_alien_bricks.json new file mode 100644 index 0000000..6b48dec --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/cracked_alien_bricks.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/cracked_alien_bricks" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/meteor_rock.json b/multiloader/common/src/main/resources/assets/nerospace/items/meteor_rock.json new file mode 100644 index 0000000..fb59abb --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/meteor_rock.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/meteor_rock" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/station_floor.json b/multiloader/common/src/main/resources/assets/nerospace/items/station_floor.json new file mode 100644 index 0000000..a4dd876 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/station_floor.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/station_floor" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/station_wall.json b/multiloader/common/src/main/resources/assets/nerospace/items/station_wall.json new file mode 100644 index 0000000..20f00b6 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/station_wall.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/station_wall" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 8fc8426..2085222 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -16,5 +16,14 @@ "item.nerospace.nerosteel_ingot": "Nerosteel Ingot", "item.nerospace.xertz_quartz": "Xertz Quartz", "item.nerospace.cindrite": "Cindrite", - "item.nerospace.glacite": "Glacite" -} + "item.nerospace.glacite": "Glacite", + "block.nerospace.station_floor": "Station Floor", + "block.nerospace.station_wall": "Station Wall", + "block.nerospace.alien_bricks": "Alien Bricks", + "block.nerospace.cracked_alien_bricks": "Cracked Alien Bricks", + "block.nerospace.alien_tile": "Alien Tile", + "block.nerospace.alien_pillar": "Alien Pillar", + "block.nerospace.alien_lamp": "Alien Lamp", + "block.nerospace.alien_crystal_block": "Alien Crystal Block", + "block.nerospace.meteor_rock": "Meteor Rock" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_bricks.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_bricks.json new file mode 100644 index 0000000..5216746 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_bricks.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/alien_bricks" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_crystal_block.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_crystal_block.json new file mode 100644 index 0000000..fda6c65 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_crystal_block.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/alien_crystal_block" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_lamp.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_lamp.json new file mode 100644 index 0000000..fc53b5b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_lamp.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/alien_lamp" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_pillar.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_pillar.json new file mode 100644 index 0000000..be791e6 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_pillar.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/alien_pillar" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_tile.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_tile.json new file mode 100644 index 0000000..5170045 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/alien_tile.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/alien_tile" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/cracked_alien_bricks.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/cracked_alien_bricks.json new file mode 100644 index 0000000..dc528f3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/cracked_alien_bricks.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/cracked_alien_bricks" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_rock.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_rock.json new file mode 100644 index 0000000..a9bbcc8 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_rock.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/meteor_rock" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/station_floor.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/station_floor.json new file mode 100644 index 0000000..d6ce8bb --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/station_floor.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/station_floor" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/station_wall.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/station_wall.json new file mode 100644 index 0000000..cd9c2dd --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/station_wall.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/station_wall" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_bricks.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_bricks.png new file mode 100644 index 0000000000000000000000000000000000000000..6ebdefdef5f718fa5d28be3bbad92398e115a6e7 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`t)4E9Ar*7hUUuYaP!MQ+XrFmi zFsjX^{RPX#N^9?qEpIqFlAta2?P=44$rjF6*2UngC7)Ph0>1 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_crystal_block.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_crystal_block.png new file mode 100644 index 0000000000000000000000000000000000000000..3a51668304960457b4073f0d52d7c6c64ca31ce8 GIT binary patch literal 341 zcmV-b0jmCqP)$jmYWrKPm{VMLU7D{ST|U_CZeeqU1C zm<+&&@?$B1V3psOq?GS;Qi=leRK|9R$vlP2q!pBs%7I@btjCr+v4FrVP%r>r7x=)K z10}2I!XHO25>&r_vOo3!0BZG}QgnGr-^*q-FQN%0SFLSjNf z0!PFo*J;mq4KGW~(AYS)v61mAgLED5qKr9gGCwPt54OcOEA(DxJtATBs_EfdrWuMy jE?Dp?h#CaVn#*|b=$ykDFP}aHn#|zo>gTe~DWM4f4Jt5o literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_pillar.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_pillar.png new file mode 100644 index 0000000000000000000000000000000000000000..45903db543b4e845ae2b539b24215c35448c30d8 GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`W}YsNAr*6yw|xBg>%0SFLSjNf z0tW|o(vG5+T+Bdlc&_wzk4MIVNxxSxE@tbIYGCM@l9tZEa9PsyVCj)qpa~3~u6{1- HoD!M%0SFLSjNf z!h=sijodZ2%i0}Xoq5mgFMcc*SE=Tev%HJ3CxnwZ=ueNHeaxW*6Iq_GB@7Iv9=f&y TS2fIlW-)lW`njxgN@xNAVZtUO literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/cracked_alien_bricks.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/cracked_alien_bricks.png new file mode 100644 index 0000000000000000000000000000000000000000..8a08963c42b65163263ab34e6e89d25474b74e9b GIT binary patch literal 342 zcmV-c0jd6pP)B1j|R8M1UGBA&&A%vnT+tUSli3?i;PLY7VzrW)<;WKx?hw0_=u zRTX{I)suhNVhrzjZeRJP0Cb)aR~nsX=saUF#=i0&Ag(lJ+t~QBZT13dx{=HZ@yoUW zpy+Cgpy+Du*BfrU0!U`X*biQzWL7N3Sifu=#(p@(L~lBM3OB&=d!>cgrEHrW_7Bg= zJS^um-MC+GNQ`ujD{WCL7n0Z!_Ayx~i!Mq83-KTO!Q<>Uh-433Zhvt<`WL{?KZIg3 zkJCPgUSmH{boE}qgl6=LHeB1g42POTWo ziXlYJ4ns5aKe!XSRks^RsiwMK)vNdF;@#W#55v$C;|M@X6J=T9TtVAz;rlJzEIEr~ z&ik4SpzC&cUjwjMTmcY51AzVX8SiVX&9OEor3t^j#oC;<-IBHK7Gq|#?e>sdEpe_m zfNieLx^72|gj#!)HTtOyN1$;`gEGxPXPo7H?VKWRpnOxkDo4wHFth8vqnX_12snYw+9Ajp5-Hu`C zxxT-nZMTHiZ(Si3|ul->H&IA3g&8tih^;$!zeryS7oSgDDoUVKs_mv6|(OL1+HvMTJ1`^G4ng# zI0FFGIzd$d0PYSEftf*7ak*3+k0suWyF0GeFMJSb96uXmRfU-~f9@W+yazz7Qxq^r zR%QkfiO#i7t%Tf-Nx3_V3Bk4lbLB46=wl~E30kb?wS0p(rL9n0A)YIlhL zSQF2@KRGPR{lObi0E94eI(?)Q!21cMDga_!lg{g`?KsM%wO#r+gqfx}r(AT5D*(eV z5W)=bd?19G+wBwYr%e~{r`@AcD{)ws`(qrxUgCFde0rdDO^ho|b58uCb3bx^K`5k1 zsVX6eVUmlHf|ut-p5$F5@<}8@m{XItJvaiCWr;EE=FGL$HCop@4L@s?v1DAOsx+ct ziOGU1wZ&iT`9@+~6LbEbi2ffBjA?OhOl-pX+ZSV6w61Y(+ Date: Fri, 19 Jun 2026 10:44:31 +0800 Subject: [PATCH 13/82] Add oxygen suits, pickaxe and item registrations Register new items and assets for oxygen suit variants and a nerosium pickaxe. ModItems.java: add tool/armor materials, equipment asset keys, armor/tool item registrations, overloaded item helper, tag and equipment-asset helpers, and include items in creative tabs. Add resource files (equipment JSON, item models, textures, recipes) and language entries for the new items. --- .../neroland/nerospace/registry/ModItems.java | 102 +++++++++++++++--- .../nerospace/equipment/oxygen_suit.json | 14 +++ .../nerospace/equipment/oxygen_suit_cold.json | 14 +++ .../nerospace/equipment/oxygen_suit_heat.json | 14 +++ .../nerospace/equipment/oxygen_suit_t2.json | 14 +++ .../nerospace/items/nerosium_pickaxe.json | 6 ++ .../nerospace/items/oxygen_suit_boots.json | 6 ++ .../items/oxygen_suit_chestplate.json | 6 ++ .../items/oxygen_suit_cold_boots.json | 6 ++ .../items/oxygen_suit_cold_chestplate.json | 6 ++ .../items/oxygen_suit_cold_helmet.json | 6 ++ .../items/oxygen_suit_cold_leggings.json | 6 ++ .../items/oxygen_suit_heat_boots.json | 6 ++ .../items/oxygen_suit_heat_chestplate.json | 6 ++ .../items/oxygen_suit_heat_helmet.json | 6 ++ .../items/oxygen_suit_heat_leggings.json | 6 ++ .../nerospace/items/oxygen_suit_helmet.json | 6 ++ .../nerospace/items/oxygen_suit_leggings.json | 6 ++ .../nerospace/items/oxygen_suit_t2_boots.json | 6 ++ .../items/oxygen_suit_t2_chestplate.json | 6 ++ .../items/oxygen_suit_t2_helmet.json | 6 ++ .../items/oxygen_suit_t2_leggings.json | 6 ++ .../assets/nerospace/lang/en_us.json | 19 +++- .../models/item/nerosium_pickaxe.json | 6 ++ .../models/item/oxygen_suit_boots.json | 6 ++ .../models/item/oxygen_suit_chestplate.json | 6 ++ .../models/item/oxygen_suit_cold_boots.json | 6 ++ .../item/oxygen_suit_cold_chestplate.json | 6 ++ .../models/item/oxygen_suit_cold_helmet.json | 6 ++ .../item/oxygen_suit_cold_leggings.json | 6 ++ .../models/item/oxygen_suit_heat_boots.json | 6 ++ .../item/oxygen_suit_heat_chestplate.json | 6 ++ .../models/item/oxygen_suit_heat_helmet.json | 6 ++ .../item/oxygen_suit_heat_leggings.json | 6 ++ .../models/item/oxygen_suit_helmet.json | 6 ++ .../models/item/oxygen_suit_leggings.json | 6 ++ .../models/item/oxygen_suit_t2_boots.json | 6 ++ .../item/oxygen_suit_t2_chestplate.json | 6 ++ .../models/item/oxygen_suit_t2_helmet.json | 6 ++ .../models/item/oxygen_suit_t2_leggings.json | 6 ++ .../entity/equipment/humanoid/oxygen_suit.png | Bin 0 -> 1209 bytes .../equipment/humanoid/oxygen_suit_cold.png | Bin 0 -> 916 bytes .../equipment/humanoid/oxygen_suit_heat.png | Bin 0 -> 910 bytes .../equipment/humanoid/oxygen_suit_t2.png | Bin 0 -> 949 bytes .../humanoid_leggings/oxygen_suit.png | Bin 0 -> 681 bytes .../humanoid_leggings/oxygen_suit_cold.png | Bin 0 -> 526 bytes .../humanoid_leggings/oxygen_suit_heat.png | Bin 0 -> 548 bytes .../humanoid_leggings/oxygen_suit_t2.png | Bin 0 -> 557 bytes .../textures/item/nerosium_pickaxe.png | Bin 0 -> 181 bytes .../textures/item/oxygen_suit_boots.png | Bin 0 -> 145 bytes .../textures/item/oxygen_suit_chestplate.png | Bin 0 -> 161 bytes .../textures/item/oxygen_suit_cold_boots.png | Bin 0 -> 143 bytes .../item/oxygen_suit_cold_chestplate.png | Bin 0 -> 138 bytes .../textures/item/oxygen_suit_cold_helmet.png | Bin 0 -> 165 bytes .../item/oxygen_suit_cold_leggings.png | Bin 0 -> 133 bytes .../textures/item/oxygen_suit_heat_boots.png | Bin 0 -> 134 bytes .../item/oxygen_suit_heat_chestplate.png | Bin 0 -> 144 bytes .../textures/item/oxygen_suit_heat_helmet.png | Bin 0 -> 168 bytes .../item/oxygen_suit_heat_leggings.png | Bin 0 -> 138 bytes .../textures/item/oxygen_suit_helmet.png | Bin 0 -> 172 bytes .../textures/item/oxygen_suit_leggings.png | Bin 0 -> 145 bytes .../textures/item/oxygen_suit_t2_boots.png | Bin 0 -> 136 bytes .../item/oxygen_suit_t2_chestplate.png | Bin 0 -> 153 bytes .../textures/item/oxygen_suit_t2_helmet.png | Bin 0 -> 168 bytes .../textures/item/oxygen_suit_t2_leggings.png | Bin 0 -> 142 bytes .../nerospace/recipe/nerosium_pickaxe.json | 16 +++ .../nerospace/recipe/oxygen_suit_boots.json | 14 +++ .../recipe/oxygen_suit_cold_boots.json | 16 +++ .../recipe/oxygen_suit_cold_chestplate.json | 16 +++ .../recipe/oxygen_suit_cold_helmet.json | 16 +++ .../recipe/oxygen_suit_cold_leggings.json | 16 +++ .../recipe/oxygen_suit_heat_boots.json | 16 +++ .../recipe/oxygen_suit_heat_chestplate.json | 16 +++ .../recipe/oxygen_suit_heat_helmet.json | 16 +++ .../recipe/oxygen_suit_heat_leggings.json | 16 +++ .../nerospace/recipe/oxygen_suit_helmet.json | 15 +++ .../recipe/oxygen_suit_leggings.json | 15 +++ .../recipe/oxygen_suit_t2_boots.json | 16 +++ .../recipe/oxygen_suit_t2_chestplate.json | 16 +++ .../recipe/oxygen_suit_t2_helmet.json | 16 +++ .../recipe/oxygen_suit_t2_leggings.json | 16 +++ 81 files changed, 619 insertions(+), 14 deletions(-) create mode 100644 multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_cold.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_heat.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_t2.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosium_pickaxe.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_pickaxe.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_boots.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_chestplate.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_helmet.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_leggings.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_cold.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_heat.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_t2.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_cold.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_heat.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_t2.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_pickaxe.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_boots.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_chestplate.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_boots.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_chestplate.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_helmet.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_leggings.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_boots.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_chestplate.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_helmet.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_leggings.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_helmet.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_leggings.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_boots.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_chestplate.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_helmet.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_leggings.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_pickaxe.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_boots.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_boots.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_chestplate.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_helmet.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_leggings.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_boots.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_chestplate.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_helmet.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_leggings.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_helmet.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_leggings.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_boots.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_chestplate.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_helmet.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_leggings.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 47fd5f9..64a6b5a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -2,13 +2,23 @@ import java.util.List; import java.util.Map; +import java.util.function.UnaryOperator; import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.TagKey; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.Item; +import net.minecraft.world.item.ToolMaterial; +import net.minecraft.world.item.equipment.ArmorMaterial; +import net.minecraft.world.item.equipment.ArmorType; +import net.minecraft.world.item.equipment.EquipmentAsset; +import net.minecraft.world.item.equipment.EquipmentAssets; import net.minecraft.world.level.ItemLike; import net.minecraft.world.level.block.Block; @@ -17,14 +27,16 @@ /** * Item registrations shared by both loaders, plus the creative-tab grouping the - * loader entry points consume (tab placement defined once, in common). + * loader entry points consume. Tools/armor use vanilla {@code Item.Properties} + * delegates ({@code pickaxe(...)}, {@code humanoidArmor(...)}) — confirmed + * present on the vanilla classpath, so they compile on both loaders. */ public final class ModItems { public static final RegistrationProvider ITEMS = RegistrationProvider.get(Registries.ITEM, NerospaceCommon.MOD_ID); - // Ore / material block items + // --- Block items -------------------------------------------------------- public static final RegistryEntry NEROSIUM_ORE_ITEM = blockItem("nerosium_ore", ModBlocks.NEROSIUM_ORE); public static final RegistryEntry DEEPSLATE_NEROSIUM_ORE_ITEM = blockItem("deepslate_nerosium_ore", ModBlocks.DEEPSLATE_NEROSIUM_ORE); public static final RegistryEntry NEROSIUM_BLOCK_ITEM = blockItem("nerosium_block", ModBlocks.NEROSIUM_BLOCK); @@ -36,8 +48,6 @@ public final class ModItems { public static final RegistryEntry CINDRITE_BLOCK_ITEM = blockItem("cindrite_block", ModBlocks.CINDRITE_BLOCK); public static final RegistryEntry GLACITE_ORE_ITEM = blockItem("glacite_ore", ModBlocks.GLACITE_ORE); public static final RegistryEntry GLACITE_BLOCK_ITEM = blockItem("glacite_block", ModBlocks.GLACITE_BLOCK); - - // Station / alien decorative / meteor block items public static final RegistryEntry STATION_FLOOR_ITEM = blockItem("station_floor", ModBlocks.STATION_FLOOR); public static final RegistryEntry STATION_WALL_ITEM = blockItem("station_wall", ModBlocks.STATION_WALL); public static final RegistryEntry ALIEN_BRICKS_ITEM = blockItem("alien_bricks", ModBlocks.ALIEN_BRICKS); @@ -48,7 +58,7 @@ public final class ModItems { public static final RegistryEntry ALIEN_CRYSTAL_BLOCK_ITEM = blockItem("alien_crystal_block", ModBlocks.ALIEN_CRYSTAL_BLOCK); public static final RegistryEntry METEOR_ROCK_ITEM = blockItem("meteor_rock", ModBlocks.METEOR_ROCK); - // Materials + // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); public static final RegistryEntry NEROSIUM_INGOT = item("nerosium_ingot"); public static final RegistryEntry RAW_NEROSTEEL = item("raw_nerosteel"); @@ -57,34 +67,100 @@ public final class ModItems { public static final RegistryEntry CINDRITE = item("cindrite"); public static final RegistryEntry GLACITE = item("glacite"); + // --- Tool + armor materials -------------------------------------------- + public static final ToolMaterial NEROSIUM_TOOL_MATERIAL = new ToolMaterial( + BlockTags.INCORRECT_FOR_IRON_TOOL, 350, 7.0F, 2.5F, 15, cTag("ingots/nerosium")); + + public static final ResourceKey OXYGEN_SUIT_ASSET = equipAsset("oxygen_suit"); + public static final ResourceKey OXYGEN_SUIT_T2_ASSET = equipAsset("oxygen_suit_t2"); + public static final ResourceKey OXYGEN_SUIT_HEAT_ASSET = equipAsset("oxygen_suit_heat"); + public static final ResourceKey OXYGEN_SUIT_COLD_ASSET = equipAsset("oxygen_suit_cold"); + + private static final Map T1_DEFENSE = + Map.of(ArmorType.HELMET, 3, ArmorType.CHESTPLATE, 7, ArmorType.LEGGINGS, 6, ArmorType.BOOTS, 3); + private static final Map T2_DEFENSE = + Map.of(ArmorType.HELMET, 4, ArmorType.CHESTPLATE, 8, ArmorType.LEGGINGS, 6, ArmorType.BOOTS, 4); + + public static final ArmorMaterial OXYGEN_SUIT_MATERIAL = new ArmorMaterial( + 28, T1_DEFENSE, 12, SoundEvents.ARMOR_EQUIP_IRON, 1.5F, 0.0F, cTag("ingots/nerosteel"), OXYGEN_SUIT_ASSET); + public static final ArmorMaterial OXYGEN_SUIT_T2_MATERIAL = new ArmorMaterial( + 36, T2_DEFENSE, 14, SoundEvents.ARMOR_EQUIP_NETHERITE, 2.0F, 0.0F, cTag("gems/cindrite"), OXYGEN_SUIT_T2_ASSET); + public static final ArmorMaterial OXYGEN_SUIT_HEAT_MATERIAL = new ArmorMaterial( + 36, T2_DEFENSE, 14, SoundEvents.ARMOR_EQUIP_NETHERITE, 2.0F, 0.0F, cTag("gems/cindrite"), OXYGEN_SUIT_HEAT_ASSET); + public static final ArmorMaterial OXYGEN_SUIT_COLD_MATERIAL = new ArmorMaterial( + 36, T2_DEFENSE, 14, SoundEvents.ARMOR_EQUIP_NETHERITE, 2.0F, 0.0F, cTag("gems/glacite"), OXYGEN_SUIT_COLD_ASSET); + + // --- Tools + armor items ----------------------------------------------- + public static final RegistryEntry NEROSIUM_PICKAXE = + item("nerosium_pickaxe", p -> p.pickaxe(NEROSIUM_TOOL_MATERIAL, 1.0F, -2.8F)); + + public static final RegistryEntry OXYGEN_SUIT_HELMET = armor("oxygen_suit_helmet", OXYGEN_SUIT_MATERIAL, ArmorType.HELMET); + public static final RegistryEntry OXYGEN_SUIT_CHESTPLATE = armor("oxygen_suit_chestplate", OXYGEN_SUIT_MATERIAL, ArmorType.CHESTPLATE); + public static final RegistryEntry OXYGEN_SUIT_LEGGINGS = armor("oxygen_suit_leggings", OXYGEN_SUIT_MATERIAL, ArmorType.LEGGINGS); + public static final RegistryEntry OXYGEN_SUIT_BOOTS = armor("oxygen_suit_boots", OXYGEN_SUIT_MATERIAL, ArmorType.BOOTS); + public static final RegistryEntry OXYGEN_SUIT_T2_HELMET = armor("oxygen_suit_t2_helmet", OXYGEN_SUIT_T2_MATERIAL, ArmorType.HELMET); + public static final RegistryEntry OXYGEN_SUIT_T2_CHESTPLATE = armor("oxygen_suit_t2_chestplate", OXYGEN_SUIT_T2_MATERIAL, ArmorType.CHESTPLATE); + public static final RegistryEntry OXYGEN_SUIT_T2_LEGGINGS = armor("oxygen_suit_t2_leggings", OXYGEN_SUIT_T2_MATERIAL, ArmorType.LEGGINGS); + public static final RegistryEntry OXYGEN_SUIT_T2_BOOTS = armor("oxygen_suit_t2_boots", OXYGEN_SUIT_T2_MATERIAL, ArmorType.BOOTS); + public static final RegistryEntry OXYGEN_SUIT_HEAT_HELMET = armor("oxygen_suit_heat_helmet", OXYGEN_SUIT_HEAT_MATERIAL, ArmorType.HELMET); + public static final RegistryEntry OXYGEN_SUIT_HEAT_CHESTPLATE = armor("oxygen_suit_heat_chestplate", OXYGEN_SUIT_HEAT_MATERIAL, ArmorType.CHESTPLATE); + public static final RegistryEntry OXYGEN_SUIT_HEAT_LEGGINGS = armor("oxygen_suit_heat_leggings", OXYGEN_SUIT_HEAT_MATERIAL, ArmorType.LEGGINGS); + public static final RegistryEntry OXYGEN_SUIT_HEAT_BOOTS = armor("oxygen_suit_heat_boots", OXYGEN_SUIT_HEAT_MATERIAL, ArmorType.BOOTS); + public static final RegistryEntry OXYGEN_SUIT_COLD_HELMET = armor("oxygen_suit_cold_helmet", OXYGEN_SUIT_COLD_MATERIAL, ArmorType.HELMET); + public static final RegistryEntry OXYGEN_SUIT_COLD_CHESTPLATE = armor("oxygen_suit_cold_chestplate", OXYGEN_SUIT_COLD_MATERIAL, ArmorType.CHESTPLATE); + public static final RegistryEntry OXYGEN_SUIT_COLD_LEGGINGS = armor("oxygen_suit_cold_leggings", OXYGEN_SUIT_COLD_MATERIAL, ArmorType.LEGGINGS); + public static final RegistryEntry OXYGEN_SUIT_COLD_BOOTS = armor("oxygen_suit_cold_boots", OXYGEN_SUIT_COLD_MATERIAL, ArmorType.BOOTS); + + // --- helpers ------------------------------------------------------------ private static RegistryEntry item(String name) { - return ITEMS.register(name, key -> new Item(new Item.Properties().setId(key))); + return item(name, p -> p); + } + + private static RegistryEntry item(String name, UnaryOperator cfg) { + return ITEMS.register(name, key -> new Item(cfg.apply(new Item.Properties().setId(key)))); + } + + private static RegistryEntry armor(String name, ArmorMaterial material, ArmorType type) { + return item(name, p -> p.humanoidArmor(material, type)); } private static RegistryEntry blockItem(String name, RegistryEntry block) { return ITEMS.register(name, key -> new BlockItem(block.get(), new Item.Properties().setId(key))); } + private static TagKey cTag(String path) { + return TagKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath("c", path)); + } + + private static ResourceKey equipAsset(String name) { + return ResourceKey.create(EquipmentAssets.ROOT_ID, Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, name)); + } + /** Items grouped by the vanilla creative tab they should appear in. */ public static Map, List> creativeTabItems() { return Map.of( CreativeModeTabs.NATURAL_BLOCKS, - List.of( - NEROSIUM_ORE_ITEM.get(), DEEPSLATE_NEROSIUM_ORE_ITEM.get(), + List.of(NEROSIUM_ORE_ITEM.get(), DEEPSLATE_NEROSIUM_ORE_ITEM.get(), NEROSTEEL_ORE_ITEM.get(), XERTZ_QUARTZ_ORE_ITEM.get(), CINDRITE_ORE_ITEM.get(), GLACITE_ORE_ITEM.get(), METEOR_ROCK_ITEM.get()), CreativeModeTabs.BUILDING_BLOCKS, - List.of( - NEROSIUM_BLOCK_ITEM.get(), RAW_NEROSIUM_BLOCK_ITEM.get(), + List.of(NEROSIUM_BLOCK_ITEM.get(), RAW_NEROSIUM_BLOCK_ITEM.get(), NEROSTEEL_BLOCK_ITEM.get(), CINDRITE_BLOCK_ITEM.get(), GLACITE_BLOCK_ITEM.get(), STATION_FLOOR_ITEM.get(), STATION_WALL_ITEM.get(), ALIEN_BRICKS_ITEM.get(), CRACKED_ALIEN_BRICKS_ITEM.get(), ALIEN_TILE_ITEM.get(), ALIEN_PILLAR_ITEM.get(), ALIEN_LAMP_ITEM.get(), ALIEN_CRYSTAL_BLOCK_ITEM.get()), CreativeModeTabs.INGREDIENTS, - List.of( - RAW_NEROSIUM.get(), NEROSIUM_INGOT.get(), + List.of(RAW_NEROSIUM.get(), NEROSIUM_INGOT.get(), RAW_NEROSTEEL.get(), NEROSTEEL_INGOT.get(), - XERTZ_QUARTZ.get(), CINDRITE.get(), GLACITE.get())); + XERTZ_QUARTZ.get(), CINDRITE.get(), GLACITE.get()), + CreativeModeTabs.TOOLS_AND_UTILITIES, + List.of(NEROSIUM_PICKAXE.get()), + CreativeModeTabs.COMBAT, + List.of( + OXYGEN_SUIT_HELMET.get(), OXYGEN_SUIT_CHESTPLATE.get(), OXYGEN_SUIT_LEGGINGS.get(), OXYGEN_SUIT_BOOTS.get(), + OXYGEN_SUIT_T2_HELMET.get(), OXYGEN_SUIT_T2_CHESTPLATE.get(), OXYGEN_SUIT_T2_LEGGINGS.get(), OXYGEN_SUIT_T2_BOOTS.get(), + OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), + OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get())); } private ModItems() { diff --git a/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit.json b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit.json new file mode 100644 index 0000000..d313428 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit.json @@ -0,0 +1,14 @@ +{ + "layers": { + "humanoid": [ + { + "texture": "nerospace:oxygen_suit" + } + ], + "humanoid_leggings": [ + { + "texture": "nerospace:oxygen_suit" + } + ] + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_cold.json b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_cold.json new file mode 100644 index 0000000..9c648b9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_cold.json @@ -0,0 +1,14 @@ +{ + "layers": { + "humanoid": [ + { + "texture": "nerospace:oxygen_suit_cold" + } + ], + "humanoid_leggings": [ + { + "texture": "nerospace:oxygen_suit_cold" + } + ] + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_heat.json b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_heat.json new file mode 100644 index 0000000..77fcd06 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_heat.json @@ -0,0 +1,14 @@ +{ + "layers": { + "humanoid": [ + { + "texture": "nerospace:oxygen_suit_heat" + } + ], + "humanoid_leggings": [ + { + "texture": "nerospace:oxygen_suit_heat" + } + ] + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_t2.json b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_t2.json new file mode 100644 index 0000000..60415a3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/equipment/oxygen_suit_t2.json @@ -0,0 +1,14 @@ +{ + "layers": { + "humanoid": [ + { + "texture": "nerospace:oxygen_suit_t2" + } + ], + "humanoid_leggings": [ + { + "texture": "nerospace:oxygen_suit_t2" + } + ] + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_pickaxe.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_pickaxe.json new file mode 100644 index 0000000..dab4654 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_pickaxe.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/nerosium_pickaxe" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_boots.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_boots.json new file mode 100644 index 0000000..c18b2ce --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_boots.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_chestplate.json new file mode 100644 index 0000000..bcec876 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_chestplate.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_boots.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_boots.json new file mode 100644 index 0000000..a824a6e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_boots.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_cold_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_chestplate.json new file mode 100644 index 0000000..d4f338c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_chestplate.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_cold_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_helmet.json new file mode 100644 index 0000000..b22f9e2 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_helmet.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_cold_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_leggings.json new file mode 100644 index 0000000..96daf36 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_cold_leggings.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_cold_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_boots.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_boots.json new file mode 100644 index 0000000..f40c585 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_boots.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_heat_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_chestplate.json new file mode 100644 index 0000000..098a14d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_chestplate.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_heat_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_helmet.json new file mode 100644 index 0000000..96d9669 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_helmet.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_heat_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_leggings.json new file mode 100644 index 0000000..ab0b22c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_heat_leggings.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_heat_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_helmet.json new file mode 100644 index 0000000..4145350 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_helmet.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_leggings.json new file mode 100644 index 0000000..4553a1d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_leggings.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_boots.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_boots.json new file mode 100644 index 0000000..05b2e9a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_boots.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_t2_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_chestplate.json new file mode 100644 index 0000000..f376a15 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_chestplate.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_t2_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_helmet.json new file mode 100644 index 0000000..ff3baf9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_helmet.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_t2_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_leggings.json new file mode 100644 index 0000000..7ca4979 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_suit_t2_leggings.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/oxygen_suit_t2_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 2085222..5dc081f 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -25,5 +25,22 @@ "block.nerospace.alien_pillar": "Alien Pillar", "block.nerospace.alien_lamp": "Alien Lamp", "block.nerospace.alien_crystal_block": "Alien Crystal Block", - "block.nerospace.meteor_rock": "Meteor Rock" + "block.nerospace.meteor_rock": "Meteor Rock", + "item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe", + "item.nerospace.oxygen_suit_helmet": "Oxygen Suit Helmet", + "item.nerospace.oxygen_suit_chestplate": "Oxygen Suit Chestplate", + "item.nerospace.oxygen_suit_leggings": "Oxygen Suit Leggings", + "item.nerospace.oxygen_suit_boots": "Oxygen Suit Boots", + "item.nerospace.oxygen_suit_t2_helmet": "Tier 2 Oxygen Suit Helmet", + "item.nerospace.oxygen_suit_t2_chestplate": "Tier 2 Oxygen Suit Chestplate", + "item.nerospace.oxygen_suit_t2_leggings": "Tier 2 Oxygen Suit Leggings", + "item.nerospace.oxygen_suit_t2_boots": "Tier 2 Oxygen Suit Boots", + "item.nerospace.oxygen_suit_heat_helmet": "Thermal Suit Helmet", + "item.nerospace.oxygen_suit_heat_chestplate": "Thermal Suit Chestplate", + "item.nerospace.oxygen_suit_heat_leggings": "Thermal Suit Leggings", + "item.nerospace.oxygen_suit_heat_boots": "Thermal Suit Boots", + "item.nerospace.oxygen_suit_cold_helmet": "Cryo Suit Helmet", + "item.nerospace.oxygen_suit_cold_chestplate": "Cryo Suit Chestplate", + "item.nerospace.oxygen_suit_cold_leggings": "Cryo Suit Leggings", + "item.nerospace.oxygen_suit_cold_boots": "Cryo Suit Boots" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_pickaxe.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_pickaxe.json new file mode 100644 index 0000000..ce7d1bf --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_pickaxe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "nerospace:item/nerosium_pickaxe" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_boots.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_boots.json new file mode 100644 index 0000000..49d8025 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_boots.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_chestplate.json new file mode 100644 index 0000000..e9be709 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_chestplate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_boots.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_boots.json new file mode 100644 index 0000000..1f74e14 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_boots.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_cold_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_chestplate.json new file mode 100644 index 0000000..c0b5527 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_chestplate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_cold_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_helmet.json new file mode 100644 index 0000000..42d347a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_helmet.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_cold_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_leggings.json new file mode 100644 index 0000000..277a9b3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_cold_leggings.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_cold_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_boots.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_boots.json new file mode 100644 index 0000000..433ab94 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_boots.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_heat_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_chestplate.json new file mode 100644 index 0000000..aa7a7c5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_chestplate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_heat_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_helmet.json new file mode 100644 index 0000000..7473be7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_helmet.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_heat_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_leggings.json new file mode 100644 index 0000000..6915b11 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_heat_leggings.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_heat_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_helmet.json new file mode 100644 index 0000000..7cc4042 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_helmet.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_leggings.json new file mode 100644 index 0000000..258834c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_leggings.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_boots.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_boots.json new file mode 100644 index 0000000..099d3e2 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_boots.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_t2_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_chestplate.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_chestplate.json new file mode 100644 index 0000000..57ac68d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_chestplate.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_t2_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_helmet.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_helmet.json new file mode 100644 index 0000000..b836b6e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_helmet.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_t2_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_leggings.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_leggings.json new file mode 100644 index 0000000..4764304 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/oxygen_suit_t2_leggings.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/oxygen_suit_t2_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit.png new file mode 100644 index 0000000000000000000000000000000000000000..f8944bb91794a9540752cbad36cb826e51b32a64 GIT binary patch literal 1209 zcmV;q1V;ObP)jHpW zeF%m}-urzd_e$_Xu!2ej0Qs&1 zb^zpmWjF*YsHFg)XbHXmc!;R20Amu%3 zm>5AYl(Cuy-{~>N!~lRdubcrB1mIP3kM1;$JPxzIY=r@NQFdv`pLAK|({8MK0B zojp-R8-Vz~Rzws|zu}jr#lX^<)FsB{Ok${}`juqpb{8(f{k8WJ9_^Fu<#q zFXUY?JZh}aLM6Cdt)=X(s`Y)~nE~grhF}BbowGwsaJx}px6D%74wKnkh_XN`x|OHG zbWV`!(l1kHQxq(~4uq*0H{BstiF)_-)mm={1#98S+pd|VwK_fBG?{Sq%ZXLm0>w(w zw?jNcna<|fbDkm2KD-gy0C2JRWUUnh14L0I^#=Xl4YpwzS~06mOlXa-CmYQSkouRd z^2*@OqXIO8`by@hJSy_y*<%2}bT$`3I99Kt-vOyBaE{w#5mgH40I)j1sutH>Knp|s z{p)A^{_PLk+?(O-*#qG6t)(D6cUuAF3haVbrn^v0w@$4grLbl!CtR+Ke9@tpDKT3A zhOhkzPQU+)r4qhMI9)R5|0U8vZc~OH+XS6dh@5#w@ab$WLCp0zx4mT>O*-rpU9fs2 z4bIPlrELu+{$eRuAI3_SDlz}Mis-n(gud_QZC`b#7uIe>LABp|Gk~Z1)Q||D@tCix zzfI&y(+4~JNK=J2nJf173HrUR9G3WC2Qy8_7R>#a(!@-e0l1EPHmT`bN~)O_JDjKm zAyJ>1(foZHrV?}(aouY=o6AnQR^g-BzkGrV0LW6K{9kf4S?VO;H5CwBOqF$oK4ox+ zisN!xg^wtb-V!?ocy{ed36!qsD9v*q)5rxVh-{CJ~3tF6qZbEPEKfhm2~_f3&=N25o$`g1I22F4`P zk-p423n-}Vs{$jy}&AZ4wSrg+a1xQu!G|JX9$yfU;ms;s$k$8N(< X$RkP`R7j+a00000NkvXXu0mjf^SVZ> literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_cold.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_cold.png new file mode 100644 index 0000000000000000000000000000000000000000..4cf23b1ee5145333c9caf5cbea9f67498d21b4da GIT binary patch literal 916 zcmV;F18e+=P)}KIYBLn>QaX5gZs_Ei&8q_meXKK)>6@`Nd`a zOL+6_VdUCllW5#FK`wk9;H7{hZX$`BdE113x2?9f89P3cX6drjmWrU0rz=nwOYpV; ziCMZVD2BiVuY(nMDL@m{2X(N5ASY0zxgJ4p$)hHvXM;S}_1%I!{4u7m~PH zP>5|Ujqy3tc?tk{djHNw^MnR52EZtQ76NDKvheLAJbhDp8_)pE%oBcIFEF{u^55a0 zhvA@?k8^=E;dtM+IYw(9z)kk$gS(}1-z&f^fIw;Urja{A$)HPk4lcN+gy}pjB}z|* zgX&!0B60>ALmJPw&COnkyh<#=1g9n93Pp#ulF}$(@{Y60X$@lUVpI?IQqC} z9J2}UQq2ECN&DA-vrIa-kow;CevrgX93G!q849IO?-9W2YK2-z_-)o5zP6z5YbvIs5(c+~=u8D7w6#m$TNKk(}FIVLxm8b&*dy@k){7-XN{(-7Lkyl(}f+HXnV}y zSkG;H$7t7ykq^)m$_27P-v6L0plzUKa@zZzX*~!1iDGi7gq70^&nq+@d>>pIKv8?# z%Swj1@H~Z;xqy5ywWY<=d5Xj1Q~a9zLBHG1=}?syQZ{#5ad2u~vH;&ao|_jXHb6N{ qQ9Gn~na@@Qv?Z?f*`@tQY1gXB35qYKTs_8XJ}=!)m92AEiEikrwA#Q zaS^kGF~TxdYq2@wnKS3!Id=?Z=WB9z?(4iCH!W1)aDQ**oBujX0RY2Z7xT+3|4TTT z|7ca+ag%6Y_ko=GCcq?scn~2TMETf-VXv!>HyItvqS<0u8j~Q%1xWMc0!^_5vjs@Z z7R!QQ2%PaISb<3Z8lx68!3u(cpwIuCiXZ`mT0#?Cp!MW_yxQrn2~E-rG_FEChzbJn z?WH-sCP~-s@%dzSqjf@?2m|0`KudwM#j^0*!aV(^@g|@HxQQpcjw2wxVm6L&cbWhI zUR?=5YF3`d8K!D+Q{b%wI5WgFht&nR1rRnx5P1-k1XeN6!5PXQ zhikrXnfsq8<={8Z9;{?=3+eBD?+5W9!t{9VB`EZKYQ})IYlYSorAknCd;bbqJ7}mA z6Eynj^d8Pqn24=&sD){Qd_l6FU$;S%F!chV8lqrnv7$Nx=FAgqbu&xrbo|i!<|i?l zoRS6Y!v)ALJrGi6a!Xl+pv0I&31Ovv_D687;d1J25X9PE~R zh9u4MJLNef3~Ba6pS-_LrCAby_5v&+H6n$;84ZCXT1Iw_DXkGBOR{8!LK>78#DmBy z$q>`@-D6!(_xRE)t+l4bf(1xMG&AF=#&tG3El z3Fgf65>`HR$_Gx29Mbc8-g)QVcki><{aM0U)PYc^+q&s>NA>H3o3P7gzVwKZ0}t6Ep$ON`^=UAQ|AA3S60w0;bYxEE5VMegC z5(rfR@RYSO-kAP42Cg1|0>@(j+u7T~a`20HyAv7QeENIa`$5!*(d%wm z3GzLknlWJNTE2BfE(^$R?_VKn2MuLnf<|AN%wkM~L~NNuU6>}w7bMs7%Qk2dPQ8Gy zhA3FNSWy`PbLNS*vYDmwh!R-&>2vq7x`T(qxggz)&nLqtK0WUmh_-B93asuF$p2-ztdn*a+zJ0DpLZ?r~jD_t?@at#wU{84Hk% zXlBM!jp>r2G-zs{wKG@78^8k69t6NiU!tIcrs@(V)M%kei>8#s=Zd0vk;Z6V6DY3* zY>@u%<&_7X30hOR-09VL;t`9C%AS*o{WhKF!d0xWGhfeumYAcIJ z!vwwV7QTM^n0YgxF3+cJUJfJg(mH1Wu5~;&FP^Gonxe5!^Fj|O{CxVIV4{!qxeDzs XU5H6j1&kzL00000NkvXXu0mjfO-!^$ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit.png new file mode 100644 index 0000000000000000000000000000000000000000..63694c3e16794574d30c45a88e627303b0b46c3e GIT binary patch literal 681 zcmV;a0#^NrP)MjF|#}vS-Q}$RAF!-#3@X9^BsDt9w(YpE$}T&s2-~tL>eV zy$T0@=^D4FFoDMo>G9HC3qtsuNy}eq(t!ub- z{kDisBzY2KWD-<+6As6TVqz`e`&9V@2!cxFpG5WyB6ul34v P00000NkvXXu0mjfMT0eU literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_cold.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_cold.png new file mode 100644 index 0000000000000000000000000000000000000000..f7eab40e2a876ebe34660b5c10adbb273bb3d7fd GIT binary patch literal 526 zcmV+p0`dKcP)15QU$O!KqY*V2leFE{t)N+6QovBApMCgq$Jd2suG+pab{_!KhH7!Wb2)wu-By z5Ft%F?+=;x zkN3B^Ikx#OxV`0Kh9ErFvVUoYt`{eB&O9#{vs#9y-Z*oW_W*K>ulpaDU}PJVjskgS ziy-v?*+9ym{spW7XzM7S&x27$ogiO|fh}i?oQ`4xAaQoPS*2t1iB~$`HBL5(`NT`d z?$$S*)`VIL;ErxLD@3uu^Xq#zp7n%P0YJ6g;wmUY+Tx1g_Gst(R6yk}U}L$M<>Xst zOQs9Ix1~bYI0Ino1VFOupjn>Nh`@i3^ zfS<@PsegfJ0?rMTx97aP(&3O4B)hZCH z&F8mWP^n40WDuuTeNsSJ0;x%`vhLz%f#@zEL3K|-c9AF_Fn=a^{TF35_{q>Kf!tN1 z^l9d3>!U!s$TUnGFQt`rwL++`A@O#B)Fh&O`Hb;^+o*simoJ7~_KVEar_W0yKwJcm z(WFj8CDGa-DTb1oyWIh+=?-%)S6s$n2}tG81iRUKA4K#_vmRb~QZID|0Dcv~gP{`O mt9jR2=O0AU?ht-%Uz#8L6CT|2F#GcW0000NklEVA)6lERD_jdrbv zWf91JU@VQaquJT{cO<~$@pwEQkH_Qjcsw4@e`6#KFU$8I0ElOE=))e_Zd0cfLs7$`?Gx}g^Gx9)b#bmJ8py~n2 ziIhqG3s@6yYoL5QPevVWlH3;~n`W;Cof{3{66_*L3tOb(+VL(s`6MD07q-pE>aKf2 zs|w(OE|L_v(O6DCfeC;kyYUD-+8ns-lj?nB+t}mo4Qm1b`)cu($&JSOWUx+NYab$g z@rvO|CI9?6z^d_Ny3n`_*a$*ZDc?F@QZD@1CWU+BOn_`z#Q9{3z`AT&eC6pU;Z=5T zrq9PW`xS>?^Y974WgH?99oh!&omIvHsqvFZsRlv)B zk)*}j=MV{SZi3fnqElH(v^TgahLV~ajYX0+?l8A9=4C7;;F3cV^kHB6Afj_xb@|Ve vI>f|@u?&VJz<2YmTIU}`uI?Z|w}<8zLeC*&-6*-K00000NkvXXu0mjf!K(+Q literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_pickaxe.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_pickaxe.png new file mode 100644 index 0000000000000000000000000000000000000000..9dc0cd3e7df83d2e421a14d8412bdcb8263a32bc GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`HJ&bxAr*6y6Be-juz!2-|NqN! zf7DglcnypU3=IDGpZ@v(^CeL>5a<(Tm#B}h_>-Oh1lKPoCd@Hyow$IJ2MEMN%of~8 zD^O=x@3v-vPu9u*Mk`fW=Jtx8D17mE`r`|W&jqLTCN&zE7 YV3L)hQMvSWpgR~mUHx3vIVCg!03EVFzW@LL literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_boots.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_boots.png new file mode 100644 index 0000000000000000000000000000000000000000..6d769ff4444a9aa8389ee1e7fc7582341aa1a99a GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`5uPrNAr*6y6C_v{H#9a19;aWk=5MmQTNj=<>D4=Nf(*CMgJZqX7IQoO`A#-uSMVIrZ8vyuOZ5)h sgNe=UjSVwo+U)K3R2}i~$a}!ZkQS_NuxIy28=xHwp00i_>zopr03r}HMgRZ+ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_chestplate.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_chestplate.png new file mode 100644 index 0000000000000000000000000000000000000000..8286768ecd3e194a363f7f42b052eb22c13f3ada GIT binary patch literal 161 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`8J;eVAr*6y6C^SYbey?a{^nY} z#O;$_y&9HkUa=iS-Cn(jY)`%`;8%T>D=mNT}`IcJcv>Z^gxrhDlpm ziD>thW(`XB*tnR$Jh#09~J4X=}x+?eegHZtnGVA}p~!7n9Gf7#ahC%1I}ur0_| zU|Au}#vfOsw1P2~w@rN78*ArD+yOj03ZHX&&NAegvHzdONj4s428L>NY30U*kcZ8?JL3EPo`Fp0J=vyseQ@W8;ShN7dJK6?6vQXElFk hb8r>M^dAox7)-uNs8l%x#sSS`@O1TaS?83{1OSiuF_QoQ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_boots.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_boots.png new file mode 100644 index 0000000000000000000000000000000000000000..abed14ea9e14378e4c8d493bff3d16ce25dea9b4 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`zMd|QAr*6y6C_v{H#9a19@}v_ z<&S)9(9(B0%8Hg^GaN2&J8zMDQ2n^en&3jl9o+M3el!LP+F9;kJa6|fti_)%??AV` hjD_gK$@TiEI9xG literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_chestplate.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_chestplate.png new file mode 100644 index 0000000000000000000000000000000000000000..2cce18aca29ce740a5faf8452372ae510d62e212 GIT binary patch literal 144 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`;hrvzAr*6y6C^SYbe!qj9Ppbz zS>LL#TfbP0l+XkK;Ds}u literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_helmet.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_helmet.png new file mode 100644 index 0000000000000000000000000000000000000000..943a61333a74021e759cc3f1234945c47be87309 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%``JOJ0Ar*6y6C^SYbm#>u}wH zBfy+zUd<230Onm%Jo*xx6Q41OFwd)1dc(AbE$8F}Rj|QhoS0*!AGgRv*Q)Vw!?N7@7e$=rbe+!@sZNxTXb|e~xBGKo6~}ao ludyHA2$#(+=;Y^PVBkv=KVYHCzZ7UVgQu&X%Q~loCIDt8E?fWr literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_helmet.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_helmet.png new file mode 100644 index 0000000000000000000000000000000000000000..0d63250782204cf7a9c3c18559096e643661d50e GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`#hxyXAr*6y6C^SYbm)C}aC7?q zA0`?e9~ZoEeE7z?jC*~WM8T$@=GVzeZp?NL8yR&@%#k+FGql-M;23Cd*pavO{xw71 z?Tj}}1emVyHcOkY6TH%}j_ojaA*ZFvP6ktf^|4Bx#%+w7>-H*nO=dcIjVFtNVMn6W V?*%d5Qb6k&JYD@<);T3K0RZH{I^h5S literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_leggings.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_leggings.png new file mode 100644 index 0000000000000000000000000000000000000000..4e0cbed0c00ba21e15329b8e71e817a8c857ae6d GIT binary patch literal 145 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`5uPrNAr*6y6C^%02+rL6{>Iw> zM>2{w1vS4;cAU1O@UhrQJqN=B*+*3-@g8{1IwQES)7StACg!M0Ex5t3h)FR1LC|g1 s>jgU)c3beq6ucEG6MrBeFT=p_cb2?w_gBXaKsy*bUHx3vIVCg!09W=h`Tzg` literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_boots.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_boots.png new file mode 100644 index 0000000000000000000000000000000000000000..bb85a5d6f3e361a6d2ad5911d6c3f7eacfbd94dc GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`{+=$5Ar*6y6C_v{H#9a1916kT}mCMx7lh6aLy5=w|Ar*6y6C^%02+j-+xcYs* zgsl;9lKjpA*Mj2ewUUL3St q(^b$Je4q9FJevcnIHv!2z`*dVR3bFr>scDm0tQc4KbLh*2~7Y+Gd2tW literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_pickaxe.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_pickaxe.json new file mode 100644 index 0000000..cf14784 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_pickaxe.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "#": "#c:ingots/nerosium", + "|": "#c:rods/wooden" + }, + "pattern": [ + "###", + " | ", + " | " + ], + "result": { + "id": "nerospace:nerosium_pickaxe" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_boots.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_boots.json new file mode 100644 index 0000000..893290b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_boots.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "N N", + "N N" + ], + "result": { + "id": "nerospace:oxygen_suit_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_boots.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_boots.json new file mode 100644 index 0000000..8231518 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_boots.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:glacite", + "H": "nerospace:oxygen_suit_t2_boots" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_cold_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_chestplate.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_chestplate.json new file mode 100644 index 0000000..328c483 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_chestplate.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:glacite", + "H": "nerospace:oxygen_suit_t2_chestplate" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_cold_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_helmet.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_helmet.json new file mode 100644 index 0000000..f48dad4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_helmet.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:glacite", + "H": "nerospace:oxygen_suit_t2_helmet" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_cold_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_leggings.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_leggings.json new file mode 100644 index 0000000..83fac89 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_cold_leggings.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:glacite", + "H": "nerospace:oxygen_suit_t2_leggings" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_cold_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_boots.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_boots.json new file mode 100644 index 0000000..d53dba3 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_boots.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:cindrite", + "H": "nerospace:oxygen_suit_t2_boots" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_heat_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_chestplate.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_chestplate.json new file mode 100644 index 0000000..6e9e7d6 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_chestplate.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:cindrite", + "H": "nerospace:oxygen_suit_t2_chestplate" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_heat_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_helmet.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_helmet.json new file mode 100644 index 0000000..e9a404b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_helmet.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:cindrite", + "H": "nerospace:oxygen_suit_t2_helmet" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_heat_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_leggings.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_leggings.json new file mode 100644 index 0000000..3d152ea --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_heat_leggings.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "nerospace:cindrite", + "H": "nerospace:oxygen_suit_t2_leggings" + }, + "pattern": [ + " G ", + "GHG", + " G " + ], + "result": { + "id": "nerospace:oxygen_suit_heat_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_helmet.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_helmet.json new file mode 100644 index 0000000..422a3ba --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_helmet.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "G": "#c:glass_blocks/colorless", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NNN", + "NGN" + ], + "result": { + "id": "nerospace:oxygen_suit_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_leggings.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_leggings.json new file mode 100644 index 0000000..bb773fe --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_leggings.json @@ -0,0 +1,15 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NNN", + "N N", + "N N" + ], + "result": { + "id": "nerospace:oxygen_suit_leggings" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_boots.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_boots.json new file mode 100644 index 0000000..c592531 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_boots.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:gems/cindrite", + "H": "nerospace:oxygen_suit_boots" + }, + "pattern": [ + " C ", + "CHC", + " C " + ], + "result": { + "id": "nerospace:oxygen_suit_t2_boots" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_chestplate.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_chestplate.json new file mode 100644 index 0000000..cef7335 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_chestplate.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:gems/cindrite", + "H": "nerospace:oxygen_suit_chestplate" + }, + "pattern": [ + " C ", + "CHC", + " C " + ], + "result": { + "id": "nerospace:oxygen_suit_t2_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_helmet.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_helmet.json new file mode 100644 index 0000000..b058b5f --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_helmet.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:gems/cindrite", + "H": "nerospace:oxygen_suit_helmet" + }, + "pattern": [ + " C ", + "CHC", + " C " + ], + "result": { + "id": "nerospace:oxygen_suit_t2_helmet" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_leggings.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_leggings.json new file mode 100644 index 0000000..7532758 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_t2_leggings.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "#c:gems/cindrite", + "H": "nerospace:oxygen_suit_leggings" + }, + "pattern": [ + " C ", + "CHC", + " C " + ], + "result": { + "id": "nerospace:oxygen_suit_t2_leggings" + } +} \ No newline at end of file From 05a4b55b8c2461bc888ba1fc010ae2633c05f92a Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:47:58 +0800 Subject: [PATCH 14/82] Add new items, assets and recipes Register several new items (nerosium_dust, alien_fragment, alien_tech_scrap, alien_core, rocket_fuel_canister, frame_casing, grav_striders, drift_fleece) in ModItems and add them to the creative Ingredients tab. Add item asset JSONs, item model JSONs, and PNG textures for each new item. Update en_us.json with display names for the new items. Add crafting recipes: frame_casing (shaped), rocket_fuel_canister (shapeless), and an oxygen_suit_chestplate recipe that uses the rocket fuel canister. --- .../neroland/nerospace/registry/ModItems.java | 12 +++++++++++- .../assets/nerospace/items/alien_core.json | 6 ++++++ .../assets/nerospace/items/alien_fragment.json | 6 ++++++ .../nerospace/items/alien_tech_scrap.json | 6 ++++++ .../assets/nerospace/items/drift_fleece.json | 6 ++++++ .../assets/nerospace/items/frame_casing.json | 6 ++++++ .../assets/nerospace/items/grav_striders.json | 6 ++++++ .../assets/nerospace/items/nerosium_dust.json | 6 ++++++ .../nerospace/items/rocket_fuel_canister.json | 6 ++++++ .../resources/assets/nerospace/lang/en_us.json | 10 +++++++++- .../nerospace/models/item/alien_core.json | 6 ++++++ .../nerospace/models/item/alien_fragment.json | 6 ++++++ .../nerospace/models/item/alien_tech_scrap.json | 6 ++++++ .../nerospace/models/item/drift_fleece.json | 6 ++++++ .../nerospace/models/item/frame_casing.json | 6 ++++++ .../nerospace/models/item/grav_striders.json | 6 ++++++ .../nerospace/models/item/nerosium_dust.json | 6 ++++++ .../models/item/rocket_fuel_canister.json | 6 ++++++ .../nerospace/textures/item/alien_core.png | Bin 0 -> 226 bytes .../nerospace/textures/item/alien_fragment.png | Bin 0 -> 141 bytes .../textures/item/alien_tech_scrap.png | Bin 0 -> 160 bytes .../nerospace/textures/item/drift_fleece.png | Bin 0 -> 381 bytes .../nerospace/textures/item/frame_casing.png | Bin 0 -> 186 bytes .../nerospace/textures/item/grav_striders.png | Bin 0 -> 121 bytes .../nerospace/textures/item/nerosium_dust.png | Bin 0 -> 175 bytes .../textures/item/rocket_fuel_canister.png | Bin 0 -> 226 bytes .../data/nerospace/recipe/frame_casing.json | 16 ++++++++++++++++ .../recipe/oxygen_suit_chestplate.json | 16 ++++++++++++++++ .../nerospace/recipe/rocket_fuel_canister.json | 13 +++++++++++++ 29 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_fragment.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_tech_scrap.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/drift_fleece.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/frame_casing.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/grav_striders.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosium_dust.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_canister.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/alien_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/alien_fragment.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/alien_tech_scrap.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/drift_fleece.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/frame_casing.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/grav_striders.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_dust.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_canister.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_fragment.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/drift_fleece.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/frame_casing.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/grav_striders.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_dust.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_canister.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/frame_casing.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_chestplate.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/rocket_fuel_canister.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 64a6b5a..ca0b961 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -66,6 +66,14 @@ public final class ModItems { public static final RegistryEntry XERTZ_QUARTZ = item("xertz_quartz"); public static final RegistryEntry CINDRITE = item("cindrite"); public static final RegistryEntry GLACITE = item("glacite"); + public static final RegistryEntry NEROSIUM_DUST = item("nerosium_dust"); + public static final RegistryEntry ALIEN_FRAGMENT = item("alien_fragment"); + public static final RegistryEntry ALIEN_TECH_SCRAP = item("alien_tech_scrap"); + public static final RegistryEntry ALIEN_CORE = item("alien_core"); + public static final RegistryEntry ROCKET_FUEL_CANISTER = item("rocket_fuel_canister"); + public static final RegistryEntry FRAME_CASING = item("frame_casing"); + public static final RegistryEntry GRAV_STRIDERS = item("grav_striders"); + public static final RegistryEntry DRIFT_FLEECE = item("drift_fleece"); // --- Tool + armor materials -------------------------------------------- public static final ToolMaterial NEROSIUM_TOOL_MATERIAL = new ToolMaterial( @@ -152,7 +160,9 @@ public static Map, List> creativeTabItems CreativeModeTabs.INGREDIENTS, List.of(RAW_NEROSIUM.get(), NEROSIUM_INGOT.get(), RAW_NEROSTEEL.get(), NEROSTEEL_INGOT.get(), - XERTZ_QUARTZ.get(), CINDRITE.get(), GLACITE.get()), + XERTZ_QUARTZ.get(), CINDRITE.get(), GLACITE.get(), + NEROSIUM_DUST.get(), ALIEN_FRAGMENT.get(), ALIEN_TECH_SCRAP.get(), ALIEN_CORE.get(), + ROCKET_FUEL_CANISTER.get(), FRAME_CASING.get(), GRAV_STRIDERS.get(), DRIFT_FLEECE.get()), CreativeModeTabs.TOOLS_AND_UTILITIES, List.of(NEROSIUM_PICKAXE.get()), CreativeModeTabs.COMBAT, diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_core.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_core.json new file mode 100644 index 0000000..90d9d61 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_core.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/alien_core" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_fragment.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_fragment.json new file mode 100644 index 0000000..4bacabf --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_fragment.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/alien_fragment" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_tech_scrap.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_tech_scrap.json new file mode 100644 index 0000000..b315c60 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_tech_scrap.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/alien_tech_scrap" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/drift_fleece.json b/multiloader/common/src/main/resources/assets/nerospace/items/drift_fleece.json new file mode 100644 index 0000000..7b3f4ba --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/drift_fleece.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/drift_fleece" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/frame_casing.json b/multiloader/common/src/main/resources/assets/nerospace/items/frame_casing.json new file mode 100644 index 0000000..918a6a5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/frame_casing.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/frame_casing" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/grav_striders.json b/multiloader/common/src/main/resources/assets/nerospace/items/grav_striders.json new file mode 100644 index 0000000..852f036 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/grav_striders.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/grav_striders" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_dust.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_dust.json new file mode 100644 index 0000000..ff10b4c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_dust.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/nerosium_dust" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_canister.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_canister.json new file mode 100644 index 0000000..a4d3f00 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_canister.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/rocket_fuel_canister" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 5dc081f..e6ee676 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -42,5 +42,13 @@ "item.nerospace.oxygen_suit_cold_helmet": "Cryo Suit Helmet", "item.nerospace.oxygen_suit_cold_chestplate": "Cryo Suit Chestplate", "item.nerospace.oxygen_suit_cold_leggings": "Cryo Suit Leggings", - "item.nerospace.oxygen_suit_cold_boots": "Cryo Suit Boots" + "item.nerospace.oxygen_suit_cold_boots": "Cryo Suit Boots", + "item.nerospace.nerosium_dust": "Nerosium Dust", + "item.nerospace.alien_fragment": "Alien Fragment", + "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", + "item.nerospace.alien_core": "Alien Core", + "item.nerospace.rocket_fuel_canister": "Rocket Fuel Canister", + "item.nerospace.frame_casing": "Frame Casing", + "item.nerospace.grav_striders": "Grav Striders", + "item.nerospace.drift_fleece": "Drift Fleece" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_core.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_core.json new file mode 100644 index 0000000..2257cf0 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_core.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/alien_core" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_fragment.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_fragment.json new file mode 100644 index 0000000..5442743 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_fragment.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/alien_fragment" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_tech_scrap.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_tech_scrap.json new file mode 100644 index 0000000..894d2e5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_tech_scrap.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/alien_tech_scrap" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/drift_fleece.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/drift_fleece.json new file mode 100644 index 0000000..5875999 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/drift_fleece.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/drift_fleece" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/frame_casing.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/frame_casing.json new file mode 100644 index 0000000..a259d85 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/frame_casing.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/frame_casing" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/grav_striders.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/grav_striders.json new file mode 100644 index 0000000..aba7ff4 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/grav_striders.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/grav_striders" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_dust.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_dust.json new file mode 100644 index 0000000..f0c7778 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/nerosium_dust.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/nerosium_dust" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_canister.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_canister.json new file mode 100644 index 0000000..2ede78d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_canister.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/rocket_fuel_canister" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_core.png new file mode 100644 index 0000000000000000000000000000000000000000..0c0c06130b042f6bcc040d144152f73493e3cae6 GIT binary patch literal 226 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`D?MEtLn`JZCoB*!ND`hRF!^`C z@1LWq-6dWbvLwu!e?G`baJtX#_NxMFNhZqO3wQG!HQ_Ye`QM;*DxY-#Plo*i#&XV0 z+5tR2_8)v`Z+7-Yn*p0l!|PJ3< Yfl0zopr01kstasU7T literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_fragment.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_fragment.png new file mode 100644 index 0000000000000000000000000000000000000000..36c19d131cf437506a2f2d977c9243520fa2ec7f GIT binary patch literal 141 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`A)YRdAr*6y6C_wgnB8K8q}2WX zxIeqUVZ(t{(>SL;W8h`p!}iFup|PXEe7Fi*Ar*6y6C_wgnB7VkrM0*J z7ZM8M(T}r9KH#=Kayx@7zwj>Qa@RF+8eRs39dBj@(fw#%%G8-Om@zu9@=$JZbJ0j*^4boFyt I=akR{0GRkVUH||9 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/drift_fleece.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/drift_fleece.png new file mode 100644 index 0000000000000000000000000000000000000000..3eebfd45b117d69a57664cd19f80b8324840d30f GIT binary patch literal 381 zcmV-@0fPRCP)W(`Y4Zhd!=jV+p}Q zSX|`fPr+?2h}tQZ5X9{a*Rk>3P0C)8w~vI1=PvPU|KPEinAu$8mmG{SyuP=&IzL*= b|Ka)uzA1RI4z`UO00000NkvXXu0mjf2$-*k literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/frame_casing.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/frame_casing.png new file mode 100644 index 0000000000000000000000000000000000000000..17dba83133fd488b9cef0f7e83ea29378bea0fd5 GIT binary patch literal 186 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jh-%!Ar*6y6C~mi4z_v!7th)8 z<^EB1PW|3B?Sgk3W;JQ-nU?WlhT)r=M*V`TmvsAIs+VM)`#(ZeQbHm{q{JZRK+Bm< zPt60_*38VzhbLJI2mJ8)p|X*&#JjDLJ7Ce_qv|jJ_i8WyAa%hgQPSC|K`utVjoYN; jj6{^0&S6h}X9kAZPds$s08Ffy~DOUG;7}DXNm%zxdJ4SFr T?&Gp_pjix_u6{1-oD!M<4;3ce literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_dust.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_dust.png new file mode 100644 index 0000000000000000000000000000000000000000..80d4a201d213eb5e66113fd15b36561d4515cef6 GIT binary patch literal 175 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Wu7jMAr*6y6D0mU_`iky|I5Xl zYHe`=H$VKZkK*fcYhrtN$zc-r3BG1-E$M&CmtQ<+Z)Hwwu8*+zqdz(7(|?eb(@Ulu znA`u(bj|{wEY28Zo&zTT{ZIe&-}b#AGsSWH-q(>l%FN71wBJ-&ah&9~O^NiWW?(qo W6L`Vylv5DyHn?K+u;|^%ZI%URpM@#ynDWrT%>QJ#qk3N4kR39SlDT*pr^;#|LGtH ZgMFJ_;8E!&f1sloJYD@<);T3K0RVe0Sx5i? literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/frame_casing.json b/multiloader/common/src/main/resources/data/nerospace/recipe/frame_casing.json new file mode 100644 index 0000000..389137c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/frame_casing.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "I": "#c:ingots/nerosteel" + }, + "pattern": [ + "III", + "I I", + "III" + ], + "result": { + "count": 4, + "id": "nerospace:frame_casing" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_chestplate.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_chestplate.json new file mode 100644 index 0000000..221a467 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_suit_chestplate.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "C": "nerospace:rocket_fuel_canister", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "N N", + "NCN", + "NNN" + ], + "result": { + "id": "nerospace:oxygen_suit_chestplate" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_fuel_canister.json b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_fuel_canister.json new file mode 100644 index 0000000..b6b2ab4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_fuel_canister.json @@ -0,0 +1,13 @@ +{ + "type": "minecraft:crafting_shapeless", + "category": "misc", + "ingredients": [ + "minecraft:blaze_powder", + "minecraft:coal", + "#c:ingots/iron" + ], + "result": { + "count": 2, + "id": "nerospace:rocket_fuel_canister" + } +} \ No newline at end of file From 201b5d0e00914a406b898fb64ff6a542681faf80 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:59:28 +0800 Subject: [PATCH 15/82] Create MULTILOADER_MIGRATION.md --- docs/MULTILOADER_MIGRATION.md | 144 ++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/MULTILOADER_MIGRATION.md diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md new file mode 100644 index 0000000..4bc8eac --- /dev/null +++ b/docs/MULTILOADER_MIGRATION.md @@ -0,0 +1,144 @@ +# Nerospace multiloader — migration status & remaining-work plan + +Companion to `MULTILOADER.md` (which holds the architecture decision and toolchain +field notes). This file tracks **what content has been ported into `multiloader/common` +and how the remaining systems should be ported**, based on the concrete NeoForge ↔ Fabric +API divergences found during the port. + +Last updated: 2026-06-19. Verified build targets: **NeoForge @ 26.1.2** and **Fabric @ 26.2** +(both `BUILD SUCCESSFUL` via the gradle MCP after every batch). + +--- + +## 1. What is ported + +All of the following lives once in `multiloader/common` and drives both loaders through the +`RegistrationProvider` seam (`common/registry/RegistrationProvider.java`) + the per-loader +`RegistrationFactory` services. Creative-tab placement is defined once in +`ModItems.creativeTabItems()` and applied by each loader entry point. + +| Area | Count | Notes | +|------|-------|-------| +| Blocks | 20 | ores, storage blocks, station + alien decorative, meteor rock | +| Items (non-block) | 32 | materials, `nerosium_pickaxe`, 16 oxygen-suit armor pieces, alien/utility items | +| Block items | 20 | one per block | +| Loot tables | 20 | ore drops (silk/fortune), block self-drops | +| Recipes | 30 | crafting (tag-based) + smelting/blasting for nerosium & nerosteel | +| Tags | block: `mineable/pickaxe`, `needs_iron_tool`; item: `c:` ores/ingots/gems/raw/storage | | +| Lang | 52 keys | | +| Equipment (worn armor) | 4 defs + textures | oxygen suit base/T2/heat/cold | + +**The pattern that worked (keep using it for all data-only content):** the root project's +datagen has already emitted every asset/loot/recipe/tag/lang JSON under +`src/generated/resources`. Migration = (a) write the registration in `common` via +`RegistrationProvider`, (b) copy the matching generated JSON + the committed textures into +`multiloader/common/src/main/resources`, (c) build both loaders. No multiloader datagen is +needed yet — the root is the source of truth for the JSON. + +--- + +## 2. Confirmed cross-loader facts (so they aren't re-discovered) + +- **26.x is de-obfuscated** → Fabric Loom uses **no `mappings`**; NeoForge uses ModDevGradle/NeoForm. +- **`ResourceLocation` is `net.minecraft.resources.Identifier`** in 26.x; **`ResourceKey.identifier()`** (not `.location()`). +- Item-model definitions live in **`assets//items/.json`** (1.21.4+ format), not `models/item`. +- Loot/recipe/tag dirs are **singular**: `loot_table/`, `recipe/`, `tags/block`, `tags/item`. +- **`Item.Properties.pickaxe(...)` / `humanoidArmor(...)` are vanilla** (present on the Fabric classpath) — tools/armor compile in `common`. +- **Fabric API is consumed with plain `implementation`** (the umbrella + modules resolve transitively; `modImplementation` is *not* registered in Loom's de-obf mode). Creative-tab API is `net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents.modifyOutputEvent(...)`. +- **NeoForge 26.2 userdev is not on Maven** → self-build to `mavenLocal` (already working on the dev machine). +- Editing an *existing* resource file can read stale through the agent's mount cache; the **dev-side Gradle output is the source of truth** (confirmed via a dev-side read). + +--- + +## 3. Remaining work + +### 3a. Data-only — same fast path as above (low risk, no platform code) + +These are vanilla JSON the root already generated; port the same way (copy generated JSON, +add any trivial registration). They make existing content *function in the world*: + +- **Worldgen** (`ModFeatures`, `world/` 18 files): `configured_feature` + `placed_feature` + for the ores are vanilla JSON (common). **Biome injection differs**: NeoForge = a + `biome_modifier` JSON (data, common-ish); Fabric = `BiomeModifications` API (code, fabric-api). + → put the feature JSON in common; add a tiny per-loader biome hook. This is what makes the + migrated ores actually spawn (today they're craftable/creative only). +- **Sounds** (`ModSounds`): `sounds.json` + `.ogg` assets — common. +- **Advancements / criteria triggers** (`ModCriteria`): JSON advancements are common; custom + trigger *types* need registration (mostly common). +- **Data components** (`ModDataComponents`, 3): vanilla `DataComponentType` registry — common + via `RegistrationProvider` over `Registries.DATA_COMPONENT_TYPE`. +- Remaining **recipes/loot/tags** for already-migrated content. + +### 3b. Loader-divergent systems — need the platform seam (`Services`) + +Ordered by how much they unblock. Each needs a common interface + two loader impls; none can +be runtime-verified in this environment (compile-verify only — flag rendering/behavior for a +real client test). + +1. **Fluids** (`fluid/`, `ModFluids`) — *smallest divergent unit; good seam pilot.* + - NeoForge: `FluidType` (no Fabric analog) + `BaseFlowingFluid.Source/.Flowing` + + `BaseFlowingFluid.Properties` linking type/still/flowing/bucket/block. + - Fabric: extend vanilla `FlowableFluid` directly (own `Source`/`Flowing` like `WaterFluid`), + register render handler via fabric-api (`FluidRenderHandlerRegistry`) + `FluidVariantAttributes`. + - Seam: `IFluidPlatform { Fluid rocketFuelStill(); Fluid rocketFuelFlowing(); }` registered + per loader; the `LiquidBlock` + `BucketItem` (vanilla) stay in common, built from a supplier + of the still fluid. Watch registration order (fluid before block/bucket). + +2. **Block entities + menus** (`ModBlockEntities` 27, `ModMenuTypes` 13, `machine/` 43, + `storage/` 19, `solar/`, `pipe/`, `rocket/`). + - `BlockEntityType` / `MenuType` registration is vanilla → `RegistrationProvider` over the + respective registries (common). + - Block-entity *ticking* and menu *opening* differ slightly (NeoForge `menuProvider`/ + `openMenu` vs Fabric `ExtendedScreenHandlerFactory`); screens are **client-only** and + registered differently (NeoForge `RegisterMenuScreensEvent` vs Fabric `MenuScreens`/ + `HandledScreens`). + +3. **Capabilities / storage** — *the biggest divergence* (`ModCapabilities` 37, `gas/`, `module/`). + - NeoForge: `Capabilities.{ItemHandler,FluidHandler,EnergyStorage}` + `RegisterCapabilitiesEvent`. + - Fabric: `team.reborn.energy` / Fabric Transfer API (`Storage`, `Storage`) + + `BlockApiLookup` registration. + - Seam: a common storage abstraction (`IPlatformEnergy/IItemStore/IFluidStore`) exposed by + block entities; each loader adapts it to its capability/lookup system. This is the design + that should be settled **before** porting the machines en masse — it shapes every machine. + +4. **Networking** (`network/` 5): NeoForge payload registration (`RegisterPayloadHandlersEvent`, + `CustomPacketPayload`) vs Fabric `PayloadTypeRegistry` + `ClientPlayNetworking`/`ServerPlayNetworking`. + Seam: common payload records + a `INetworkPlatform.sendToServer/sendToClient`. + +5. **Entities** (`ModEntities` 13, `entity/` 12, `village/`): `EntityType` registration is vanilla + (common); **attributes** (NeoForge `EntityAttributeCreationEvent` vs Fabric + `FabricDefaultAttributeRegistry`) and **renderers/models** (client) are per-loader. The + `model_sync` tooling is root-only — multiloader entity models would be authored fresh or shared. + +6. **Attachments** (`ModAttachments` 5): NeoForge `AttachmentType` vs Fabric + (`fabric-data-attachment-api-v1`, already on the classpath). Seam over `Services`. + +7. **Client rendering** (`client/` 52): BERs, entity renderers, screens, HUD, model layers, + item properties — all client-only and loader-divergent (NeoForge client events vs Fabric + `ClientModInitializer` + registries). Port alongside each system's server side. + +8. **Dimensions** (`ModDimensions` 4, `ModDimensionTypes`): dimension/dimension_type JSON is + common; the dimension *travel* code and any custom chunk generators are code (mostly common, + some per-loader hooks). + +### 3c. Out of scope / defer +- `datagen/` (9) — root-only; not needed unless multiloader grows its own datagen. +- `gametest/`, `telemetry/`, `compat/`, `command/` — port last; `compat` (JEI etc.) is per-mod. + +--- + +## 4. Recommended order + +1. **Worldgen ore features (3a)** — cheap, makes migrated ores spawn; introduces the one small + per-loader biome hook. +2. **Capability/storage seam design (3b-3)** — settle the abstraction first; it gates machines. +3. **Fluids (3b-1)** — pilot the divergent-registration seam end to end. +4. **One machine vertical slice** — a single block entity + menu + screen + storage on both + loaders, to prove 3b-2/3/7 together before bulk-porting `machine/`. +5. Bulk machines/storage → networking → entities → dimensions/rockets → client polish. + +## 5. Standing constraints +- **No runtime verification here** — every batch is compile-verified on both loaders via the + gradle MCP; rendering/behavior/worldgen need a real client/world test on the dev machine. +- **`.vscode` JSON + re-edited resources** can read stale through the mount; trust dev-side Gradle. +- **Commit/push stays manual** (per project rules). From ee28d73348af3f4eae0a1beff72e35a5883c879e Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:02:03 +0800 Subject: [PATCH 16/82] Add nerosium ore features and Fabric biome hook Introduce worldgen data and wiring for the new Nerosium ore. Adds a configured_feature (nerosium_ore.json) and placed_feature (nerosium_ore_placed.json) defining ore size, targets (stone/deepslate), spawn count and height range. Adds a NeoForge biome_modifier JSON to inject the placed feature in overworld biomes. Updates Fabric initializer to register creative tab items and programmatically add the placed feature to overworld biomes (UNDERGROUND_ORES) via BiomeModifications so the ore appears on Fabric runtime. --- .../configured_feature/nerosium_ore.json | 27 +++++++++++++++++++ .../placed_feature/nerosium_ore_placed.json | 27 +++++++++++++++++++ .../nerospace/fabric/NerospaceFabric.java | 26 ++++++++++++++++-- .../biome_modifier/add_nerosium_ore.json | 6 +++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosium_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosium_ore_placed.json create mode 100644 multiloader/neoforge/src/main/resources/data/nerospace/neoforge/biome_modifier/add_nerosium_ore.json diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosium_ore.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosium_ore.json new file mode 100644 index 0000000..18cdd00 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosium_ore.json @@ -0,0 +1,27 @@ +{ + "type": "minecraft:ore", + "config": { + "discard_chance_on_air_exposure": 0.0, + "size": 9, + "targets": [ + { + "state": { + "Name": "nerospace:nerosium_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + }, + { + "state": { + "Name": "nerospace:deepslate_nerosium_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + } + ] + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosium_ore_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosium_ore_placed.json new file mode 100644 index 0000000..7bbc42d --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosium_ore_placed.json @@ -0,0 +1,27 @@ +{ + "feature": "nerospace:nerosium_ore", + "placement": [ + { + "type": "minecraft:count", + "count": 8 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "max_inclusive": { + "absolute": 56 + }, + "min_inclusive": { + "absolute": -24 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 592df59..a1f9cd4 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -1,14 +1,23 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.biome.v1.BiomeModifications; +import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.placement.PlacedFeature; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.ModItems; /** - * Fabric entry point. Shared init registers content eagerly, then fills creative - * tabs from the common grouping via the Fabric API creative-tab module. + * Fabric entry point. Shared init registers content eagerly, then Fabric-side + * wiring: creative-tab fill (Fabric API creative-tab module) and biome injection + * of the ore placed-features (Fabric API biome module — the counterpart to the + * NeoForge {@code biome_modifier} JSON). */ public final class NerospaceFabric implements ModInitializer { @@ -16,8 +25,21 @@ public final class NerospaceFabric implements ModInitializer { public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); NerospaceCommon.init(); + ModItems.creativeTabItems().forEach((tab, items) -> CreativeModeTabEvents.modifyOutputEvent(tab) .register(output -> items.forEach(output::accept))); + + addOverworldOre("nerosium_ore_placed"); + } + + private static void addOverworldOre(String placedFeatureName) { + ResourceKey key = ResourceKey.create( + Registries.PLACED_FEATURE, + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, placedFeatureName)); + BiomeModifications.addFeature( + BiomeSelectors.foundInOverworld(), + GenerationStep.Decoration.UNDERGROUND_ORES, + key); } } diff --git a/multiloader/neoforge/src/main/resources/data/nerospace/neoforge/biome_modifier/add_nerosium_ore.json b/multiloader/neoforge/src/main/resources/data/nerospace/neoforge/biome_modifier/add_nerosium_ore.json new file mode 100644 index 0000000..8a46214 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/data/nerospace/neoforge/biome_modifier/add_nerosium_ore.json @@ -0,0 +1,6 @@ +{ + "type": "neoforge:add_features", + "biomes": "#c:is_overworld", + "features": "nerospace:nerosium_ore_placed", + "step": "underground_ores" +} \ No newline at end of file From 1d5848b85c7371d3b499c1deef4cc38a2c750699 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:04:35 +0800 Subject: [PATCH 17/82] Update multiloader.yml --- .github/workflows/multiloader.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml index 5c5f83e..636bece 100644 --- a/.github/workflows/multiloader.yml +++ b/.github/workflows/multiloader.yml @@ -15,10 +15,6 @@ on: paths: - "multiloader/**" - ".github/workflows/multiloader.yml" - pull_request: - paths: - - "multiloader/**" - - ".github/workflows/multiloader.yml" workflow_dispatch: permissions: @@ -30,13 +26,13 @@ jobs: runs-on: ubuntu-latest # No continue-on-error: any failed cell fails the workflow. strategy: - fail-fast: false # still run every cell so one failure doesn't mask others + fail-fast: false # still run every cell so one failure doesn't mask others matrix: include: - loader: neoforge - mc: "26.1.2" # ModDevGradle + NeoForge 26.1.2.76 — verified green + mc: "26.1.2" # ModDevGradle + NeoForge 26.1.2.76 — verified green - loader: fabric - mc: "26.2" # Fabric Loom 1.17 + fabric-api 0.152.1+26.2 (de-obf, no mappings) — verified green + mc: "26.2" # Fabric Loom 1.17 + fabric-api 0.152.1+26.2 (de-obf, no mappings) — verified green # PENDING — not buildable on a clean CI runner; re-add when unblocked: # - { loader: fabric, mc: "26.1.2" } # Fabric never shipped MC 26.1.2 (26.1 -> 26.1.1 -> 26.2) # - { loader: neoforge, mc: "26.2" } # needs NeoForge 26.2 userdev on Maven (or self-built to mavenLocal — see multiloader/README.md) From 69cadcaab2cb21f2ceda2fd783ff12f31eff81fe Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:24:47 +0800 Subject: [PATCH 18/82] Add Item Store block entity and build infra updates Introduce a new Item Store block and block-entity: ItemStoreBlock and ItemStoreBlockEntity (27-slot WorldlyContainer exposing a vanilla ChestMenu). Register the block, item and block-entity via ModBlocks/ModItems/ModBlockEntities and wire ModRegistries.init(). Add resources: blockstate, model, item model, textures, lang entries, loot table, crafting recipe, and update mineable/needs_iron_tool tags. Platform/build changes: add Fabric access widener and reference it in fabric.mod.json and fabric/build.gradle to support Fabric @26.1.2; bump neo_version_26.2 in multiloader/gradle.properties. Update CI workflow (.github/workflows/multiloader.yml) and multiloader/README.md to reflect the current build-matrix status and notes about loader/version compatibility. --- .github/workflows/multiloader.yml | 31 ++-- multiloader/README.md | 20 ++- .../nerospace/registry/ModBlockEntities.java | 29 ++++ .../nerospace/registry/ModBlocks.java | 11 ++ .../neroland/nerospace/registry/ModItems.java | 5 +- .../nerospace/registry/ModRegistries.java | 1 + .../nerospace/storage/ItemStoreBlock.java | 49 +++++++ .../storage/ItemStoreBlockEntity.java | 136 ++++++++++++++++++ .../nerospace/blockstates/item_store.json | 7 + .../assets/nerospace/items/item_store.json | 6 + .../assets/nerospace/lang/en_us.json | 4 +- .../nerospace/models/block/item_store.json | 8 ++ .../nerospace/textures/block/item_store.png | Bin 0 -> 290 bytes .../textures/block/item_store_top.png | Bin 0 -> 412 bytes .../tags/block/mineable/pickaxe.json | 29 +++- .../minecraft/tags/block/needs_iron_tool.json | 19 ++- .../loot_table/blocks/item_store.json | 21 +++ .../data/nerospace/recipe/item_store.json | 16 +++ multiloader/fabric/build.gradle | 2 + .../main/resources/nerospace.accesswidener | 3 + .../fabric/src/main/templates/fabric.mod.json | 1 + multiloader/gradle.properties | 2 +- 22 files changed, 368 insertions(+), 32 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/item_store.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/item_store.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/item_store.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store_top.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/item_store.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/item_store.json create mode 100644 multiloader/fabric/src/main/resources/nerospace.accesswidener diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml index 636bece..73d5d87 100644 --- a/.github/workflows/multiloader.yml +++ b/.github/workflows/multiloader.yml @@ -1,14 +1,19 @@ name: Multiloader Build -# Builds the multiloader scaffold (multiloader/) — MultiLoader-Template layout +# Builds the multiloader (multiloader/) — MultiLoader-Template layout # (ModDevGradle common+neoforge, Fabric Loom fabric; no architectury-loom). # Independent of the root single-loader build (build.yml). # # STRICT: there is no continue-on-error, so ANY matrix cell that fails to build -# fails the whole workflow. The matrix therefore lists ONLY cells that are -# expected to build on a clean runner (both verified green via the gradle MCP on -# 2026-06-18). Cells that can't build on CI yet are listed (commented) below with -# the reason — re-add them here once they're unblocked. +# fails the whole workflow. All four loader x version cells are verified buildable +# from public artifacts (gradle MCP, 2026-06-19): +# - neoforge @ 26.1.2 -> NeoForge 26.1.2.76 +# - neoforge @ 26.2 -> NeoForge 26.2.0.3-beta (now on the public NeoForged Maven) +# - fabric @ 26.2 -> Fabric Loom 1.17 + fabric-api 0.152.1+26.2 (de-obf, no mappings) +# - fabric @ 26.1.2 -> Fabric Loom 1.17 + fabric-api 0.150.0+26.1.2; needs the access +# widener (fabric/src/main/resources/nerospace.accesswidener) because +# vanilla MC 26.1.2 kept BlockEntityType's constructor private +# (Mojang made it public in 26.2; NeoForge widens it on both). on: push: @@ -24,18 +29,18 @@ jobs: build: name: ${{ matrix.loader }} @ MC ${{ matrix.mc }} runs-on: ubuntu-latest - # No continue-on-error: any failed cell fails the workflow. strategy: - fail-fast: false # still run every cell so one failure doesn't mask others + fail-fast: false matrix: include: - loader: neoforge - mc: "26.1.2" # ModDevGradle + NeoForge 26.1.2.76 — verified green + mc: "26.1.2" + - loader: neoforge + mc: "26.2" + - loader: fabric + mc: "26.1.2" - loader: fabric - mc: "26.2" # Fabric Loom 1.17 + fabric-api 0.152.1+26.2 (de-obf, no mappings) — verified green - # PENDING — not buildable on a clean CI runner; re-add when unblocked: - # - { loader: fabric, mc: "26.1.2" } # Fabric never shipped MC 26.1.2 (26.1 -> 26.1.1 -> 26.2) - # - { loader: neoforge, mc: "26.2" } # needs NeoForge 26.2 userdev on Maven (or self-built to mavenLocal — see multiloader/README.md) + mc: "26.2" steps: - name: Checkout repository @@ -53,8 +58,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - # The multiloader build uses its OWN Gradle wrapper (9.5.1), independent of - # the repo root's 9.2.1 — Fabric Loom 1.17 requires Gradle >= 9.4. - name: Make Gradle wrapper executable run: chmod +x ./multiloader/gradlew diff --git a/multiloader/README.md b/multiloader/README.md index 8dffadc..5169a4c 100644 --- a/multiloader/README.md +++ b/multiloader/README.md @@ -14,8 +14,8 @@ Built via the gradle MCP on this machine: | Cell | Toolchain | 26.2 | 26.1.2 | | --- | --- | --- | --- | | `common` | ModDevGradle (NeoForm) | ✅ builds (`26.2-1`) | NeoForm `26.1.2-1` | -| `fabric` | Fabric Loom `1.17.11` | ✅ **builds** (`fabric-api 0.152.1+26.2`) | confirm API pin | -| `neoforge` | ModDevGradle (NeoForge) | ⏳ needs NeoForge 26.2 userdev (beta on Maven / self-build) | NeoForge `26.1.2.76` | +| `fabric` | Fabric Loom `1.17.11` | ✅ **builds** (`fabric-api 0.152.1+26.2`) | ✅ builds (`fabric-api 0.150.0+26.1.2`; needs access widener — see below) | +| `neoforge` | ModDevGradle (NeoForge) | ✅ builds (`26.2.0.3-beta`, on public NeoForged Maven) | NeoForge `26.1.2.76` | `./gradlew :common:build :fabric:build -Pminecraft_version=26.2` → **BUILD SUCCESSFUL**. @@ -64,7 +64,7 @@ cd multiloader Jars land in `multiloader//build/libs/`. -## NeoForge on 26.2 (the one pending cell) +## All four cells build (was: NeoForge 26.2 pending) NeoForge's own loader userdev for 26.2 may already be on Maven as a beta (`neo_version_26.2=26.2.0.1-beta` is pinned — the official MultiLoader-Template's @@ -111,3 +111,17 @@ build. Until then the root build remains the source of truth. - [architectury-loom #328 — no de-obf 26.x](https://github.com/architectury/architectury-loom/issues/328) - [jaredlll08/MultiLoader-Template](https://github.com/jaredlll08/MultiLoader-Template) · [official Fabric example (de-obf)](https://github.com/FabricMC/fabric-example-mod) - [NeoForm](https://projects.neoforged.net/neoforged/neoform) · [NeoForge](https://projects.neoforged.net/neoforged/neoforge) · [Fabric develop](https://fabricmc.net/develop) + +## Build matrix status (2026-06-19) + +All four loader × version cells build from **public artifacts** (verified via the gradle MCP), +and CI (`.github/workflows/multiloader.yml`) builds all four strictly (any failure fails the run): + +| | 26.1.2 | 26.2 | +| --- | --- | --- | +| **neoforge** | ✅ `26.1.2.76` | ✅ `26.2.0.3-beta` (public Maven) | +| **fabric** | ✅ (access widener) | ✅ `fabric-api 0.152.1+26.2` | + +Fabric @ 26.1.2 needs `fabric/src/main/resources/nerospace.accesswidener` because vanilla +MC 26.1.2 kept `BlockEntityType`'s constructor + `BlockEntitySupplier` private (Mojang made them +public in 26.2; NeoForge widens them on both). The widener is a no-op on 26.2. diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java new file mode 100644 index 0000000..66d959c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -0,0 +1,29 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; +import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; + +/** + * Block-entity types, shared by both loaders via {@link RegistrationProvider} over the vanilla + * {@code BLOCK_ENTITY_TYPE} registry. Registration is loader-agnostic; only mod-pipe capability + * exposure (next step) needs the platform seam. + */ +public final class ModBlockEntities { + + public static final RegistrationProvider> BLOCK_ENTITIES = + RegistrationProvider.get(Registries.BLOCK_ENTITY_TYPE, NerospaceCommon.MOD_ID); + + public static final RegistryEntry> ITEM_STORE = + BLOCK_ENTITIES.register("item_store", + key -> new BlockEntityType<>(ItemStoreBlockEntity::new, java.util.Set.of(ModBlocks.ITEM_STORE.get()))); + + private ModBlockEntities() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index ab46f55..927192d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -9,6 +9,7 @@ import net.minecraft.world.level.material.MapColor; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.storage.ItemStoreBlock; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** @@ -66,6 +67,16 @@ public final class ModBlocks { public static final RegistryEntry METEOR_ROCK = block("meteor_rock", p -> p.mapColor(MapColor.COLOR_BLACK).strength(3.0F, 4.0F).requiresCorrectToolForDrops().lightLevel(s -> 3).sound(SoundType.STONE)); + // Block entity — item storage (pilot for the block-entity + capability seam). + public static final RegistryEntry ITEM_STORE = BLOCKS.register("item_store", + key -> new ItemStoreBlock(BlockBehaviour.Properties.of() + .setId(key) + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL) + .noOcclusion())); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index ca0b961..3942f11 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -57,6 +57,7 @@ public final class ModItems { public static final RegistryEntry ALIEN_LAMP_ITEM = blockItem("alien_lamp", ModBlocks.ALIEN_LAMP); public static final RegistryEntry ALIEN_CRYSTAL_BLOCK_ITEM = blockItem("alien_crystal_block", ModBlocks.ALIEN_CRYSTAL_BLOCK); public static final RegistryEntry METEOR_ROCK_ITEM = blockItem("meteor_rock", ModBlocks.METEOR_ROCK); + public static final RegistryEntry ITEM_STORE_ITEM = blockItem("item_store", ModBlocks.ITEM_STORE); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -170,7 +171,9 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HELMET.get(), OXYGEN_SUIT_CHESTPLATE.get(), OXYGEN_SUIT_LEGGINGS.get(), OXYGEN_SUIT_BOOTS.get(), OXYGEN_SUIT_T2_HELMET.get(), OXYGEN_SUIT_T2_CHESTPLATE.get(), OXYGEN_SUIT_T2_LEGGINGS.get(), OXYGEN_SUIT_T2_BOOTS.get(), OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), - OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get())); + OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), + CreativeModeTabs.FUNCTIONAL_BLOCKS, + List.of(ITEM_STORE_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index dcf55c1..26d6199 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -13,5 +13,6 @@ private ModRegistries() { public static void init() { ModBlocks.init(); ModItems.init(); + ModBlockEntities.init(); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlock.java new file mode 100644 index 0000000..16fbe6f --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlock.java @@ -0,0 +1,49 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +/** Item Store block — right-click opens a vanilla 3-row chest GUI; holds an {@link ItemStoreBlockEntity}. */ +public class ItemStoreBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(ItemStoreBlock::new); + + public ItemStoreBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new ItemStoreBlockEntity(pos, state); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && level.getBlockEntity(pos) instanceof ItemStoreBlockEntity be) { + player.openMenu(be); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlockEntity.java new file mode 100644 index 0000000..d9b0bf1 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/ItemStoreBlockEntity.java @@ -0,0 +1,136 @@ +package za.co.neroland.nerospace.storage; + +import java.util.stream.IntStream; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Item Store — a 27-slot {@link WorldlyContainer} block entity. Being a vanilla Container, it + * interoperates with hoppers (and opens a vanilla {@link ChestMenu}) on BOTH loaders with no + * loader-specific code. Exposure to MOD pipes (NeoForge item capability / Fabric Transfer API) + * is the platform-seam layer added on top of this. + */ +public class ItemStoreBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int SIZE = 27; + private static final int[] ALL_SLOTS = IntStream.range(0, SIZE).toArray(); + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + + public ItemStoreBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.ITEM_STORE.get(), pos, state); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + ContainerHelper.saveAllItems(output, this.items); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.items.clear(); + ContainerHelper.loadAllItems(input, this.items); + } + + // --- MenuProvider (vanilla chest GUI — no custom MenuType needed) --------- + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.item_store"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return ChestMenu.threeRows(containerId, playerInventory, this); + } + + // --- WorldlyContainer (all slots, all faces) ------------------------------ + @Override + public int[] getSlotsForFace(Direction side) { + return ALL_SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return true; + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return true; + } + + // --- Container ------------------------------------------------------------ + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.stream().allMatch(ItemStack::isEmpty); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack result = ContainerHelper.removeItem(this.items, slot, amount); + if (!result.isEmpty()) { + this.setChanged(); + } + return result; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + stack.limitSize(this.getMaxStackSize()); + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + if (this.level == null || this.level.getBlockEntity(this.worldPosition) != this) { + return false; + } + return player.distanceToSqr( + this.worldPosition.getX() + 0.5, + this.worldPosition.getY() + 0.5, + this.worldPosition.getZ() + 0.5) <= 64.0; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/item_store.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/item_store.json new file mode 100644 index 0000000..fd91f02 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/item_store.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/item_store" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/item_store.json b/multiloader/common/src/main/resources/assets/nerospace/items/item_store.json new file mode 100644 index 0000000..b5e9aba --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/item_store.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/item_store" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index e6ee676..415c816 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -50,5 +50,7 @@ "item.nerospace.rocket_fuel_canister": "Rocket Fuel Canister", "item.nerospace.frame_casing": "Frame Casing", "item.nerospace.grav_striders": "Grav Striders", - "item.nerospace.drift_fleece": "Drift Fleece" + "item.nerospace.drift_fleece": "Drift Fleece", + "block.nerospace.item_store": "Item Store", + "container.nerospace.item_store": "Item Store" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/item_store.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/item_store.json new file mode 100644 index 0000000..c926e3e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/item_store.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cube_bottom_top", + "textures": { + "top": "nerospace:block/item_store_top", + "bottom": "nerospace:block/item_store_top", + "side": "nerospace:block/item_store" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store.png new file mode 100644 index 0000000000000000000000000000000000000000..ab1442b4f3f2d90c1e0f6dcc1c36080996be1dd5 GIT binary patch literal 290 zcmV+-0p0$IP)729SP4U}kLn!Bh)cYeRBtEdf;oqy^pp>aGq~Cn7RecB*u%tx1)EVzTpb zHh|^le(JvT6*D{K&O)p70Ji-%lehjj#fKfZdw!_&U;8t6QkPtTW?kxY31I1K`f4H& z`IjTS_UDQ2Q4a~ALeZ13lytnSVeX*LrC?P+W-In literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store_top.png new file mode 100644 index 0000000000000000000000000000000000000000..a6a629602e31440bcd2839f5690dcbf999679c84 GIT binary patch literal 412 zcmV;N0b~A&P)vruHW5bocAK-#6Rt4j23*769=1@h%D5Hu9~M1^_Ho2A$<_orNiDsp-AD3b@Wn zQ@!4d#Cq>gJv;&ch$3R}A}j)?tPz8s5EQ0D?;QXjQ9!-kAO?@6%A{t_1Uk!+C_pI< zrSyn|D1s;oX`+Bc;U+K~!<03MA^<>V`NWU2J55bwByLbj!*zCq%5|J$p*bIPmWvOJ z&6tI$Moo!={J`+XRmAfYLKfmWX2NNX)0000 (Lnet/minecraft/world/level/block/entity/BlockEntityType$BlockEntitySupplier;Ljava/util/Set;)V diff --git a/multiloader/fabric/src/main/templates/fabric.mod.json b/multiloader/fabric/src/main/templates/fabric.mod.json index 9293afa..abae495 100644 --- a/multiloader/fabric/src/main/templates/fabric.mod.json +++ b/multiloader/fabric/src/main/templates/fabric.mod.json @@ -6,6 +6,7 @@ "description": "Nerospace - multiloader build (Fabric).", "authors": ["${mod_authors}"], "license": "${mod_license}", + "accessWidener": "nerospace.accesswidener", "environment": "*", "entrypoints": { "main": ["za.co.neroland.nerospace.fabric.NerospaceFabric"], diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties index 3eaa3ce..7df1f60 100644 --- a/multiloader/gradle.properties +++ b/multiloader/gradle.properties @@ -45,7 +45,7 @@ neo_version_26.1.2=26.1.2.76 # 26.2 beta is on Maven per the official MultiLoader-Template default. If it ever # fails to resolve, self-build the 26.2.x branch to mavenLocal() (see README) and # set this to the version it publishes — that is the ONLY change needed. -neo_version_26.2=26.2.0.1-beta +neo_version_26.2=26.2.0.3-beta ## Fabric (https://fabricmc.net/develop) ------------------------------------ fabric_loader_version=0.19.3 From 1322fb6b8f5d5dbe4fb43e4502fbe3234b4f1d00 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:23:57 +0800 Subject: [PATCH 19/82] Expose item-storage capability to loaders Register the item-storage capability for the item_store block entity on both Fabric and NeoForge sides so automation/pipes can move items in/out. - Fabric: import Fabric Transfer API and register ItemStorage.SIDED for ModBlockEntities.ITEM_STORE using ContainerStorage.of(...). - NeoForge: add NeoForgeCapabilities that registers Capabilities.Item.BLOCK for ModBlockEntities.ITEM_STORE (using WorldlyContainerWrapper/VanillaContainerWrapper) and wire its registration into NerospaceNeoForge. This creates a seam between Fabric's ItemStorage and NeoForge's transfer API to expose the block entity inventory to platform transfer systems. --- .../nerospace/fabric/NerospaceFabric.java | 9 +++++ .../neoforge/NeoForgeCapabilities.java | 34 +++++++++++++++++++ .../nerospace/neoforge/NerospaceNeoForge.java | 1 + 3 files changed, 44 insertions(+) create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index a1f9cd4..08ae341 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -4,6 +4,8 @@ import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents; +import net.fabricmc.fabric.api.transfer.v1.item.ContainerStorage; +import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; @@ -11,6 +13,7 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModItems; /** @@ -31,6 +34,12 @@ public void onInitialize() { .register(output -> items.forEach(output::accept))); addOverworldOre("nerosium_ore_placed"); + + // Item-storage capability (Fabric Transfer API) — counterpart to NeoForge + // Capabilities.Item.BLOCK; lets mod pipes move items in/out of the item store. + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.ITEM_STORE.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java new file mode 100644 index 0000000..f2f2132 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -0,0 +1,34 @@ +package za.co.neroland.nerospace.neoforge; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.capabilities.Capabilities; +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; +import net.neoforged.neoforge.transfer.item.VanillaContainerWrapper; +import net.neoforged.neoforge.transfer.item.WorldlyContainerWrapper; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * NeoForge side of the item-storage capability seam. Exposes the item_store block entity's + * inventory as {@code Capabilities.Item.BLOCK} (NeoForge 26.x transfer API: a + * {@code ResourceHandler}) so mod pipes/automation can move items in and out — + * the counterpart to Fabric's {@code ItemStorage.SIDED}. + */ +public final class NeoForgeCapabilities { + + private NeoForgeCapabilities() { + } + + public static void register(IEventBus modEventBus) { + modEventBus.addListener(NeoForgeCapabilities::onRegisterCapabilities); + } + + private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.ITEM_STORE.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + } +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index b088b39..1988af5 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -24,6 +24,7 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NerospaceCommon.LOGGER.info("[Nerospace] NeoForge bootstrap"); NerospaceCommon.init(); NeoForgeRegistrationFactory.registerAll(modEventBus); + NeoForgeCapabilities.register(modEventBus); modEventBus.addListener(this::onBuildCreativeTabs); } From f2e73f06d0d50edc2a8a59f37629a1b083d1773b Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:35:40 +0800 Subject: [PATCH 20/82] Add battery block and energy API Add a passive Battery block and block entity with a mod-neutral energy API. Introduces NerospaceEnergyStorage and a simple EnergyBuffer implementation (CAPACITY=1_000_000, MAX_IO=10_000) with NBT save/load accessors. Register the Battery block, item and block entity, include it on the creative tab, and add models, textures, blockstate, loot table, and a crafting recipe. Update mineable/needs_iron tags and language entry. Wire up loader-specific seams: Fabric BlockApiLookup and NeoForge BlockCapability are registered to expose the battery's NerospaceEnergyStorage for cross-loader mod usage. --- .../nerospace/energy/EnergyBuffer.java | 60 ++++++++++ .../energy/NerospaceEnergyStorage.java | 21 ++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 11 ++ .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/storage/BatteryBlock.java | 37 ++++++ .../nerospace/storage/BatteryBlockEntity.java | 43 +++++++ .../assets/nerospace/blockstates/battery.json | 7 ++ .../assets/nerospace/items/battery.json | 6 + .../assets/nerospace/lang/en_us.json | 3 +- .../nerospace/models/block/battery.json | 105 ++++++++++++++++++ .../nerospace/textures/block/battery.png | Bin 0 -> 299 bytes .../nerospace/textures/block/battery_top.png | Bin 0 -> 416 bytes .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../nerospace/loot_table/blocks/battery.json | 21 ++++ .../data/nerospace/recipe/battery.json | 17 +++ .../nerospace/fabric/NerospaceFabric.java | 13 +++ .../neoforge/NeoForgeCapabilities.java | 27 ++++- 19 files changed, 377 insertions(+), 8 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/energy/NerospaceEnergyStorage.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/battery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/battery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/battery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/battery.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/battery_top.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/battery.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/battery.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java new file mode 100644 index 0000000..d3dd20b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java @@ -0,0 +1,60 @@ +package za.co.neroland.nerospace.energy; + +/** + * Simple bounded energy buffer backing a block entity. Values fit in an int (NBT uses + * {@code putInt}/{@code getIntOr} in 26.x); the interface is {@code long} for headroom. + */ +public final class EnergyBuffer implements NerospaceEnergyStorage { + + private int amount; + private final int capacity; + private final int maxInsert; + private final int maxExtract; + private final Runnable onChanged; + + public EnergyBuffer(int capacity, int maxInsert, int maxExtract, Runnable onChanged) { + this.capacity = capacity; + this.maxInsert = maxInsert; + this.maxExtract = maxExtract; + this.onChanged = onChanged; + } + + @Override + public long getAmount() { + return this.amount; + } + + @Override + public long getCapacity() { + return this.capacity; + } + + @Override + public long insert(long maxAmount, boolean simulate) { + int accepted = (int) Math.max(0, Math.min(maxAmount, Math.min(this.maxInsert, this.capacity - this.amount))); + if (accepted > 0 && !simulate) { + this.amount += accepted; + this.onChanged.run(); + } + return accepted; + } + + @Override + public long extract(long maxAmount, boolean simulate) { + int removed = (int) Math.max(0, Math.min(maxAmount, Math.min(this.maxExtract, this.amount))); + if (removed > 0 && !simulate) { + this.amount -= removed; + this.onChanged.run(); + } + return removed; + } + + /** Raw accessors for NBT save/load. */ + public int getRaw() { + return this.amount; + } + + public void setRaw(int value) { + this.amount = Math.max(0, Math.min(this.capacity, value)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/NerospaceEnergyStorage.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/NerospaceEnergyStorage.java new file mode 100644 index 0000000..e080e2e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/NerospaceEnergyStorage.java @@ -0,0 +1,21 @@ +package za.co.neroland.nerospace.energy; + +/** + * Loader-neutral energy storage interface. Each loader exposes it through its own block-lookup + * mechanism (NeoForge {@code BlockCapability}, Fabric {@code BlockApiLookup}) so the mod's own + * generators, batteries and machines interoperate on both loaders. Cross-mod energy interop + * (NeoForge's {@code Capabilities.Energy} / the Fabric energy libraries) is deferred — those + * libraries have not ported to 26.x, and the mod is standalone for now. + */ +public interface NerospaceEnergyStorage { + + long getAmount(); + + long getCapacity(); + + /** @return energy actually inserted (0 if none). */ + long insert(long maxAmount, boolean simulate); + + /** @return energy actually extracted (0 if none). */ + long extract(long maxAmount, boolean simulate); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 66d959c..151960a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -5,6 +5,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; +import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; /** @@ -21,6 +22,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("item_store", key -> new BlockEntityType<>(ItemStoreBlockEntity::new, java.util.Set.of(ModBlocks.ITEM_STORE.get()))); + public static final RegistryEntry> BATTERY = + BLOCK_ENTITIES.register("battery", + key -> new BlockEntityType<>(BatteryBlockEntity::new, java.util.Set.of(ModBlocks.BATTERY.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 927192d..3908c84 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -9,6 +9,7 @@ import net.minecraft.world.level.material.MapColor; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -77,6 +78,16 @@ public final class ModBlocks { .sound(SoundType.METAL) .noOcclusion())); + + public static final RegistryEntry BATTERY = BLOCKS.register("battery", + key -> new BatteryBlock(BlockBehaviour.Properties.of() + .setId(key) + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL) + .noOcclusion())); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 3942f11..11fc1a8 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -58,6 +58,7 @@ public final class ModItems { public static final RegistryEntry ALIEN_CRYSTAL_BLOCK_ITEM = blockItem("alien_crystal_block", ModBlocks.ALIEN_CRYSTAL_BLOCK); public static final RegistryEntry METEOR_ROCK_ITEM = blockItem("meteor_rock", ModBlocks.METEOR_ROCK); public static final RegistryEntry ITEM_STORE_ITEM = blockItem("item_store", ModBlocks.ITEM_STORE); + public static final RegistryEntry BATTERY_ITEM = blockItem("battery", ModBlocks.BATTERY); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -173,7 +174,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlock.java new file mode 100644 index 0000000..e9cc19c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlock.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Battery block — holds a {@link BatteryBlockEntity} energy buffer. */ +public class BatteryBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(BatteryBlock::new); + + public BatteryBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new BatteryBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlockEntity.java new file mode 100644 index 0000000..f92f544 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/BatteryBlockEntity.java @@ -0,0 +1,43 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Battery — a passive energy buffer block entity. Exposes {@link NerospaceEnergyStorage} to the + * mod's energy capability/lookup on both loaders (see the loader entry points). No ticker, no GUI. + */ +public class BatteryBlockEntity extends BlockEntity { + + public static final int CAPACITY = 1_000_000; + public static final int MAX_IO = 10_000; + + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_IO, MAX_IO, this::setChanged); + + public BatteryBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.BATTERY.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/battery.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/battery.json new file mode 100644 index 0000000..3e9d52d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/battery.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/battery" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/battery.json b/multiloader/common/src/main/resources/assets/nerospace/items/battery.json new file mode 100644 index 0000000..d441c51 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/battery.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/battery" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 415c816..eb40b48 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -52,5 +52,6 @@ "item.nerospace.grav_striders": "Grav Striders", "item.nerospace.drift_fleece": "Drift Fleece", "block.nerospace.item_store": "Item Store", - "container.nerospace.item_store": "Item Store" + "container.nerospace.item_store": "Item Store", + "block.nerospace.battery": "Battery" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/battery.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/battery.json new file mode 100644 index 0000000..59649fc --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/battery.json @@ -0,0 +1,105 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 1, + 0, + 1 + ], + "to": [ + 15, + 14, + 15 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 3, + 14, + 3 + ], + "to": [ + 7, + 16, + 7 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 9, + 14, + 9 + ], + "to": [ + 13, + 16, + 13 + ] + } + ], + "textures": { + "particle": "nerospace:block/battery", + "side": "nerospace:block/battery", + "top": "nerospace:block/battery_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/battery.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/battery.png new file mode 100644 index 0000000000000000000000000000000000000000..70eb5011d576c09b67208c5444a91b7d2c4b02fa GIT binary patch literal 299 zcmV+`0o4A9P)6MKST7$Cf4Aclv&N|G^8k z0KolvK~;eystQ#F0Eh?x=rMgOcFSi2P=UNB$&*B8g5;2M_GFSnf;x*?MC?TZOfkKiZH>XKktDuq#m8d<0Ayxo;`uuXVjw@yK{If$4&dfGZ}~(-bp4q*$URH{ z4DcE2zrZ^NqNd={x?G|UbirhPO!w<$=;#e(i%U3SB2Z`5a~@j*@WPq87(~Rd?gqa8 x4RJnar>VFXAFfgQwJJYll0D{IJH7fl{{r+L%g2I6$k_k@002ovPDHLkV1ln4eN_Me literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/battery_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/battery_top.png new file mode 100644 index 0000000000000000000000000000000000000000..ebf6e05bcf9b5e1db77df56af7d61ee7cc8ebdee GIT binary patch literal 416 zcmV;R0bl-!P)1p=5Jg{%DzS(ygi&E!1Qn`PPfu&+6*wdHu;hDg$_YxVM99Q4_-eK;cUOy6r1<)6#8UajpO$cb@HL z#SB0sQY~sDQDDFCeoUKJOB*su_*MWA!@$eK2&@gz-kpeHFrX6aP$SR}&TC#R86}WA zZ}LSVqXe8QI1c?};hbGd!P--s)uOid>=HO7IrKYIc^+m4Q&_q4w09=}YpPEL=^*VJ zLGHXYa_1Q(AQGvY7MRv}DJQcK!(e_#3Cyb{{k}savdT^{yiX5sTV7N6lHJGK6r%2X zlwfu=tW<8Ai(+PJx(ZBHX%1ELChERZ6{7?SUvlVo z+>lW>t&!@#VhS{QWlafT+Kz(zeaE5SSw92VZdSi!rga?pi)UZudb#n0f**bW0000< KMNUMnLSTY?K(adk literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json index d73ed09..80deec3 100644 --- a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -20,6 +20,7 @@ "nerospace:alien_lamp", "nerospace:alien_crystal_block", "nerospace:meteor_rock", - "nerospace:item_store" + "nerospace:item_store", + "nerospace:battery" ] } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json index d3c3919..64b86e0 100644 --- a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json @@ -12,6 +12,7 @@ "nerospace:glacite_block", "nerospace:station_floor", "nerospace:station_wall", - "nerospace:item_store" + "nerospace:item_store", + "nerospace:battery" ] } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/battery.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/battery.json new file mode 100644 index 0000000..b0f0967 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/battery.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:battery" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/battery" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/battery.json b/multiloader/common/src/main/resources/data/nerospace/recipe/battery.json new file mode 100644 index 0000000..8fefaaa --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/battery.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "I": "#c:ingots/nerosium", + "N": "#c:ingots/nerosteel", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "NRN", + "RIR", + "NRN" + ], + "result": { + "id": "nerospace:battery" + } +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 08ae341..481bd99 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -4,8 +4,10 @@ import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents; +import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; import net.fabricmc.fabric.api.transfer.v1.item.ContainerStorage; import net.fabricmc.fabric.api.transfer.v1.item.ItemStorage; +import net.minecraft.core.Direction; import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; @@ -13,6 +15,7 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModItems; @@ -24,6 +27,12 @@ */ public final class NerospaceFabric implements ModInitializer { + /** Mod-owned energy lookup; mirrors the NeoForge energy BlockCapability of the same id. */ + public static final BlockApiLookup ENERGY = + BlockApiLookup.get( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "energy"), + NerospaceEnergyStorage.class, Direction.class); + @Override public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); @@ -40,6 +49,10 @@ public void onInitialize() { ItemStorage.SIDED.registerForBlockEntity( (be, direction) -> ContainerStorage.of(be, direction), ModBlockEntities.ITEM_STORE.get()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.BATTERY.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index f2f2132..7270f9b 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -1,21 +1,35 @@ package za.co.neroland.nerospace.neoforge; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.capabilities.BlockCapability; import net.neoforged.neoforge.capabilities.Capabilities; import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent; import net.neoforged.neoforge.transfer.item.VanillaContainerWrapper; import net.neoforged.neoforge.transfer.item.WorldlyContainerWrapper; +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; /** - * NeoForge side of the item-storage capability seam. Exposes the item_store block entity's - * inventory as {@code Capabilities.Item.BLOCK} (NeoForge 26.x transfer API: a - * {@code ResourceHandler}) so mod pipes/automation can move items in and out — - * the counterpart to Fabric's {@code ItemStorage.SIDED}. + * NeoForge side of the capability seams: + *

    + *
  • item storage via the standard {@code Capabilities.Item.BLOCK} (26.x transfer API);
  • + *
  • energy via a mod-owned {@link #ENERGY} {@link BlockCapability} over + * {@link NerospaceEnergyStorage} (the Fabric side uses a matching {@code BlockApiLookup}) — + * self-contained until the platforms' energy libraries port to 26.x.
  • + *
*/ public final class NeoForgeCapabilities { + /** Mod-owned energy capability; mirrors the Fabric {@code BlockApiLookup} of the same id. */ + public static final BlockCapability ENERGY = + BlockCapability.createSided( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "energy"), + NerospaceEnergyStorage.class); + private NeoForgeCapabilities() { } @@ -30,5 +44,10 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { (be, side) -> side != null ? new WorldlyContainerWrapper(be, side) : VanillaContainerWrapper.of(be)); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.BATTERY.get(), + (be, side) -> be.getEnergy()); } } From c5321afa252b31b24201928b5b7459af515327ae Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:40:33 +0800 Subject: [PATCH 21/82] Add fluid tank block, BE, and fluid API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a single‑fluid tank feature: NerospaceFluidStorage interface and FluidTank implementation (bounded mB storage with change callback). Add FluidTankBlock and FluidTankBlockEntity (16,000 mB capacity) with save/load of fluid and amount. Register the block, item and block entity, add recipe, loot table, blockstate/model/textures and language entry, and update mining/tool tags. Wire the mod-owned fluid lookup/capability for Fabric (BlockApiLookup) and NeoForge (BlockCapability) so the tank is discoverable via the mod's fluid API. Platform-standard fluid handler integration is noted as a deferred enhancement. --- .../neroland/nerospace/fluid/FluidTank.java | 85 ++++ .../fluid/NerospaceFluidStorage.java | 25 ++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 11 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/storage/FluidTankBlock.java | 37 ++ .../storage/FluidTankBlockEntity.java | 44 ++ .../nerospace/blockstates/fluid_tank.json | 7 + .../assets/nerospace/items/fluid_tank.json | 6 + .../assets/nerospace/lang/en_us.json | 3 +- .../nerospace/models/block/fluid_tank.json | 425 ++++++++++++++++++ .../nerospace/textures/block/fluid_tank.png | Bin 0 -> 292 bytes .../textures/block/fluid_tank_core.png | Bin 0 -> 306 bytes .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../loot_table/blocks/fluid_tank.json | 21 + .../data/nerospace/recipe/fluid_tank.json | 16 + .../nerospace/fabric/NerospaceFabric.java | 11 + .../neoforge/NeoForgeCapabilities.java | 12 + 19 files changed, 713 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/FluidTank.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/NerospaceFluidStorage.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/fluid_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/fluid_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/fluid_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank_core.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fluid_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/fluid_tank.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/FluidTank.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/FluidTank.java new file mode 100644 index 0000000..e4f05cd --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/FluidTank.java @@ -0,0 +1,85 @@ +package za.co.neroland.nerospace.fluid; + +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; + +/** Single-fluid bounded tank (millibuckets) backing a block entity. */ +public final class FluidTank implements NerospaceFluidStorage { + + private Fluid fluid = Fluids.EMPTY; + private long amount; + private final long capacity; + private final Runnable onChanged; + + public FluidTank(long capacity, Runnable onChanged) { + this.capacity = capacity; + this.onChanged = onChanged; + } + + @Override + public Fluid getFluid() { + return this.fluid; + } + + @Override + public long getAmount() { + return this.amount; + } + + @Override + public long getCapacity() { + return this.capacity; + } + + @Override + public long fill(Fluid fluid, long amount, boolean simulate) { + if (amount <= 0 || fluid == Fluids.EMPTY) { + return 0; + } + if (this.fluid != Fluids.EMPTY && this.fluid != fluid) { + return 0; + } + long filled = Math.min(amount, this.capacity - this.amount); + if (filled > 0 && !simulate) { + if (this.fluid == Fluids.EMPTY) { + this.fluid = fluid; + } + this.amount += filled; + this.onChanged.run(); + } + return filled; + } + + @Override + public long drain(long amount, boolean simulate) { + if (amount <= 0 || this.amount == 0) { + return 0; + } + long drained = Math.min(amount, this.amount); + if (!simulate) { + this.amount -= drained; + if (this.amount == 0) { + this.fluid = Fluids.EMPTY; + } + this.onChanged.run(); + } + return drained; + } + + // Raw accessors for NBT save/load. + public Fluid getRawFluid() { + return this.fluid; + } + + public int getRawAmount() { + return (int) this.amount; + } + + public void setRaw(Fluid fluid, int amount) { + this.fluid = fluid; + this.amount = Math.max(0, Math.min((int) this.capacity, amount)); + if (this.amount == 0) { + this.fluid = Fluids.EMPTY; + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/NerospaceFluidStorage.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/NerospaceFluidStorage.java new file mode 100644 index 0000000..c19fd3b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/NerospaceFluidStorage.java @@ -0,0 +1,25 @@ +package za.co.neroland.nerospace.fluid; + +import net.minecraft.world.level.material.Fluid; + +/** + * Loader-neutral single-fluid tank interface (amount in millibuckets). Exposed per loader via a + * mod-owned capability/lookup, like {@link za.co.neroland.nerospace.energy.NerospaceEnergyStorage}. + * Platform-standard fluid handlers (NeoForge {@code Capabilities.Fluid} / Fabric + * {@code FluidStorage}) + vanilla bucket interop are a deferred enhancement. + */ +public interface NerospaceFluidStorage { + + /** The stored fluid, or {@code Fluids.EMPTY} if empty. */ + Fluid getFluid(); + + long getAmount(); + + long getCapacity(); + + /** Fill with {@code fluid} (must match the stored fluid unless empty). @return mB filled. */ + long fill(Fluid fluid, long amount, boolean simulate); + + /** Drain the stored fluid. @return mB drained. */ + long drain(long amount, boolean simulate); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 151960a..3177f15 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -6,6 +6,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; import za.co.neroland.nerospace.storage.BatteryBlockEntity; +import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; /** @@ -26,6 +27,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("battery", key -> new BlockEntityType<>(BatteryBlockEntity::new, java.util.Set.of(ModBlocks.BATTERY.get()))); + public static final RegistryEntry> FLUID_TANK = + BLOCK_ENTITIES.register("fluid_tank", + key -> new BlockEntityType<>(FluidTankBlockEntity::new, java.util.Set.of(ModBlocks.FLUID_TANK.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 3908c84..cbae73b 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -10,6 +10,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.storage.BatteryBlock; +import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -88,6 +89,16 @@ public final class ModBlocks { .sound(SoundType.METAL) .noOcclusion())); + + public static final RegistryEntry FLUID_TANK = BLOCKS.register("fluid_tank", + key -> new FluidTankBlock(BlockBehaviour.Properties.of() + .setId(key) + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL) + .noOcclusion())); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 11fc1a8..a2b2f23 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -59,6 +59,7 @@ public final class ModItems { public static final RegistryEntry METEOR_ROCK_ITEM = blockItem("meteor_rock", ModBlocks.METEOR_ROCK); public static final RegistryEntry ITEM_STORE_ITEM = blockItem("item_store", ModBlocks.ITEM_STORE); public static final RegistryEntry BATTERY_ITEM = blockItem("battery", ModBlocks.BATTERY); + public static final RegistryEntry FLUID_TANK_ITEM = blockItem("fluid_tank", ModBlocks.FLUID_TANK); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -174,7 +175,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlock.java new file mode 100644 index 0000000..4938aea --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlock.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Fluid Tank block — holds a {@link FluidTankBlockEntity}. */ +public class FluidTankBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(FluidTankBlock::new); + + public FluidTankBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new FluidTankBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlockEntity.java new file mode 100644 index 0000000..f7ae314 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/FluidTankBlockEntity.java @@ -0,0 +1,44 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.fluid.FluidTank; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Fluid Tank — a single-fluid buffer block entity, exposed via the mod's fluid capability/lookup. */ +public class FluidTankBlockEntity extends BlockEntity { + + public static final int CAPACITY = 16_000; // mB (16 buckets) + + private final FluidTank tank = new FluidTank(CAPACITY, this::setChanged); + + public FluidTankBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.FLUID_TANK.get(), pos, state); + } + + public NerospaceFluidStorage getTank() { + return this.tank; + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putString("Fluid", BuiltInRegistries.FLUID.getKey(this.tank.getRawFluid()).toString()); + output.putInt("Amount", this.tank.getRawAmount()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + Fluid fluid = BuiltInRegistries.FLUID.getValue(Identifier.parse(input.getStringOr("Fluid", "minecraft:empty"))); + this.tank.setRaw(fluid, input.getIntOr("Amount", 0)); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/fluid_tank.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/fluid_tank.json new file mode 100644 index 0000000..1c83a4f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/fluid_tank.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/fluid_tank" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/fluid_tank.json b/multiloader/common/src/main/resources/assets/nerospace/items/fluid_tank.json new file mode 100644 index 0000000..09f1e79 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/fluid_tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/fluid_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index eb40b48..80963f1 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -53,5 +53,6 @@ "item.nerospace.drift_fleece": "Drift Fleece", "block.nerospace.item_store": "Item Store", "container.nerospace.item_store": "Item Store", - "block.nerospace.battery": "Battery" + "block.nerospace.battery": "Battery", + "block.nerospace.fluid_tank": "Fluid Tank" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/fluid_tank.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/fluid_tank.json new file mode 100644 index 0000000..6ff0bca --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/fluid_tank.json @@ -0,0 +1,425 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#core" + }, + "east": { + "texture": "#core" + }, + "north": { + "texture": "#core" + }, + "south": { + "texture": "#core" + }, + "up": { + "texture": "#core" + }, + "west": { + "texture": "#core" + } + }, + "from": [ + 2, + 2, + 2 + ], + "to": [ + 14, + 14, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 2, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 14 + ], + "to": [ + 2, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 0 + ], + "to": [ + 14, + 2, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 14 + ], + "to": [ + 14, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 2 + ], + "to": [ + 2, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 2 + ], + "to": [ + 16, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 0 + ], + "to": [ + 14, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 14 + ], + "to": [ + 14, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "core": "nerospace:block/fluid_tank_core", + "particle": "nerospace:block/fluid_tank", + "side": "nerospace:block/fluid_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank.png new file mode 100644 index 0000000000000000000000000000000000000000..2b004878c65158d124d73accc7a00416f9d8123a GIT binary patch literal 292 zcmV+<0o(qGP)WJH1iF1na*wNy2AeD?WtJ)E!gjU6!o zz*1MV)&KxzHk=u)HMn~}n;GuY<=cP>zO~6#Gc!X(05ebkz}no8ZA7V-tj`29I!Ku~U`47tLjg`|fg{vxwvU0;Da zxjU9RGB~|%9YjqiXlK9#rz~~tja!=DMW25S6il(QAJl~!Q%(}Bj^qIKg1h4hF_LPb qrc!2Gs}}vx3E7Ex5aY1-cm4qIS-5LL4|$~k0000 FLUID = + BlockApiLookup.get( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "fluid"), + NerospaceFluidStorage.class, Direction.class); + @Override public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); @@ -53,6 +60,10 @@ public void onInitialize() { ENERGY.registerForBlockEntity( (be, direction) -> be.getEnergy(), ModBlockEntities.BATTERY.get()); + + FLUID.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.FLUID_TANK.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 7270f9b..e8375c5 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -11,6 +11,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; /** @@ -30,6 +31,12 @@ public final class NeoForgeCapabilities { Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "energy"), NerospaceEnergyStorage.class); + /** Mod-owned fluid capability; mirrors the Fabric {@code BlockApiLookup} of the same id. */ + public static final BlockCapability FLUID = + BlockCapability.createSided( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "fluid"), + NerospaceFluidStorage.class); + private NeoForgeCapabilities() { } @@ -49,5 +56,10 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.BATTERY.get(), (be, side) -> be.getEnergy()); + + event.registerBlockEntity( + FLUID, + ModBlockEntities.FLUID_TANK.get(), + (be, side) -> be.getTank()); } } From 77fa26e488c5414d500da53cf891d041f4c54f31 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:50:18 +0800 Subject: [PATCH 22/82] Add combustion generator block and block entity Introduce a Combustion Generator machine: adds CombustionGeneratorBlock and CombustionGeneratorBlockEntity that burn fuel items into energy (single fuel slot, capacity 100000, FE_PER_TICK 20). Add EnergyBuffer.generate(int) to support internal generation for generators. Register the block, block entity and item, update creative tab and block tags (mineable/needs_iron_tool). Add blockstate, model, textures, loot table, recipe and localization entry. Wire platform integrations: Fabric item/energy capabilities and NeoForge item/energy capability registrations. Save/load and container behaviour implemented on the block entity; common fuels and burn values defined. --- .../nerospace/energy/EnergyBuffer.java | 9 + .../machine/CombustionGeneratorBlock.java | 71 +++++++ .../CombustionGeneratorBlockEntity.java | 176 ++++++++++++++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 10 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../blockstates/combustion_generator.json | 19 ++ .../nerospace/items/combustion_generator.json | 6 + .../assets/nerospace/lang/en_us.json | 3 +- .../models/block/combustion_generator.json | 74 ++++++++ .../textures/block/combustion_generator.png | Bin 0 -> 393 bytes .../block/combustion_generator_front.png | Bin 0 -> 425 bytes .../block/combustion_generator_top.png | Bin 0 -> 474 bytes .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../blocks/combustion_generator.json | 21 +++ .../recipe/combustion_generator.json | 17 ++ .../nerospace/fabric/NerospaceFabric.java | 7 + .../neoforge/NeoForgeCapabilities.java | 11 ++ 19 files changed, 434 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/combustion_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/combustion_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/combustion_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_front.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_top.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/combustion_generator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/combustion_generator.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java index d3dd20b..b45c1f6 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java @@ -49,6 +49,15 @@ public long extract(long maxAmount, boolean simulate) { return removed; } + /** Internal generation (bypasses the insert limit) — for generators. */ + public void generate(int amount) { + int add = (int) Math.max(0, Math.min(amount, this.capacity - this.amount)); + if (add > 0) { + this.amount += add; + this.onChanged.run(); + } + } + /** Raw accessors for NBT save/load. */ public int getRaw() { return this.amount; diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java new file mode 100644 index 0000000..4fb88a2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java @@ -0,0 +1,71 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Combustion Generator block — directional, ticks its {@link CombustionGeneratorBlockEntity}. */ +public class CombustionGeneratorBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(CombustionGeneratorBlock::new); + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + @SuppressWarnings("this-escape") + public CombustionGeneratorBlock(Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CombustionGeneratorBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.COMBUSTION_GENERATOR.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java new file mode 100644 index 0000000..1073e7e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java @@ -0,0 +1,176 @@ +package za.co.neroland.nerospace.machine; + +import java.util.stream.IntStream; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Combustion Generator — burns a fuel item into energy. GUI-less for now: fuel is inserted by + * hoppers/pipes into the single fuel slot (item capability), energy is pulled by pipes (energy + * capability). First ticking machine: proves the item + energy seams together with a + * {@code BlockEntityTicker}. The menu/screen comes with the menu seam. + */ +public class CombustionGeneratorBlockEntity extends BlockEntity implements WorldlyContainer { + + public static final int FUEL_SLOT = 0; + public static final int SIZE = 1; + public static final int CAPACITY = 100_000; + public static final int FE_PER_TICK = 20; + private static final int[] FUEL_SLOTS = IntStream.range(0, SIZE).toArray(); + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, 0, FE_PER_TICK * 64, this::setChanged); + private int burnTime; + private int maxBurnTime; + + public CombustionGeneratorBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.COMBUSTION_GENERATOR.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + /** Burn value (ticks) for a fuel item, or 0 if not accepted. */ + public static int fuelValue(ItemStack stack) { + if (stack.is(Items.COAL) || stack.is(Items.CHARCOAL)) { + return 1_600; + } + if (stack.is(Items.COAL_BLOCK)) { + return 16_000; + } + if (stack.is(Items.BLAZE_ROD)) { + return 2_400; + } + if (stack.is(ModItems.ROCKET_FUEL_CANISTER.get())) { + return 4_000; + } + return 0; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + if (this.burnTime > 0) { + if (this.energy.getAmount() < this.energy.getCapacity()) { + this.burnTime--; + this.energy.generate(FE_PER_TICK); + } + } else { + ItemStack fuel = this.items.get(FUEL_SLOT); + int value = fuelValue(fuel); + if (value > 0 && this.energy.getAmount() < this.energy.getCapacity()) { + this.burnTime = value; + this.maxBurnTime = value; + fuel.shrink(1); + this.setChanged(); + } else if (this.maxBurnTime != 0) { + this.maxBurnTime = 0; + } + } + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + output.putInt("BurnTime", this.burnTime); + output.putInt("MaxBurnTime", this.maxBurnTime); + output.store("Fuel", ItemStack.OPTIONAL_CODEC, this.items.get(FUEL_SLOT)); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + this.burnTime = input.getIntOr("BurnTime", 0); + this.maxBurnTime = input.getIntOr("MaxBurnTime", 0); + this.items.set(FUEL_SLOT, input.read("Fuel", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + + // --- WorldlyContainer: fuel in only --------------------------------------- + @Override + public int[] getSlotsForFace(Direction side) { + return FUEL_SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return fuelValue(stack) > 0; + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return fuelValue(stack) == 0; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return fuelValue(stack) > 0; + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.get(FUEL_SLOT).isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 3177f15..1f9f510 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -5,6 +5,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; +import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; @@ -31,6 +32,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("fluid_tank", key -> new BlockEntityType<>(FluidTankBlockEntity::new, java.util.Set.of(ModBlocks.FLUID_TANK.get()))); + public static final RegistryEntry> COMBUSTION_GENERATOR = + BLOCK_ENTITIES.register("combustion_generator", + key -> new BlockEntityType<>(CombustionGeneratorBlockEntity::new, java.util.Set.of(ModBlocks.COMBUSTION_GENERATOR.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index cbae73b..df8d235 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -9,6 +9,7 @@ import net.minecraft.world.level.material.MapColor; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.machine.CombustionGeneratorBlock; import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; @@ -99,6 +100,15 @@ public final class ModBlocks { .sound(SoundType.METAL) .noOcclusion())); + + public static final RegistryEntry COMBUSTION_GENERATOR = BLOCKS.register("combustion_generator", + key -> new CombustionGeneratorBlock(BlockBehaviour.Properties.of() + .setId(key) + .mapColor(MapColor.METAL) + .strength(3.5F, 6.0F) + .requiresCorrectToolForDrops() + .sound(SoundType.METAL))); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index a2b2f23..f95e51d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -60,6 +60,7 @@ public final class ModItems { public static final RegistryEntry ITEM_STORE_ITEM = blockItem("item_store", ModBlocks.ITEM_STORE); public static final RegistryEntry BATTERY_ITEM = blockItem("battery", ModBlocks.BATTERY); public static final RegistryEntry FLUID_TANK_ITEM = blockItem("fluid_tank", ModBlocks.FLUID_TANK); + public static final RegistryEntry COMBUSTION_GENERATOR_ITEM = blockItem("combustion_generator", ModBlocks.COMBUSTION_GENERATOR); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -175,7 +176,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/combustion_generator.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/combustion_generator.json new file mode 100644 index 0000000..f6482f6 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/combustion_generator.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "nerospace:block/combustion_generator", + "y": 90 + }, + "facing=north": { + "model": "nerospace:block/combustion_generator" + }, + "facing=south": { + "model": "nerospace:block/combustion_generator", + "y": 180 + }, + "facing=west": { + "model": "nerospace:block/combustion_generator", + "y": 270 + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/combustion_generator.json b/multiloader/common/src/main/resources/assets/nerospace/items/combustion_generator.json new file mode 100644 index 0000000..f1f38ff --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/combustion_generator.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/combustion_generator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 80963f1..d5bd2dd 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -54,5 +54,6 @@ "block.nerospace.item_store": "Item Store", "container.nerospace.item_store": "Item Store", "block.nerospace.battery": "Battery", - "block.nerospace.fluid_tank": "Fluid Tank" + "block.nerospace.fluid_tank": "Fluid Tank", + "block.nerospace.combustion_generator": "Combustion Generator" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/combustion_generator.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/combustion_generator.json new file mode 100644 index 0000000..96b3204 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/combustion_generator.json @@ -0,0 +1,74 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#front" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 13, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 9, + 13, + 9 + ], + "to": [ + 14, + 16, + 14 + ] + } + ], + "textures": { + "front": "nerospace:block/combustion_generator_front", + "particle": "nerospace:block/combustion_generator", + "side": "nerospace:block/combustion_generator", + "top": "nerospace:block/combustion_generator_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator.png new file mode 100644 index 0000000000000000000000000000000000000000..8ac0c01df6a69f08b5a8e176183207984dd5807b GIT binary patch literal 393 zcmV;40e1e0P)TaB06fb`{vDiZ|83Q^7_p`stvH)ZuG(!Ls?j&IKdc$b58H2oO6n6$z?j8C?JXx zn!2JatTqc#S=a#E)~O#>flXbh0)L`&j=^xmZo8qWt22R20vHTO06ZVoa5#HinQ1^4 zD3Hdou#|;m(bCRFBvaH9Vv&Y9Pf%uc4r1{#} z7(-K6YPBU9NGP|;|BYW3e3GxX-PN-{9lQ{QWJv<~nk?%Djd}jb{Mc6&vMcRN*0QiH z<9o>0f%H{0WpHZt@v1;P3G-?=)s;StRSQv^Xcn5m!{d`~pEL=Xzc^8xkd7zeCN_1Y nvV0|%>ih~axCZ>|Tm82m&pN%AC^dy{00000NkvXXu0mjf0B*6L literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_front.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_front.png new file mode 100644 index 0000000000000000000000000000000000000000..01622698b92556e3b4e358af52d2fd785ee92af1 GIT binary patch literal 425 zcmV;a0apHrP)&_@U*gwUaj4)ML@Va1BKBzN5X{&)9o=l*f}fq&Q@06g5?;<@|sr^4Q-i}CAI z1z43idZR9iJW~m&NMJi1q#}Xm2N0aWX?rTiq*dN70F-3`2{Ne#lUg9e)D#UuzX~m^ z%3L48^8-Z|oWURzPTNyZ!I}DR!W2NZobvfr^V{tsq#~&SESi826Dfej`!z7U(JT6w zD&~f3czysDP4o`r9^B)r?~q~h8?Jv; zmL{JziRTrd$TLM;3|bqNWlRcaJ20uGwYX8a5wo2R0AQJk>W~Cy8ss;VPpCRD2nCBK zI6c2m`$+9%iKK TjF)kQ00000NkvXXu0mjfZF96` literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_top.png new file mode 100644 index 0000000000000000000000000000000000000000..92f122c07958156822249c8ae10f1c1107392004 GIT binary patch literal 474 zcmV<00VV#4P)t_AHe_#&q>BAKOiO9Ggb^xfd0>GLv z7JFxqg+g~NLZs}rcg#X#A~L))EaVbZR;9%Ogq1 z4ZV)j0%V~k3g;m?^cqetG+iP2)N1=e+v(dlEN|=cfF8s37>aW{eY$At;fBir1Q0@Xo}d zV+iC!uj3MDVTs-uoU_kX5QattvQVh9dX@rf#(2&d5FcddHRsad!j&Am!D7(Gn zg42FS> QzW@LL07*qoM6N<$f_3B8z5oCK literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json index d59eb51..d1ac24d 100644 --- a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -22,6 +22,7 @@ "nerospace:meteor_rock", "nerospace:item_store", "nerospace:battery", - "nerospace:fluid_tank" + "nerospace:fluid_tank", + "nerospace:combustion_generator" ] } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json index b9aa1ab..dddf9c3 100644 --- a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json @@ -14,6 +14,7 @@ "nerospace:station_wall", "nerospace:item_store", "nerospace:battery", - "nerospace:fluid_tank" + "nerospace:fluid_tank", + "nerospace:combustion_generator" ] } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/combustion_generator.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/combustion_generator.json new file mode 100644 index 0000000..3f07ad6 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/combustion_generator.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:combustion_generator" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/combustion_generator" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/combustion_generator.json b/multiloader/common/src/main/resources/data/nerospace/recipe/combustion_generator.json new file mode 100644 index 0000000..8ab1ce8 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/combustion_generator.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "F": "minecraft:furnace", + "N": "#c:ingots/nerosteel", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "NNN", + "NFN", + "NRN" + ], + "result": { + "id": "nerospace:combustion_generator" + } +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index fec31ee..48c97b6 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -64,6 +64,13 @@ public void onInitialize() { FLUID.registerForBlockEntity( (be, direction) -> be.getTank(), ModBlockEntities.FLUID_TANK.get()); + + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.COMBUSTION_GENERATOR.get()); + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.COMBUSTION_GENERATOR.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index e8375c5..41db178 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -61,5 +61,16 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { FLUID, ModBlockEntities.FLUID_TANK.get(), (be, side) -> be.getTank()); + + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.COMBUSTION_GENERATOR.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + event.registerBlockEntity( + ENERGY, + ModBlockEntities.COMBUSTION_GENERATOR.get(), + (be, side) -> be.getEnergy()); } } From 4ab80042d886cf00166c67e796b768cda3a4ca8b Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 02:07:48 +0400 Subject: [PATCH 23/82] Add combustion generator UI, menu & client wiring Introduce a minimal Combustion Generator UI and menu support: new CombustionGeneratorScreen (simple backdrop + energy bar), CombustionGeneratorMenu, and ModMenuTypes. Wire client-side screen registration for Fabric (MenuScreens) and NeoForge (RegisterMenuScreensEvent) and update ModRegistries to initialize menu types. Make CombustionGeneratorBlockEntity implement MenuProvider and expose a ContainerData array to sync energy/capacity/burnTime/maxBurnTime with the menu; open the menu server-side on block use. Add container translation entry for the generator. This enables server-side menu creation and client screen registration; visual polish for the GUI texture is left for follow-up. --- .../client/CombustionGeneratorScreen.java | 32 +++++++ .../machine/CombustionGeneratorBlock.java | 13 +++ .../CombustionGeneratorBlockEntity.java | 48 +++++++++- .../menu/CombustionGeneratorMenu.java | 88 +++++++++++++++++++ .../nerospace/registry/ModMenuTypes.java | 26 ++++++ .../nerospace/registry/ModRegistries.java | 1 + .../assets/nerospace/lang/en_us.json | 3 +- .../fabric/NerospaceFabricClient.java | 15 ++-- .../neoforge/NeoForgeClientSetup.java | 22 +++++ .../nerospace/neoforge/NerospaceNeoForge.java | 5 ++ 10 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/CombustionGeneratorScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/CombustionGeneratorMenu.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/CombustionGeneratorScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/CombustionGeneratorScreen.java new file mode 100644 index 0000000..669209c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/CombustionGeneratorScreen.java @@ -0,0 +1,32 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; + +/** + * Minimal functional screen for the combustion generator (bare backdrop + an energy bar). Uses the + * 26.x container-screen API ({@code extractContents(GuiGraphicsExtractor, ...)}). Slots are usable; + * visual polish (a proper GUI texture) is a follow-up — only compilation + registration on both + * loaders is verifiable headlessly. + */ +public class CombustionGeneratorScreen extends AbstractContainerScreen { + + public CombustionGeneratorScreen(CombustionGeneratorMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + public void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + int x = this.leftPos; + int y = this.topPos; + extractor.fill(x, y, x + this.imageWidth, y + this.imageHeight, 0xFFC6C6C6); + super.extractContents(extractor, mouseX, mouseY, partialTick); + int cap = this.menu.capacity(); + int filled = cap > 0 ? (int) (this.menu.energy() / (double) cap * 50.0D) : 0; + extractor.fill(x + 10, y + 16 + (50 - filled), x + 18, y + 66, 0xFFFF5A3C); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java index 4fb88a2..6fba8d1 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java @@ -4,6 +4,9 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; @@ -16,6 +19,7 @@ import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BlockStateProperties; import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; import org.jetbrains.annotations.Nullable; @@ -59,6 +63,15 @@ public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { return new CombustionGeneratorBlockEntity(pos, state); } + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof CombustionGeneratorBlockEntity gen) { + serverPlayer.openMenu(gen); + } + return InteractionResult.SUCCESS; + } + @Nullable @Override public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java index 1073e7e..8e880a3 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java @@ -5,8 +5,13 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.world.MenuProvider; import net.minecraft.world.ContainerHelper; import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; @@ -20,6 +25,7 @@ import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModItems; @@ -29,7 +35,7 @@ * capability). First ticking machine: proves the item + energy seams together with a * {@code BlockEntityTicker}. The menu/screen comes with the menu seam. */ -public class CombustionGeneratorBlockEntity extends BlockEntity implements WorldlyContainer { +public class CombustionGeneratorBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { public static final int FUEL_SLOT = 0; public static final int SIZE = 1; @@ -42,6 +48,35 @@ public class CombustionGeneratorBlockEntity extends BlockEntity implements World private int burnTime; private int maxBurnTime; + /** Synced to the menu: [0]=energy [1]=capacity [2]=burnTime [3]=maxBurnTime. */ + private final ContainerData data = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> energy.getRaw(); + case 1 -> CAPACITY; + case 2 -> burnTime; + case 3 -> maxBurnTime; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + switch (index) { + case 0 -> energy.setRaw(value); + case 2 -> burnTime = value; + case 3 -> maxBurnTime = value; + default -> { } + } + } + + @Override + public int getCount() { + return 4; + } + }; + public CombustionGeneratorBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.COMBUSTION_GENERATOR.get(), pos, state); } @@ -108,6 +143,17 @@ protected void loadAdditional(ValueInput input) { this.items.set(FUEL_SLOT, input.read("Fuel", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); } + // --- MenuProvider --------------------------------------------------------- + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.combustion_generator"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new CombustionGeneratorMenu(containerId, playerInventory, this, this.data); + } + // --- WorldlyContainer: fuel in only --------------------------------------- @Override public int[] getSlotsForFace(Direction side) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/CombustionGeneratorMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/CombustionGeneratorMenu.java new file mode 100644 index 0000000..f17bddb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/CombustionGeneratorMenu.java @@ -0,0 +1,88 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** Combustion Generator menu: 1 fuel slot + player inventory + synced energy/burn data. */ +public class CombustionGeneratorMenu extends AbstractContainerMenu { + + private static final int MACHINE_SLOTS = CombustionGeneratorBlockEntity.SIZE; + private final Container container; + private final ContainerData data; + + /** Client constructor (dummy container/data; slots + data sync from the server). */ + public CombustionGeneratorMenu(int id, Inventory playerInventory) { + this(id, playerInventory, new SimpleContainer(MACHINE_SLOTS), new SimpleContainerData(4)); + } + + public CombustionGeneratorMenu(int id, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.COMBUSTION_GENERATOR.get(), id); + checkContainerSize(container, MACHINE_SLOTS); + this.container = container; + this.data = data; + + this.addSlot(new Slot(container, CombustionGeneratorBlockEntity.FUEL_SLOT, 80, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return CombustionGeneratorBlockEntity.fuelValue(stack) > 0; + } + }); + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 84 + row * 18)); + } + } + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col, 8 + col * 18, 142)); + } + this.addDataSlots(data); + } + + public int energy() { + return this.data.get(0); + } + + public int capacity() { + return this.data.get(1); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack result = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack stack = slot.getItem(); + result = stack.copy(); + int invStart = MACHINE_SLOTS; + int invEnd = invStart + 36; + if (index < invStart) { + if (!this.moveItemStackTo(stack, invStart, invEnd, true)) { + return ItemStack.EMPTY; + } + } else if (!this.moveItemStackTo(stack, 0, invStart, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.set(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + } + return result; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java new file mode 100644 index 0000000..d6a4cf6 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -0,0 +1,26 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.flag.FeatureFlags; +import net.minecraft.world.inventory.MenuType; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** Menu types, shared via {@link RegistrationProvider} over the vanilla MENU registry. */ +public final class ModMenuTypes { + + public static final RegistrationProvider> MENUS = + RegistrationProvider.get(Registries.MENU, NerospaceCommon.MOD_ID); + + public static final RegistryEntry> COMBUSTION_GENERATOR = + MENUS.register("combustion_generator", + key -> new MenuType<>(CombustionGeneratorMenu::new, FeatureFlags.VANILLA_SET)); + + private ModMenuTypes() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index 26d6199..ba2cc30 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -14,5 +14,6 @@ public static void init() { ModBlocks.init(); ModItems.init(); ModBlockEntities.init(); + ModMenuTypes.init(); } } diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index d5bd2dd..c048df0 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -55,5 +55,6 @@ "container.nerospace.item_store": "Item Store", "block.nerospace.battery": "Battery", "block.nerospace.fluid_tank": "Fluid Tank", - "block.nerospace.combustion_generator": "Combustion Generator" + "block.nerospace.combustion_generator": "Combustion Generator", + "container.nerospace.combustion_generator": "Combustion Generator" } \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index f494c31..3296198 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -1,21 +1,18 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ClientModInitializer; +import net.minecraft.client.gui.screens.MenuScreens; + import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.client.CombustionGeneratorScreen; +import za.co.neroland.nerospace.registry.ModMenuTypes; -/** - * Fabric client entry point. The equivalent of the root project's - * {@code NerospaceClient} client-only registrations. - * - *

Migration target for: entity renderers ({@code EntityRendererRegistry}), - * screens ({@code HandledScreens}), HUD ({@code HudLayerRegistrationCallback}), - * and ModMenu config screen integration. - */ +/** Fabric client entry point — screen registration (vanilla {@code MenuScreens}). */ public final class NerospaceFabricClient implements ClientModInitializer { @Override public void onInitializeClient() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric client bootstrap"); - // TODO (migration): client-only registrations. + MenuScreens.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java new file mode 100644 index 0000000..f80ed59 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -0,0 +1,22 @@ +package za.co.neroland.nerospace.neoforge; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; + +import za.co.neroland.nerospace.client.CombustionGeneratorScreen; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** NeoForge client-only wiring (screen registration). Loaded only behind a Dist.CLIENT guard. */ +public final class NeoForgeClientSetup { + + private NeoForgeClientSetup() { + } + + public static void init(IEventBus modEventBus) { + modEventBus.addListener(NeoForgeClientSetup::onRegisterScreens); + } + + private static void onRegisterScreens(RegisterMenuScreensEvent event) { + event.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); + } +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 1988af5..72a692c 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -5,7 +5,9 @@ import net.minecraft.world.level.ItemLike; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; +import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.common.Mod; +import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; import za.co.neroland.nerospace.NerospaceCommon; @@ -25,6 +27,9 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NerospaceCommon.init(); NeoForgeRegistrationFactory.registerAll(modEventBus); NeoForgeCapabilities.register(modEventBus); + if (FMLEnvironment.getDist() == Dist.CLIENT) { + NeoForgeClientSetup.init(modEventBus); + } modEventBus.addListener(this::onBuildCreativeTabs); } From 99f1308414611ba7d0181c1fd330dfc5b9c2d595 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:11:20 +0200 Subject: [PATCH 24/82] Add Nerosium Grinder machine and UI Introduce the Nerosium Grinder: block, block entity, menu, and screen with full client/server wiring and assets. Adds machine logic (tick, energy consumption, crafting, sided item access) and in-code GrinderRecipes (ores/raw -> 2 dust, ingot -> 1 dust) intended for a future datapack swap. Includes resource files (models, textures, blockstate, item model), loot/recipe JSON, and registers the block/item/menu/block-entity across registries and Fabric/NeoForge integration. Also adds EnergyBuffer.consume for internal energy use and exposes progress/energy data via the menu for the UI. --- .../client/NerosiumGrinderScreen.java | 32 +++ .../nerospace/energy/EnergyBuffer.java | 9 + .../nerospace/machine/GrinderRecipes.java | 27 +++ .../machine/NerosiumGrinderBlock.java | 84 +++++++ .../machine/NerosiumGrinderBlockEntity.java | 219 ++++++++++++++++++ .../nerospace/menu/NerosiumGrinderMenu.java | 102 ++++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 7 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../blockstates/nerosium_grinder.json | 19 ++ .../nerospace/items/nerosium_grinder.json | 6 + .../assets/nerospace/lang/en_us.json | 4 +- .../models/block/nerosium_grinder.json | 170 ++++++++++++++ .../textures/block/nerosium_grinder.png | Bin 0 -> 428 bytes .../textures/block/nerosium_grinder_front.png | Bin 0 -> 423 bytes .../textures/block/nerosium_grinder_top.png | Bin 0 -> 328 bytes .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../loot_table/blocks/nerosium_grinder.json | 21 ++ .../nerospace/recipe/nerosium_grinder.json | 17 ++ .../nerospace/fabric/NerospaceFabric.java | 7 + .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeCapabilities.java | 11 + .../neoforge/NeoForgeClientSetup.java | 2 + 25 files changed, 754 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/NerosiumGrinderScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/GrinderRecipes.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/NerosiumGrinderMenu.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_grinder.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/nerosium_grinder.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_grinder.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_front.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_top.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_grinder.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_grinder.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/NerosiumGrinderScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/NerosiumGrinderScreen.java new file mode 100644 index 0000000..4ab411a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/NerosiumGrinderScreen.java @@ -0,0 +1,32 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; + +/** Minimal functional screen for the grinder (backdrop + progress arrow + energy bar). */ +public class NerosiumGrinderScreen extends AbstractContainerScreen { + + public NerosiumGrinderScreen(NerosiumGrinderMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + public void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + int x = this.leftPos; + int y = this.topPos; + extractor.fill(x, y, x + this.imageWidth, y + this.imageHeight, 0xFFC6C6C6); + super.extractContents(extractor, mouseX, mouseY, partialTick); + // progress arrow (between input @56 and output @116) + int max = this.menu.maxProgress(); + int prog = max > 0 ? (int) (this.menu.progress() / (double) max * 22.0D) : 0; + extractor.fill(x + 80, y + 38, x + 80 + prog, y + 42, 0xFF50C0FF); + // energy bar + int cap = this.menu.capacity(); + int filled = cap > 0 ? (int) (this.menu.energy() / (double) cap * 50.0D) : 0; + extractor.fill(x + 10, y + 16 + (50 - filled), x + 18, y + 66, 0xFFFF5A3C); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java index b45c1f6..5e82ec5 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java @@ -58,6 +58,15 @@ public void generate(int amount) { } } + /** Internal consumption (bypasses the extract limit) — for machines. */ + public void consume(int amount) { + int removed = (int) Math.max(0, Math.min(amount, this.amount)); + if (removed > 0) { + this.amount -= removed; + this.onChanged.run(); + } + } + /** Raw accessors for NBT save/load. */ public int getRaw() { return this.amount; diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/GrinderRecipes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/GrinderRecipes.java new file mode 100644 index 0000000..a6dfd87 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/GrinderRecipes.java @@ -0,0 +1,27 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.registry.ModItems; + +/** In-code grinding recipes (ores/raw -> 2 dust; ingot -> 1 dust). Isolated for a later datapack swap. */ +public final class GrinderRecipes { + + private GrinderRecipes() { + } + + public static ItemStack getResult(ItemStack input) { + if (input.isEmpty()) { + return ItemStack.EMPTY; + } + if (input.is(ModItems.NEROSIUM_ORE_ITEM.get()) + || input.is(ModItems.DEEPSLATE_NEROSIUM_ORE_ITEM.get()) + || input.is(ModItems.RAW_NEROSIUM.get())) { + return new ItemStack(ModItems.NEROSIUM_DUST.get(), 2); + } + if (input.is(ModItems.NEROSIUM_INGOT.get())) { + return new ItemStack(ModItems.NEROSIUM_DUST.get(), 1); + } + return ItemStack.EMPTY; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlock.java new file mode 100644 index 0000000..10f9c70 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlock.java @@ -0,0 +1,84 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Nerosium Grinder block — directional, ticks + opens its {@link NerosiumGrinderBlockEntity}. */ +public class NerosiumGrinderBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(NerosiumGrinderBlock::new); + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + @SuppressWarnings("this-escape") + public NerosiumGrinderBlock(Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new NerosiumGrinderBlockEntity(pos, state); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof NerosiumGrinderBlockEntity grinder) { + serverPlayer.openMenu(grinder); + } + return InteractionResult.SUCCESS; + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.NEROSIUM_GRINDER.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java new file mode 100644 index 0000000..2b2388d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java @@ -0,0 +1,219 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Nerosium Grinder — grid-powered processing machine. Input slot + output slot + an energy buffer + * fed by pipes (insert-only); grinds inputs into dust over time. Exercises the item (in/out) and + * energy seams, a ticker, and the menu/screen seam together. + */ +public class NerosiumGrinderBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int INPUT_SLOT = 0; + public static final int OUTPUT_SLOT = 1; + public static final int SIZE = 2; + public static final int CAPACITY = 20_000; + public static final int MAX_INSERT = 500; + public static final int ENERGY_PER_TICK = 20; + public static final int MAX_PROGRESS = 200; + private static final int[] SLOTS = {INPUT_SLOT, OUTPUT_SLOT}; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_INSERT, 0, this::setChanged); + private int progress; + + private final ContainerData data = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> progress; + case 1 -> MAX_PROGRESS; + case 2 -> energy.getRaw(); + case 3 -> CAPACITY; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + if (index == 0) { + progress = value; + } + } + + @Override + public int getCount() { + return 4; + } + }; + + public NerosiumGrinderBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.NEROSIUM_GRINDER.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + boolean changed = false; + ItemStack input = this.items.get(INPUT_SLOT); + ItemStack result = GrinderRecipes.getResult(input); + boolean canWork = !result.isEmpty() && canInsertOutput(result) && this.energy.getAmount() >= ENERGY_PER_TICK; + if (canWork) { + this.progress++; + this.energy.consume(ENERGY_PER_TICK); + if (this.progress >= MAX_PROGRESS) { + craft(result); + this.progress = 0; + } + changed = true; + } else if (this.progress != 0) { + this.progress = 0; + changed = true; + } + if (changed) { + this.setChanged(); + } + } + + private void craft(ItemStack result) { + this.items.get(INPUT_SLOT).shrink(1); + ItemStack output = this.items.get(OUTPUT_SLOT); + if (output.isEmpty()) { + this.items.set(OUTPUT_SLOT, result.copy()); + } else { + output.grow(result.getCount()); + } + } + + private boolean canInsertOutput(ItemStack result) { + ItemStack output = this.items.get(OUTPUT_SLOT); + if (output.isEmpty()) { + return true; + } + return ItemStack.isSameItemSameComponents(output, result) + && output.getCount() + result.getCount() <= output.getMaxStackSize(); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.store("Input", ItemStack.OPTIONAL_CODEC, this.items.get(INPUT_SLOT)); + output.store("Output", ItemStack.OPTIONAL_CODEC, this.items.get(OUTPUT_SLOT)); + output.putInt("Progress", this.progress); + output.putInt("Energy", this.energy.getRaw()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.items.set(INPUT_SLOT, input.read("Input", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + this.items.set(OUTPUT_SLOT, input.read("Output", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + this.progress = input.getIntOr("Progress", 0); + this.energy.setRaw(input.getIntOr("Energy", 0)); + } + + // --- MenuProvider --------------------------------------------------------- + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.nerosium_grinder"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new NerosiumGrinderMenu(containerId, playerInventory, this, this.data); + } + + // --- WorldlyContainer: input in (grindable), output out ------------------- + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return slot == INPUT_SLOT && !GrinderRecipes.getResult(stack).isEmpty(); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return slot == OUTPUT_SLOT; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return slot == INPUT_SLOT && !GrinderRecipes.getResult(stack).isEmpty(); + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.stream().allMatch(ItemStack::isEmpty); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/NerosiumGrinderMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/NerosiumGrinderMenu.java new file mode 100644 index 0000000..8764711 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/NerosiumGrinderMenu.java @@ -0,0 +1,102 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.machine.GrinderRecipes; +import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** Nerosium Grinder menu: input + output slots + player inventory + progress/energy data. */ +public class NerosiumGrinderMenu extends AbstractContainerMenu { + + private static final int MACHINE_SLOTS = NerosiumGrinderBlockEntity.SIZE; + private final Container container; + private final ContainerData data; + + public NerosiumGrinderMenu(int id, Inventory playerInventory) { + this(id, playerInventory, new SimpleContainer(MACHINE_SLOTS), new SimpleContainerData(4)); + } + + public NerosiumGrinderMenu(int id, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.NEROSIUM_GRINDER.get(), id); + checkContainerSize(container, MACHINE_SLOTS); + this.container = container; + this.data = data; + + this.addSlot(new Slot(container, NerosiumGrinderBlockEntity.INPUT_SLOT, 56, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return !GrinderRecipes.getResult(stack).isEmpty(); + } + }); + this.addSlot(new Slot(container, NerosiumGrinderBlockEntity.OUTPUT_SLOT, 116, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + }); + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 84 + row * 18)); + } + } + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col, 8 + col * 18, 142)); + } + this.addDataSlots(data); + } + + public int progress() { + return this.data.get(0); + } + + public int maxProgress() { + return this.data.get(1); + } + + public int energy() { + return this.data.get(2); + } + + public int capacity() { + return this.data.get(3); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack result = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack stack = slot.getItem(); + result = stack.copy(); + int invStart = MACHINE_SLOTS; + int invEnd = invStart + 36; + if (index < invStart) { + if (!this.moveItemStackTo(stack, invStart, invEnd, true)) { + return ItemStack.EMPTY; + } + } else if (!this.moveItemStackTo(stack, 0, 1, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.set(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + } + return result; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 1f9f510..e681493 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -6,6 +6,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; +import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; @@ -36,6 +37,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("combustion_generator", key -> new BlockEntityType<>(CombustionGeneratorBlockEntity::new, java.util.Set.of(ModBlocks.COMBUSTION_GENERATOR.get()))); + public static final RegistryEntry> NEROSIUM_GRINDER = + BLOCK_ENTITIES.register("nerosium_grinder", + key -> new BlockEntityType<>(NerosiumGrinderBlockEntity::new, java.util.Set.of(ModBlocks.NEROSIUM_GRINDER.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index df8d235..bae9b36 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -10,6 +10,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.machine.CombustionGeneratorBlock; +import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; @@ -109,6 +110,12 @@ public final class ModBlocks { .requiresCorrectToolForDrops() .sound(SoundType.METAL))); + + public static final RegistryEntry NEROSIUM_GRINDER = BLOCKS.register("nerosium_grinder", + key -> new NerosiumGrinderBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL))); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index f95e51d..01cd422 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -61,6 +61,7 @@ public final class ModItems { public static final RegistryEntry BATTERY_ITEM = blockItem("battery", ModBlocks.BATTERY); public static final RegistryEntry FLUID_TANK_ITEM = blockItem("fluid_tank", ModBlocks.FLUID_TANK); public static final RegistryEntry COMBUSTION_GENERATOR_ITEM = blockItem("combustion_generator", ModBlocks.COMBUSTION_GENERATOR); + public static final RegistryEntry NEROSIUM_GRINDER_ITEM = blockItem("nerosium_grinder", ModBlocks.NEROSIUM_GRINDER); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -176,7 +177,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index d6a4cf6..3029328 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -6,6 +6,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; +import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** Menu types, shared via {@link RegistrationProvider} over the vanilla MENU registry. */ @@ -18,6 +19,10 @@ public final class ModMenuTypes { MENUS.register("combustion_generator", key -> new MenuType<>(CombustionGeneratorMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> NEROSIUM_GRINDER = + MENUS.register("nerosium_grinder", + key -> new MenuType<>(NerosiumGrinderMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_grinder.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_grinder.json new file mode 100644 index 0000000..67b9a92 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/nerosium_grinder.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "nerospace:block/nerosium_grinder", + "y": 90 + }, + "facing=north": { + "model": "nerospace:block/nerosium_grinder" + }, + "facing=south": { + "model": "nerospace:block/nerosium_grinder", + "y": 180 + }, + "facing=west": { + "model": "nerospace:block/nerosium_grinder", + "y": 270 + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_grinder.json b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_grinder.json new file mode 100644 index 0000000..ebc4440 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/nerosium_grinder.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/nerosium_grinder" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index c048df0..86bb487 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -56,5 +56,7 @@ "block.nerospace.battery": "Battery", "block.nerospace.fluid_tank": "Fluid Tank", "block.nerospace.combustion_generator": "Combustion Generator", - "container.nerospace.combustion_generator": "Combustion Generator" + "container.nerospace.combustion_generator": "Combustion Generator", + "block.nerospace.nerosium_grinder": "Nerosium Grinder", + "container.nerospace.nerosium_grinder": "Nerosium Grinder" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_grinder.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_grinder.json new file mode 100644 index 0000000..6f4dc89 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/nerosium_grinder.json @@ -0,0 +1,170 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#front" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 14, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "front": "nerospace:block/nerosium_grinder_front", + "particle": "nerospace:block/nerosium_grinder", + "side": "nerospace:block/nerosium_grinder", + "top": "nerospace:block/nerosium_grinder_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder.png new file mode 100644 index 0000000000000000000000000000000000000000..6afe2f15f20c9bb800eba82af70788d9bfd18373 GIT binary patch literal 428 zcmV;d0aN~oP)+#U^4S;5nuP?~g7i;)E zSLPjr5svFo7CF$^K@diUA@x0apQo)1px#o-BFAw(8=xuqOdfLHowPDQRh86LL7K!i zKu5R+Ko4CGwprVsn!358AdGB)W;7WWYfaQrP*(*)IF^Esn*&ZpkCuW=Nzx={-S4(7 zX!PH9u()vR8?~>y)eG(vu$X^_n_&Qia7dGwaC6ICUDFJ$ zeX@@0kts=4l@{Rf;Hs_QYIIBxMx;rM5RUN}gpoO%x+?HD0(15NOVj)3*`VL!uki(2 W=e;W_eSS*-0000IKmc35LqO33Ln5a4`XFz>4T&QRw+b+PhfRL3Q2KN!`O9;wz%~Zw>`BFkEyHdO{M314{Paa42~z#6)Vd-P32~BAy)9b;H5dlw zvX}=&UR6)_sda_S6mgP<--80%7-nX*KQXJ_%?>_4Sh(v$f?|=Q_$%6_`vSpD$2c1+ R^oalf002ovPDHLkV1n(Mwsimi literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_top.png new file mode 100644 index 0000000000000000000000000000000000000000..7ff06dc16a2e21d3a0ae811a8c1a4c663cc73fb9 GIT binary patch literal 328 zcmV-O0k{5%P)5P-jwsq!dglo3%vMJN$f75y>&6=P~*Vq$t!kBNxNiBOSW5EW4oCPIlQq2f_O z3E@#OlZlXIVPeVJ&)k{!?!Mzrvz;L40&W1-?<Qiw^)!iL7G0-~Gyc&#kKZo{KeG0MG-f>dUW3B7O8{{E22eLB9D|H!8Hjnk@)F z{i1{aa8_xpP|`)AFNU?k>&rtm#J1dybWVvZI=It~&I&>dE>ln{yP`t5SzRdK^Z>Fn z>8wzq*-ntZt)8ouZHqCP67P4#Ehv>?Os0*)k%x%B7;;uA`q)GsqDG9uZQa4s{j&26 aO1=R^Td|Fr2r;Vw0000 be.getEnergy(), ModBlockEntities.COMBUSTION_GENERATOR.get()); + + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.NEROSIUM_GRINDER.get()); + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.NEROSIUM_GRINDER.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 3296198..29b3f83 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -5,6 +5,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; +import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; /** Fabric client entry point — screen registration (vanilla {@code MenuScreens}). */ @@ -14,5 +15,6 @@ public final class NerospaceFabricClient implements ClientModInitializer { public void onInitializeClient() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric client bootstrap"); MenuScreens.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); + MenuScreens.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 41db178..4c2c4d7 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -72,5 +72,16 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.COMBUSTION_GENERATOR.get(), (be, side) -> be.getEnergy()); + + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.NEROSIUM_GRINDER.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + event.registerBlockEntity( + ENERGY, + ModBlockEntities.NEROSIUM_GRINDER.get(), + (be, side) -> be.getEnergy()); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index f80ed59..6af5aed 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -4,6 +4,7 @@ import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; +import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; /** NeoForge client-only wiring (screen registration). Loaded only behind a Dist.CLIENT guard. */ @@ -18,5 +19,6 @@ public static void init(IEventBus modEventBus) { private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); + event.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); } } From 87bb6c7ea5da7411ddec08c577419120fa248e80 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:16:10 +0200 Subject: [PATCH 25/82] Add passive generator block, BE, UI and assets Introduce a new Passive Generator: adds PassiveGeneratorBlock, PassiveGeneratorBlockEntity (energy buffer, core item handling, ticking logic, save/load), PassiveGeneratorMenu and PassiveGeneratorScreen. Registers block, block entity, item and menu type, and wires item/energy capabilities for Fabric and NeoForge. Includes blockstate, block/item models, textures, loot table, crafting recipe, data tags, and language entries; updates creative tab to include the new block. --- .../client/PassiveGeneratorScreen.java | 27 +++ .../machine/PassiveGeneratorBlock.java | 65 ++++++ .../machine/PassiveGeneratorBlockEntity.java | 192 ++++++++++++++++++ .../nerospace/menu/PassiveGeneratorMenu.java | 87 ++++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 7 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../blockstates/passive_generator.json | 7 + .../nerospace/items/passive_generator.json | 6 + .../assets/nerospace/lang/en_us.json | 4 +- .../models/block/passive_generator.json | 73 +++++++ .../textures/block/passive_generator.png | Bin 0 -> 391 bytes .../textures/block/passive_generator_top.png | Bin 0 -> 431 bytes .../c/tags/item/storage_blocks/nerosium.json | 5 + .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../loot_table/blocks/passive_generator.json | 21 ++ .../nerospace/recipe/passive_generator.json | 17 ++ .../nerospace/fabric/NerospaceFabric.java | 7 + .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeCapabilities.java | 11 + .../neoforge/NeoForgeClientSetup.java | 2 + 23 files changed, 548 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/PassiveGeneratorScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PassiveGeneratorMenu.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/passive_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/passive_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/passive_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator_top.png create mode 100644 multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosium.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/passive_generator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/passive_generator.json diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/PassiveGeneratorScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/PassiveGeneratorScreen.java new file mode 100644 index 0000000..bb71b93 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/PassiveGeneratorScreen.java @@ -0,0 +1,27 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; + +/** Minimal functional screen for the passive generator (backdrop + energy bar). */ +public class PassiveGeneratorScreen extends AbstractContainerScreen { + + public PassiveGeneratorScreen(PassiveGeneratorMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title); + } + + @Override + public void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + int x = this.leftPos; + int y = this.topPos; + extractor.fill(x, y, x + this.imageWidth, y + this.imageHeight, 0xFFC6C6C6); + super.extractContents(extractor, mouseX, mouseY, partialTick); + int cap = this.menu.capacity(); + int filled = cap > 0 ? (int) (this.menu.energy() / (double) cap * 50.0D) : 0; + extractor.fill(x + 10, y + 16 + (50 - filled), x + 18, y + 66, 0xFFFF5A3C); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlock.java new file mode 100644 index 0000000..5071b60 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlock.java @@ -0,0 +1,65 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Passive Generator block — ticks + opens its {@link PassiveGeneratorBlockEntity}. */ +public class PassiveGeneratorBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(PassiveGeneratorBlock::new); + + public PassiveGeneratorBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new PassiveGeneratorBlockEntity(pos, state); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof PassiveGeneratorBlockEntity gen) { + serverPlayer.openMenu(gen); + } + return InteractionResult.SUCCESS; + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.PASSIVE_GENERATOR.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java new file mode 100644 index 0000000..aa25db5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java @@ -0,0 +1,192 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Passive Generator — consumes a nerosium "core" (raw/ingot/dust) which grants a long run-time of + * a small steady energy trickle. Item + energy seams + ticker + GUI; hands-off but weak. + */ +public class PassiveGeneratorBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int CORE_SLOT = 0; + public static final int SIZE = 1; + public static final int CAPACITY = 100_000; + public static final int FE_PER_TICK = 8; + public static final int CORE_TICKS = 24_000; + private static final int[] SLOTS = {CORE_SLOT}; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, 0, FE_PER_TICK * 64, this::setChanged); + private int coreTicks; + + private final ContainerData data = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> energy.getRaw(); + case 1 -> CAPACITY; + case 2 -> coreTicks; + case 3 -> CORE_TICKS; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + if (index == 2) { + coreTicks = value; + } + } + + @Override + public int getCount() { + return 4; + } + }; + + public PassiveGeneratorBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.PASSIVE_GENERATOR.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + public static boolean isCore(ItemStack stack) { + return stack.is(ModItems.RAW_NEROSIUM.get()) || stack.is(ModItems.NEROSIUM_INGOT.get()) + || stack.is(ModItems.NEROSIUM_DUST.get()); + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + if (this.coreTicks <= 0) { + ItemStack core = this.items.get(CORE_SLOT); + if (isCore(core)) { + core.shrink(1); + this.coreTicks = CORE_TICKS; + this.setChanged(); + } + } + if (this.coreTicks > 0 && this.energy.getAmount() < this.energy.getCapacity()) { + this.coreTicks--; + this.energy.generate(FE_PER_TICK); + } + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + output.putInt("CoreTicks", this.coreTicks); + output.store("Core", ItemStack.OPTIONAL_CODEC, this.items.get(CORE_SLOT)); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + this.coreTicks = input.getIntOr("CoreTicks", 0); + this.items.set(CORE_SLOT, input.read("Core", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.passive_generator"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new PassiveGeneratorMenu(containerId, playerInventory, this, this.data); + } + + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return isCore(stack); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return !isCore(stack); + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return isCore(stack); + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.get(CORE_SLOT).isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PassiveGeneratorMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PassiveGeneratorMenu.java new file mode 100644 index 0000000..a4256a7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PassiveGeneratorMenu.java @@ -0,0 +1,87 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** Passive Generator menu: 1 core slot + player inventory + synced energy/core data. */ +public class PassiveGeneratorMenu extends AbstractContainerMenu { + + private static final int MACHINE_SLOTS = PassiveGeneratorBlockEntity.SIZE; + private final Container container; + private final ContainerData data; + + public PassiveGeneratorMenu(int id, Inventory playerInventory) { + this(id, playerInventory, new SimpleContainer(MACHINE_SLOTS), new SimpleContainerData(4)); + } + + public PassiveGeneratorMenu(int id, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.PASSIVE_GENERATOR.get(), id); + checkContainerSize(container, MACHINE_SLOTS); + this.container = container; + this.data = data; + + this.addSlot(new Slot(container, PassiveGeneratorBlockEntity.CORE_SLOT, 80, 35) { + @Override + public boolean mayPlace(ItemStack stack) { + return PassiveGeneratorBlockEntity.isCore(stack); + } + }); + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col + row * 9 + 9, 8 + col * 18, 84 + row * 18)); + } + } + for (int col = 0; col < 9; col++) { + this.addSlot(new Slot(playerInventory, col, 8 + col * 18, 142)); + } + this.addDataSlots(data); + } + + public int energy() { + return this.data.get(0); + } + + public int capacity() { + return this.data.get(1); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack result = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack stack = slot.getItem(); + result = stack.copy(); + int invStart = MACHINE_SLOTS; + int invEnd = invStart + 36; + if (index < invStart) { + if (!this.moveItemStackTo(stack, invStart, invEnd, true)) { + return ItemStack.EMPTY; + } + } else if (!this.moveItemStackTo(stack, 0, invStart, false)) { + return ItemStack.EMPTY; + } + if (stack.isEmpty()) { + slot.set(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + } + return result; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index e681493..a6b4c0e 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -7,6 +7,7 @@ import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; +import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; @@ -41,6 +42,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("nerosium_grinder", key -> new BlockEntityType<>(NerosiumGrinderBlockEntity::new, java.util.Set.of(ModBlocks.NEROSIUM_GRINDER.get()))); + public static final RegistryEntry> PASSIVE_GENERATOR = + BLOCK_ENTITIES.register("passive_generator", + key -> new BlockEntityType<>(PassiveGeneratorBlockEntity::new, java.util.Set.of(ModBlocks.PASSIVE_GENERATOR.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index bae9b36..cc85892 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -11,6 +11,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.machine.CombustionGeneratorBlock; import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; +import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; @@ -116,6 +117,12 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL))); + + public static final RegistryEntry PASSIVE_GENERATOR = BLOCKS.register("passive_generator", + key -> new PassiveGeneratorBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL))); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 01cd422..b046e70 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -62,6 +62,7 @@ public final class ModItems { public static final RegistryEntry FLUID_TANK_ITEM = blockItem("fluid_tank", ModBlocks.FLUID_TANK); public static final RegistryEntry COMBUSTION_GENERATOR_ITEM = blockItem("combustion_generator", ModBlocks.COMBUSTION_GENERATOR); public static final RegistryEntry NEROSIUM_GRINDER_ITEM = blockItem("nerosium_grinder", ModBlocks.NEROSIUM_GRINDER); + public static final RegistryEntry PASSIVE_GENERATOR_ITEM = blockItem("passive_generator", ModBlocks.PASSIVE_GENERATOR); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -177,7 +178,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 3029328..412d4eb 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -7,6 +7,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; +import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** Menu types, shared via {@link RegistrationProvider} over the vanilla MENU registry. */ @@ -23,6 +24,10 @@ public final class ModMenuTypes { MENUS.register("nerosium_grinder", key -> new MenuType<>(NerosiumGrinderMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> PASSIVE_GENERATOR = + MENUS.register("passive_generator", + key -> new MenuType<>(PassiveGeneratorMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/passive_generator.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/passive_generator.json new file mode 100644 index 0000000..6ed636e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/passive_generator.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/passive_generator" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/passive_generator.json b/multiloader/common/src/main/resources/assets/nerospace/items/passive_generator.json new file mode 100644 index 0000000..ad80dfc --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/passive_generator.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/passive_generator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 86bb487..2a6a121 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -58,5 +58,7 @@ "block.nerospace.combustion_generator": "Combustion Generator", "container.nerospace.combustion_generator": "Combustion Generator", "block.nerospace.nerosium_grinder": "Nerosium Grinder", - "container.nerospace.nerosium_grinder": "Nerosium Grinder" + "container.nerospace.nerosium_grinder": "Nerosium Grinder", + "block.nerospace.passive_generator": "Passive Generator", + "container.nerospace.passive_generator": "Passive Generator" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/passive_generator.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/passive_generator.json new file mode 100644 index 0000000..8fc8a00 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/passive_generator.json @@ -0,0 +1,73 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 3, + 0, + 3 + ], + "to": [ + 13, + 7, + 13 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 7, + 0 + ], + "to": [ + 16, + 11, + 16 + ] + } + ], + "textures": { + "particle": "nerospace:block/passive_generator", + "side": "nerospace:block/passive_generator", + "top": "nerospace:block/passive_generator_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator.png new file mode 100644 index 0000000000000000000000000000000000000000..e406e339145df37c682fcb27dca22bb91d0b82c6 GIT binary patch literal 391 zcmV;20eJq2P)YU6vlss4jt|wnIwTsl0w`{X`w^+LcyhP(3f!X348_Lz)|S#AXrMbUQBQbBsfWj zJGkkf=j5ga9Xt@YoP6i|zVqkg>iXt`e?$*px7}#M7(cCfY&XEo>cH0egRW<~o1dt9g0M7S~3c%~c zw#X!c45Ix{zDx9kUj> zg$r1BtT83iI}yA(ke-CuOq!Gal5JXu;zVbmGgw?M_4=ep$-Kjf;)LO7+`h!RD%FIye_N~n?-TfjyWa17%SQkJ002ovPDHLkV1jTEyQTmD literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator_top.png new file mode 100644 index 0000000000000000000000000000000000000000..524d5f30d80fa067c506c38f4d2fc62fa0a2b246 GIT binary patch literal 431 zcmV;g0Z{&lP)At56oo%aCwYjJB5x4sAVtI>l;Ysv)v3;;I1$2RZ@f(->(VjK8m zMV^&%DOYY#3kLvMETa|fMbtwyG#7uj^?Pr+Ed)VR%wnJ_`Mx#PvLf#>>jf; zM-9xc4`i_n5DpL`ms+}Gj}r@7k*8U&0`O^0j%ur}VKAXp*N)yJWc1;5|J;AVozGG! zsMPA%mKiDlXj`3lgX&I@11(Wd(|_ be.getEnergy(), ModBlockEntities.NEROSIUM_GRINDER.get()); + + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.PASSIVE_GENERATOR.get()); + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.PASSIVE_GENERATOR.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 29b3f83..b4e32e9 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -6,6 +6,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; +import za.co.neroland.nerospace.client.PassiveGeneratorScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; /** Fabric client entry point — screen registration (vanilla {@code MenuScreens}). */ @@ -16,5 +17,6 @@ public void onInitializeClient() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric client bootstrap"); MenuScreens.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); MenuScreens.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); + MenuScreens.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 4c2c4d7..10a2f53 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -83,5 +83,16 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.NEROSIUM_GRINDER.get(), (be, side) -> be.getEnergy()); + + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.PASSIVE_GENERATOR.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + event.registerBlockEntity( + ENERGY, + ModBlockEntities.PASSIVE_GENERATOR.get(), + (be, side) -> be.getEnergy()); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 6af5aed..91f9410 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -5,6 +5,7 @@ import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; +import za.co.neroland.nerospace.client.PassiveGeneratorScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; /** NeoForge client-only wiring (screen registration). Loaded only behind a Dist.CLIENT guard. */ @@ -20,5 +21,6 @@ public static void init(IEventBus modEventBus) { private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); event.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); + event.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); } } From 0ef17fa053f97919dc507bff94a6f7f3544d4639 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:22:28 +0200 Subject: [PATCH 26/82] Add Universal Pipe and platform energy lookup Introduce a new Universal Pipe block and BlockEntity that relays energy between adjacent storages. The pipe provides a small internal buffer (CAPACITY=8000, MAX_IO=1000) and tick logic to pull from extractable neighbours and push into insertable neighbours via a cross-loader EnergyLookup seam. Add EnergyLookup interface and platform adapters for Fabric and NeoForge (with service loader entries), register the block, item and block entity, wire the pipe into Fabric BlockApi and NeoForge capability registrations, and include assets/data (model, texture, blockstate, loot, recipe, language) and tag updates. --- .../nerospace/pipe/UniversalPipeBlock.java | 52 +++++++++++ .../pipe/UniversalPipeBlockEntity.java | 83 ++++++++++++++++++ .../nerospace/platform/EnergyLookup.java | 22 +++++ .../nerospace/registry/ModBlockEntities.java | 5 ++ .../nerospace/registry/ModBlocks.java | 7 ++ .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/blockstates/universal_pipe.json | 7 ++ .../nerospace/items/universal_pipe.json | 6 ++ .../assets/nerospace/lang/en_us.json | 3 +- .../models/block/universal_pipe.json | 6 ++ .../textures/block/universal_pipe.png | Bin 0 -> 379 bytes .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../loot_table/blocks/universal_pipe.json | 21 +++++ .../data/nerospace/recipe/universal_pipe.json | 17 ++++ .../nerospace/fabric/NerospaceFabric.java | 4 + .../platform/FabricEnergyLookup.java | 20 +++++ ...o.neroland.nerospace.platform.EnergyLookup | 1 + .../neoforge/NeoForgeCapabilities.java | 5 ++ .../platform/NeoForgeEnergyLookup.java | 20 +++++ ...o.neroland.nerospace.platform.EnergyLookup | 1 + 21 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/EnergyLookup.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/universal_pipe.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/universal_pipe.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/universal_pipe.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/universal_pipe.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/universal_pipe.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/universal_pipe.json create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricEnergyLookup.java create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeEnergyLookup.java create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java new file mode 100644 index 0000000..bf22740 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java @@ -0,0 +1,52 @@ +package za.co.neroland.nerospace.pipe; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Universal Pipe block — ticks its {@link UniversalPipeBlockEntity} energy relay. */ +public class UniversalPipeBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(UniversalPipeBlock::new); + + public UniversalPipeBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new UniversalPipeBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.UNIVERSAL_PIPE.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java new file mode 100644 index 0000000..8ba1335 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -0,0 +1,83 @@ +package za.co.neroland.nerospace.pipe; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.platform.EnergyLookup; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Universal Pipe — relays energy between adjacent storages. Each tick it pulls from neighbours that + * allow extraction (generators, other pipes) into its small buffer, then pushes into neighbours that + * allow insertion (machines, batteries). Direction is enforced naturally by each storage's own + * insert/extract limits. Uses {@link EnergyLookup} — the query side of the cross-loader energy seam. + */ +public class UniversalPipeBlockEntity extends BlockEntity { + + public static final int CAPACITY = 8_000; + public static final int MAX_IO = 1_000; + + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_IO, MAX_IO, this::setChanged); + + public UniversalPipeBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.UNIVERSAL_PIPE.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + // Pull from extractable neighbours into the buffer. + for (Direction dir : Direction.values()) { + NerospaceEnergyStorage neighbour = EnergyLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); + if (neighbour == null) { + continue; + } + long room = this.energy.getCapacity() - this.energy.getAmount(); + if (room > 0) { + long moved = neighbour.extract(Math.min(room, MAX_IO), false); + if (moved > 0) { + this.energy.insert(moved, false); + } + } + } + // Push from the buffer into insertable neighbours. + for (Direction dir : Direction.values()) { + if (this.energy.getAmount() <= 0) { + break; + } + NerospaceEnergyStorage neighbour = EnergyLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); + if (neighbour == null) { + continue; + } + long offered = this.energy.extract(Math.min(this.energy.getAmount(), MAX_IO), true); + long accepted = neighbour.insert(offered, false); + if (accepted > 0) { + this.energy.extract(accepted, false); + } + } + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/EnergyLookup.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/EnergyLookup.java new file mode 100644 index 0000000..cbff87a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/EnergyLookup.java @@ -0,0 +1,22 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; + +/** + * Query side of the energy seam: find the energy storage exposed by the block at {@code pos} on + * {@code side}. Each loader implements it over its own lookup mechanism (NeoForge + * {@code Level.getCapability}, Fabric {@code BlockApiLookup.find}). Resolved via {@link Services}. + */ +public interface EnergyLookup { + + EnergyLookup INSTANCE = Services.load(EnergyLookup.class); + + @Nullable + NerospaceEnergyStorage find(Level level, BlockPos pos, @Nullable Direction side); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index a6b4c0e..83aa277 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -8,6 +8,7 @@ import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; +import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; @@ -46,6 +47,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("passive_generator", key -> new BlockEntityType<>(PassiveGeneratorBlockEntity::new, java.util.Set.of(ModBlocks.PASSIVE_GENERATOR.get()))); + public static final RegistryEntry> UNIVERSAL_PIPE = + BLOCK_ENTITIES.register("universal_pipe", + key -> new BlockEntityType<>(UniversalPipeBlockEntity::new, java.util.Set.of(ModBlocks.UNIVERSAL_PIPE.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index cc85892..7906353 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -12,6 +12,7 @@ import za.co.neroland.nerospace.machine.CombustionGeneratorBlock; import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; +import za.co.neroland.nerospace.pipe.UniversalPipeBlock; import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; @@ -123,6 +124,12 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL))); + + public static final RegistryEntry UNIVERSAL_PIPE = BLOCKS.register("universal_pipe", + key -> new UniversalPipeBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(1.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index b046e70..8decb3c 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -63,6 +63,7 @@ public final class ModItems { public static final RegistryEntry COMBUSTION_GENERATOR_ITEM = blockItem("combustion_generator", ModBlocks.COMBUSTION_GENERATOR); public static final RegistryEntry NEROSIUM_GRINDER_ITEM = blockItem("nerosium_grinder", ModBlocks.NEROSIUM_GRINDER); public static final RegistryEntry PASSIVE_GENERATOR_ITEM = blockItem("passive_generator", ModBlocks.PASSIVE_GENERATOR); + public static final RegistryEntry UNIVERSAL_PIPE_ITEM = blockItem("universal_pipe", ModBlocks.UNIVERSAL_PIPE); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -178,7 +179,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/universal_pipe.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/universal_pipe.json new file mode 100644 index 0000000..ca7ebd9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/universal_pipe.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/universal_pipe" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/universal_pipe.json b/multiloader/common/src/main/resources/assets/nerospace/items/universal_pipe.json new file mode 100644 index 0000000..fb2f04b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/universal_pipe.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/universal_pipe" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 2a6a121..e0e5cda 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -60,5 +60,6 @@ "block.nerospace.nerosium_grinder": "Nerosium Grinder", "container.nerospace.nerosium_grinder": "Nerosium Grinder", "block.nerospace.passive_generator": "Passive Generator", - "container.nerospace.passive_generator": "Passive Generator" + "container.nerospace.passive_generator": "Passive Generator", + "block.nerospace.universal_pipe": "Universal Pipe" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/universal_pipe.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/universal_pipe.json new file mode 100644 index 0000000..1cfe7a9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/universal_pipe.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/universal_pipe" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/universal_pipe.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/universal_pipe.png new file mode 100644 index 0000000000000000000000000000000000000000..d2d228308bdb113d8a614771b89700502bc7e90d GIT binary patch literal 379 zcmV->0fhdEP)>N^d!Phb%2E`~ zp~dBVvii!ZwmoR~9{_;+^OK#YnyiV=2RI*WR4IXuPD4bj4_oarNszRTlg!Xb+H_Q( z5$GPMASDnHOI@^D? be.getEnergy(), ModBlockEntities.PASSIVE_GENERATOR.get()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.UNIVERSAL_PIPE.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricEnergyLookup.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricEnergyLookup.java new file mode 100644 index 0000000..eaa6cba --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricEnergyLookup.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.fabric.NerospaceFabric; + +/** Fabric query of the mod's energy block-api lookup. */ +public final class FabricEnergyLookup implements EnergyLookup { + + @Nullable + @Override + public NerospaceEnergyStorage find(Level level, BlockPos pos, @Nullable Direction side) { + return NerospaceFabric.ENERGY.find(level, pos, side); + } +} diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup new file mode 100644 index 0000000..43812ea --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.FabricEnergyLookup diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 10a2f53..4384c19 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -94,5 +94,10 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.PASSIVE_GENERATOR.get(), (be, side) -> be.getEnergy()); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.UNIVERSAL_PIPE.get(), + (be, side) -> be.getEnergy()); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeEnergyLookup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeEnergyLookup.java new file mode 100644 index 0000000..8d0ab2f --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeEnergyLookup.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.neoforge.NeoForgeCapabilities; + +/** NeoForge query of the mod's energy capability. */ +public final class NeoForgeEnergyLookup implements EnergyLookup { + + @Nullable + @Override + public NerospaceEnergyStorage find(Level level, BlockPos pos, @Nullable Direction side) { + return level.getCapability(NeoForgeCapabilities.ENERGY, pos, side); + } +} diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup new file mode 100644 index 0000000..62c3419 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.EnergyLookup @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.NeoForgeEnergyLookup From ebe4620220cb002bcc18b31208023e8d23367db8 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 15:31:46 +0200 Subject: [PATCH 27/82] Add Trash Can and Creative Battery blocks Introduce two new storage blocks: Trash Can (void item/fluid sink) and Creative Battery (infinite energy source). Adds block & block-entity classes, registration in ModBlocks/ModBlockEntities/ModItems, creative tab entries, loot tables, a crafting recipe for the Trash Can, and blockstate/model/texture/item assets. Exposes capabilities for both Fabric (ItemStorage/FLUID/ENERGY) and NeoForge (item/fluid/energy) integration. Also updates migration docs with a 2026-06-20 progress note. --- docs/MULTILOADER_MIGRATION.md | 24 +++- .../nerospace/registry/ModBlockEntities.java | 10 ++ .../nerospace/registry/ModBlocks.java | 14 ++ .../neroland/nerospace/registry/ModItems.java | 4 +- .../storage/CreativeBatteryBlock.java | 37 ++++++ .../storage/CreativeBatteryBlockEntity.java | 42 ++++++ .../nerospace/storage/TrashCanBlock.java | 37 ++++++ .../storage/TrashCanBlockEntity.java | 124 ++++++++++++++++++ .../blockstates/creative_battery.json | 7 + .../nerospace/blockstates/trash_can.json | 7 + .../nerospace/items/creative_battery.json | 6 + .../assets/nerospace/items/trash_can.json | 6 + .../assets/nerospace/lang/en_us.json | 4 +- .../models/block/creative_battery.json | 105 +++++++++++++++ .../nerospace/models/block/trash_can.json | 6 + .../textures/block/creative_battery.png | Bin 0 -> 302 bytes .../textures/block/creative_battery_top.png | Bin 0 -> 431 bytes .../nerospace/textures/block/trash_can.png | Bin 0 -> 455 bytes .../tags/block/mineable/pickaxe.json | 3 +- .../minecraft/tags/block/needs_iron_tool.json | 3 +- .../loot_table/blocks/creative_battery.json | 21 +++ .../loot_table/blocks/trash_can.json | 21 +++ .../data/nerospace/recipe/trash_can.json | 16 +++ .../nerospace/fabric/NerospaceFabric.java | 11 ++ .../neoforge/NeoForgeCapabilities.java | 16 +++ 25 files changed, 518 insertions(+), 6 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_battery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/trash_can.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/creative_battery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/trash_can.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/creative_battery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/trash_can.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery_top.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/trash_can.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_battery.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/trash_can.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/trash_can.json diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index 4bc8eac..822eae3 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -5,8 +5,28 @@ field notes). This file tracks **what content has been ported into `multiloader/ and how the remaining systems should be ported**, based on the concrete NeoForge ↔ Fabric API divergences found during the port. -Last updated: 2026-06-19. Verified build targets: **NeoForge @ 26.1.2** and **Fabric @ 26.2** -(both `BUILD SUCCESSFUL` via the gradle MCP after every batch). +Last updated: 2026-06-20. Verified build targets: all four cells — **NeoForge @ 26.1.2 / 26.2** +and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after every batch. + +> **2026-06-20 progress update.** Every cross-loader **platform mechanism is now built and +> verified**: registration; the item / energy / fluid capability seams (both *expose* and +> *query*); block-entity tickers; and menus + screens. A working **energy network** exists +> (generators → pipe → machines/battery). Ported block entities (10): `item_store`, `battery`, +> `creative_battery`, `fluid_tank`, `trash_can` (item+fluid void), `combustion_generator`, +> `passive_generator`, `nerosium_grinder`, `universal_pipe`. Plus overworld nerosium-ore worldgen. +> All content the seams support **without** a deferred subsystem is now ported. What remains +> (below, §3b/§3c) is **subsystem work**, not per-block batches — each is a focused effort and +> several are runtime-verification-dependent (rendering / world / behavior can't be checked headlessly). +> +> Remaining, by subsystem (rough size): **rocket-fuel fluid** (hard cross-loader fluid registration — +> NeoForge `FluidType`/`BaseFlowingFluid` vs Fabric `FlowableFluid` subclass + render handler; unblocks +> refinery, fuel tank, bucket); **gas system** (`GasResource` + handlers; unblocks oxygen generator, +> gas tanks); **dimensions** (Greenxertz/Cindara/Glacira biomes+dims+travel; unblocks the planet ores' +> worldgen); **entities** (alien villager, xertz stalker + attributes + renderers); **rockets** (items, +> tiers, launch logic); **quarry** (area mining); **structures** (station/village/meteor cores + events); +> **atmosphere/terraforming** (terraformer, monitor, hydration); **solar panel** (tiers + multiblock + +> BER); **star guide** (progression UI); **creative item/fluid stores** (infinite-resource config — marginal). +> Recommended order: rocket-fuel fluid → item-pipe (item query seam) → gas → entities → dimensions → the rest. --- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 83aa277..0d4e927 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -9,6 +9,8 @@ import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; +import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; +import za.co.neroland.nerospace.storage.TrashCanBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; @@ -51,6 +53,14 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("universal_pipe", key -> new BlockEntityType<>(UniversalPipeBlockEntity::new, java.util.Set.of(ModBlocks.UNIVERSAL_PIPE.get()))); + public static final RegistryEntry> TRASH_CAN = + BLOCK_ENTITIES.register("trash_can", + key -> new BlockEntityType<>(TrashCanBlockEntity::new, java.util.Set.of(ModBlocks.TRASH_CAN.get()))); + + public static final RegistryEntry> CREATIVE_BATTERY = + BLOCK_ENTITIES.register("creative_battery", + key -> new BlockEntityType<>(CreativeBatteryBlockEntity::new, java.util.Set.of(ModBlocks.CREATIVE_BATTERY.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 7906353..f904030 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -13,6 +13,8 @@ import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; import za.co.neroland.nerospace.pipe.UniversalPipeBlock; +import za.co.neroland.nerospace.storage.CreativeBatteryBlock; +import za.co.neroland.nerospace.storage.TrashCanBlock; import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; @@ -130,6 +132,18 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.METAL).strength(1.5F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + + public static final RegistryEntry TRASH_CAN = BLOCKS.register("trash_can", + key -> new TrashCanBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_GRAY).strength(2.0F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + + + public static final RegistryEntry CREATIVE_BATTERY = BLOCKS.register("creative_battery", + key -> new CreativeBatteryBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_PINK).strength(-1.0F, 3_600_000.0F) + .sound(SoundType.METAL).noOcclusion())); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 8decb3c..b156b2a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -64,6 +64,8 @@ public final class ModItems { public static final RegistryEntry NEROSIUM_GRINDER_ITEM = blockItem("nerosium_grinder", ModBlocks.NEROSIUM_GRINDER); public static final RegistryEntry PASSIVE_GENERATOR_ITEM = blockItem("passive_generator", ModBlocks.PASSIVE_GENERATOR); public static final RegistryEntry UNIVERSAL_PIPE_ITEM = blockItem("universal_pipe", ModBlocks.UNIVERSAL_PIPE); + public static final RegistryEntry TRASH_CAN_ITEM = blockItem("trash_can", ModBlocks.TRASH_CAN); + public static final RegistryEntry CREATIVE_BATTERY_ITEM = blockItem("creative_battery", ModBlocks.CREATIVE_BATTERY); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -179,7 +181,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlock.java new file mode 100644 index 0000000..6387581 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlock.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Creative Battery block — holds a {@link CreativeBatteryBlockEntity}. */ +public class CreativeBatteryBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(CreativeBatteryBlock::new); + + public CreativeBatteryBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CreativeBatteryBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlockEntity.java new file mode 100644 index 0000000..c9a5f12 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeBatteryBlockEntity.java @@ -0,0 +1,42 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Creative Battery — an endless energy source and sink for testing power grids. */ +public class CreativeBatteryBlockEntity extends BlockEntity { + + private static final NerospaceEnergyStorage INFINITE = new NerospaceEnergyStorage() { + @Override + public long getAmount() { + return Integer.MAX_VALUE; + } + + @Override + public long getCapacity() { + return Integer.MAX_VALUE; + } + + @Override + public long insert(long maxAmount, boolean simulate) { + return Math.max(0, maxAmount); + } + + @Override + public long extract(long maxAmount, boolean simulate) { + return Math.max(0, maxAmount); + } + }; + + public CreativeBatteryBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.CREATIVE_BATTERY.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return INFINITE; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlock.java new file mode 100644 index 0000000..a45c68f --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlock.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Trash Can block — holds a {@link TrashCanBlockEntity} void sink. */ +public class TrashCanBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(TrashCanBlock::new); + + public TrashCanBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new TrashCanBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlockEntity.java new file mode 100644 index 0000000..5f7f533 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/TrashCanBlockEntity.java @@ -0,0 +1,124 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Trash Can — a bottomless sink. Exposes item and fluid surfaces that accept anything (from hoppers + * or pipes) and void it: the item container always reads empty and discards on insert; the fluid + * sink accepts any amount and stores nothing. Nothing can be pulled back out. (Gas voiding is + * deferred with the gas system.) + */ +public class TrashCanBlockEntity extends BlockEntity implements WorldlyContainer { + + private static final int SIZE = 1; + private static final int[] SLOTS = {0}; + + private final NerospaceFluidStorage voidFluid = new NerospaceFluidStorage() { + @Override + public Fluid getFluid() { + return Fluids.EMPTY; + } + + @Override + public long getAmount() { + return 0; + } + + @Override + public long getCapacity() { + return 1_000_000; + } + + @Override + public long fill(Fluid fluid, long amount, boolean simulate) { + return Math.max(0, amount); + } + + @Override + public long drain(long amount, boolean simulate) { + return 0; + } + }; + + public TrashCanBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.TRASH_CAN.get(), pos, state); + } + + public NerospaceFluidStorage getFluid() { + return this.voidFluid; + } + + // --- WorldlyContainer (void item sink) ------------------------------------ + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return true; + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return false; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return true; + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public ItemStack getItem(int slot) { + return ItemStack.EMPTY; + } + + @Override + public ItemStack removeItem(int slot, int amount) { + return ItemStack.EMPTY; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ItemStack.EMPTY; + } + + @Override + public void setItem(int slot, ItemStack stack) { + // void: store nothing + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + // nothing stored + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_battery.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_battery.json new file mode 100644 index 0000000..ce13162 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_battery.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/creative_battery" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/trash_can.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/trash_can.json new file mode 100644 index 0000000..568d42b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/trash_can.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/trash_can" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/creative_battery.json b/multiloader/common/src/main/resources/assets/nerospace/items/creative_battery.json new file mode 100644 index 0000000..174cbf4 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/creative_battery.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/creative_battery" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/trash_can.json b/multiloader/common/src/main/resources/assets/nerospace/items/trash_can.json new file mode 100644 index 0000000..4576c2e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/trash_can.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/trash_can" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index e0e5cda..a811022 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -61,5 +61,7 @@ "container.nerospace.nerosium_grinder": "Nerosium Grinder", "block.nerospace.passive_generator": "Passive Generator", "container.nerospace.passive_generator": "Passive Generator", - "block.nerospace.universal_pipe": "Universal Pipe" + "block.nerospace.universal_pipe": "Universal Pipe", + "block.nerospace.trash_can": "Trash Can", + "block.nerospace.creative_battery": "Creative Battery" } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_battery.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_battery.json new file mode 100644 index 0000000..48a8d6a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_battery.json @@ -0,0 +1,105 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 1, + 0, + 1 + ], + "to": [ + 15, + 14, + 15 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 3, + 14, + 3 + ], + "to": [ + 7, + 16, + 7 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 9, + 14, + 9 + ], + "to": [ + 13, + 16, + 13 + ] + } + ], + "textures": { + "particle": "nerospace:block/creative_battery", + "side": "nerospace:block/creative_battery", + "top": "nerospace:block/creative_battery_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/trash_can.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/trash_can.json new file mode 100644 index 0000000..b063bd6 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/trash_can.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/trash_can" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery.png new file mode 100644 index 0000000000000000000000000000000000000000..8d4814cdb1120d244e44846f1ca7637a782fd1f0 GIT binary patch literal 302 zcmV+}0nz@6P)Nkl05X%zr1wr{j#y?+?~ltXTQTX9(0eB$#sx=a5)lCZ1Y(qe z*4jw$s!p!l#j6Pl_TI-g#VgsOpeF+y)*W2n^W}V}(6RN35kW*u6L9 zb@y;aIu93QnF9Okks9-7nnCq_sQ9@l@#?z6Y6RY+lC)qMw`EeRlLGIl+VyBI-ooU9 z0`*;;al)~QO7Q)!)g0QJI|7%lb9KS7dG>ey0~l_@uh5&OmjD0&07*qoM6N<$g1K3V AX#fBK literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery_top.png new file mode 100644 index 0000000000000000000000000000000000000000..43b02c05f2c94a107265194f7ce567b5e17eb455 GIT binary patch literal 431 zcmV;g0Z{&lP){!oRiPMG097}2`bA)71RWRu5Cz- zr8?UJgr@pbkS@}GAV`f3jnr5Mafo=zMFl`puRqNxSn#e7eg|<(i#c7}AmUjiTj(Fh z4>-wxklCEg)592I*EYeg+R?CLxp6LvnZ)TTFjl2G#3kax>;!hNE-vqSGMj(syx5MLQIY2=J)fvP2;mpqBFoaw>&@G5!Cl9j!r+o|F>EI ZUI80vLyNR?sG8V22`NUm5uuqaP>we2X@z z)b`pm+alV`iPGAn!XWAspPcdZ+oH2)D7-ZJd@H%WGRzm}0wN54eqRD`lv?V#656Bj xR8*gSnMrNDS^|*YZTR~=?ZcbcX1#h7djS9>y8f-eh&%uQ002ovPDHLkV1gf6%uWCR literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json index a87f3d3..5fd2c2b 100644 --- a/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -26,6 +26,7 @@ "nerospace:combustion_generator", "nerospace:nerosium_grinder", "nerospace:passive_generator", - "nerospace:universal_pipe" + "nerospace:universal_pipe", + "nerospace:trash_can" ] } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json index bbdfa0f..7bccbc4 100644 --- a/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json @@ -18,6 +18,7 @@ "nerospace:combustion_generator", "nerospace:nerosium_grinder", "nerospace:passive_generator", - "nerospace:universal_pipe" + "nerospace:universal_pipe", + "nerospace:trash_can" ] } \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_battery.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_battery.json new file mode 100644 index 0000000..6ed086b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_battery.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:creative_battery" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/creative_battery" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/trash_can.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/trash_can.json new file mode 100644 index 0000000..7ddc66e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/trash_can.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:trash_can" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/trash_can" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/trash_can.json b/multiloader/common/src/main/resources/data/nerospace/recipe/trash_can.json new file mode 100644 index 0000000..5159299 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/trash_can.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "minecraft:cactus", + "I": "#c:ingots/iron" + }, + "pattern": [ + "III", + "ICI", + "III" + ], + "result": { + "id": "nerospace:trash_can" + } +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 211ae80..10a5ae1 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -89,6 +89,17 @@ public void onInitialize() { ENERGY.registerForBlockEntity( (be, direction) -> be.getEnergy(), ModBlockEntities.UNIVERSAL_PIPE.get()); + + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.TRASH_CAN.get()); + FLUID.registerForBlockEntity( + (be, direction) -> be.getFluid(), + ModBlockEntities.TRASH_CAN.get()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.CREATIVE_BATTERY.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 4384c19..90efb7a 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -99,5 +99,21 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.UNIVERSAL_PIPE.get(), (be, side) -> be.getEnergy()); + + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.TRASH_CAN.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + event.registerBlockEntity( + FLUID, + ModBlockEntities.TRASH_CAN.get(), + (be, side) -> be.getFluid()); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.CREATIVE_BATTERY.get(), + (be, side) -> be.getEnergy()); } } From d3ee538ebdf5d32a43af0244bae3b1be309ccb83 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:09:13 +0200 Subject: [PATCH 28/82] Port rocket-fuel fluid and gas systems Add cross-loader rocket-fuel fluid and a new gas layer plus related machines and assets. - Fluid: introduce FluidFactory seam and ModFluids to register rocket_fuel (source + flowing), add RocketFuelLiquidBlock and rocket_fuel_bucket item; add per-loader platform glue (Fabric/NeoForge) and note Fabric render caveat in docs. ModRegistries now initializes fluids before blocks/items. - Gas layer: add GasResource enum, NerospaceGasStorage interface, GasTank implementation and GasLookup seam; add GasTankBlock/GasTankBlockEntity and register them. - Machines: add OxygenGenerator and SolarPanel blocks + block entities (oxygen generator consumes energy to produce O2; solar panel generates energy). Update ModBlockEntities/ModBlocks/ModItems to register new blocks/items and include them in creative tabs. - Universal pipe: extend UniversalPipeBlockEntity to carry/relay gas (single-gas buffer), persist gas state, and use GasLookup in addition to EnergyLookup. - Assets & data: add block/item models, textures, loot tables, recipes and language strings for the new content. - Docs: update MULTILOADER_MIGRATION.md with notes about the fluid and gas ports and known follow-ups. These changes unblock the refinery/fuel tank and a basic oxygen/gas transport network across loaders. --- docs/MULTILOADER_MIGRATION.md | 37 +- .../neroland/nerospace/fluid/ModFluids.java | 35 ++ .../fluid/RocketFuelLiquidBlock.java | 18 + .../neroland/nerospace/gas/GasResource.java | 55 +++ .../za/co/neroland/nerospace/gas/GasTank.java | 82 ++++ .../nerospace/gas/NerospaceGasStorage.java | 22 + .../machine/OxygenGeneratorBlock.java | 52 +++ .../machine/OxygenGeneratorBlockEntity.java | 102 +++++ .../nerospace/machine/SolarPanelBlock.java | 52 +++ .../machine/SolarPanelBlockEntity.java | 63 +++ .../pipe/UniversalPipeBlockEntity.java | 67 ++- .../nerospace/platform/FluidFactory.java | 20 + .../nerospace/platform/GasLookup.java | 22 + .../nerospace/registry/ModBlockEntities.java | 15 + .../nerospace/registry/ModBlocks.java | 30 ++ .../neroland/nerospace/registry/ModItems.java | 13 +- .../nerospace/registry/ModRegistries.java | 6 +- .../nerospace/storage/GasTankBlock.java | 37 ++ .../nerospace/storage/GasTankBlockEntity.java | 42 ++ .../nerospace/blockstates/gas_tank.json | 7 + .../blockstates/oxygen_generator.json | 7 + .../nerospace/blockstates/rocket_fuel.json | 7 + .../nerospace/blockstates/solar_panel.json | 7 + .../assets/nerospace/items/gas_tank.json | 6 + .../nerospace/items/oxygen_generator.json | 6 + .../nerospace/items/rocket_fuel_bucket.json | 6 + .../assets/nerospace/items/solar_panel.json | 6 + .../assets/nerospace/lang/en_us.json | 8 + .../nerospace/models/block/gas_tank.json | 425 ++++++++++++++++++ .../models/block/oxygen_generator.json | 105 +++++ .../nerospace/models/block/rocket_fuel.json | 5 + .../nerospace/models/block/solar_panel.json | 8 + .../models/item/rocket_fuel_bucket.json | 6 + .../nerospace/textures/block/gas_tank.png | Bin 0 -> 315 bytes .../textures/block/gas_tank_core.png | Bin 0 -> 253 bytes .../textures/block/oxygen_generator.png | Bin 0 -> 527 bytes .../textures/block/oxygen_generator_top.png | Bin 0 -> 444 bytes .../nerospace/textures/block/rocket_fuel.png | Bin 0 -> 324 bytes .../textures/block/rocket_fuel_flow.png | Bin 0 -> 541 bytes .../textures/block/rocket_fuel_still.png | Bin 0 -> 574 bytes .../nerospace/textures/block/solar_panel.png | Bin 0 -> 262 bytes .../textures/block/solar_panel_base.png | Bin 0 -> 237 bytes .../textures/item/rocket_fuel_bucket.png | Bin 0 -> 208 bytes .../nerospace/loot_table/blocks/gas_tank.json | 21 + .../loot_table/blocks/oxygen_generator.json | 21 + .../loot_table/blocks/solar_panel.json | 12 + .../data/nerospace/recipe/gas_tank.json | 16 + .../nerospace/recipe/oxygen_generator.json | 18 + .../nerospace/fabric/NerospaceFabric.java | 25 ++ .../nerospace/fluid/RocketFuelFluid.java | 119 +++++ .../platform/FabricFluidFactory.java | 19 + .../nerospace/platform/FabricGasLookup.java | 20 + ...o.neroland.nerospace.platform.FluidFactory | 1 + ...a.co.neroland.nerospace.platform.GasLookup | 1 + .../neoforge/NeoForgeCapabilities.java | 30 ++ .../neoforge/NeoForgeClientSetup.java | 19 +- .../nerospace/neoforge/NerospaceNeoForge.java | 2 + .../platform/NeoForgeFluidFactory.java | 56 +++ .../nerospace/platform/NeoForgeGasLookup.java | 20 + ...o.neroland.nerospace.platform.FluidFactory | 1 + ...a.co.neroland.nerospace.platform.GasLookup | 1 + 61 files changed, 1763 insertions(+), 18 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/ModFluids.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelLiquidBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasResource.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasTank.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/gas/NerospaceGasStorage.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidFactory.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/GasLookup.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/gas_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/oxygen_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_fuel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/gas_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/oxygen_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_bucket.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/solar_panel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/gas_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/oxygen_generator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_fuel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_bucket.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator_top.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_flow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_still.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_base.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_bucket.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/gas_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/oxygen_generator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/gas_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_generator.json create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelFluid.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidFactory.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricGasLookup.java create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidFactory.java create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeGasLookup.java create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index 822eae3..954f499 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -18,15 +18,34 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > (below, §3b/§3c) is **subsystem work**, not per-block batches — each is a focused effort and > several are runtime-verification-dependent (rendering / world / behavior can't be checked headlessly). > -> Remaining, by subsystem (rough size): **rocket-fuel fluid** (hard cross-loader fluid registration — -> NeoForge `FluidType`/`BaseFlowingFluid` vs Fabric `FlowableFluid` subclass + render handler; unblocks -> refinery, fuel tank, bucket); **gas system** (`GasResource` + handlers; unblocks oxygen generator, -> gas tanks); **dimensions** (Greenxertz/Cindara/Glacira biomes+dims+travel; unblocks the planet ores' -> worldgen); **entities** (alien villager, xertz stalker + attributes + renderers); **rockets** (items, -> tiers, launch logic); **quarry** (area mining); **structures** (station/village/meteor cores + events); -> **atmosphere/terraforming** (terraformer, monitor, hydration); **solar panel** (tiers + multiblock + -> BER); **star guide** (progression UI); **creative item/fluid stores** (infinite-resource config — marginal). -> Recommended order: rocket-fuel fluid → item-pipe (item query seam) → gas → entities → dimensions → the rest. +> **2026-06-20 (later): rocket-fuel FLUID ported** — all 4 cells green. A `FluidFactory` platform +> seam creates the still/flowing `Fluid`: NeoForge uses `BaseFlowingFluid` backed by a registered +> `FluidType`; Fabric uses a hand-written vanilla `FlowingFluid` subclass (`RocketFuelFluid`, override +> set mirrors `WaterFluid`). Common registers the fluids (`ModFluids` + the seam), the `LiquidBlock` +> (`RocketFuelLiquidBlock`, a public-ctor subclass to dodge the protected vanilla ctor cross-loader), +> and `rocket_fuel_bucket` (`BucketItem`). `ModRegistries` now runs fluids first (eager-Fabric order). +> In-world fluid rendering is wired on NeoForge (`RegisterFluidModelsEvent` + `FluidModel.Unbaked`, both +> 26.1.2 & 26.2). **Known follow-up:** the Fabric client fluid-render module (`fabric-api` +> `FluidRenderHandlerRegistry`) is not on the de-obf Loom classpath here, so on Fabric the liquid renders +> with the default texture in-world (bucket icon, tank storage, and all behaviour still work); revisit +> when that fabric-api module is available. This unblocks the refinery / fuel tank. +> +> **2026-06-20 (later still): GAS layer ported** — all 4 cells green. Self-contained cross-loader gas +> layer mirroring the energy/fluid seams: `GasResource` (plain vanilla enum, replacing the root's +> NeoForge-transfer `Resource`), `NerospaceGasStorage` + `GasTank`, and a `GasLookup` query seam +> (NeoForge `BlockCapability` `nerospace:gas` + Fabric `BlockApiLookup`). Ported blocks: `gas_tank` +> and a GUI-less `oxygen_generator` (grid-powered electrolyser: spends energy → synthesises oxygen into +> an extract-only gas port). The **universal pipe now relays gas as well as energy**, so the network runs +> end-to-end: generator → pipe → oxygen generator → pipe → gas tank. (The world oxygen-field effect + +> HUD + the generator GUI are a deferred atmosphere subsystem.) +> +> Remaining, by subsystem (rough size): **dimensions** (Greenxertz/Cindara/Glacira biomes+dims+travel; +> unblocks the planet ores' worldgen); **entities** (alien villager, xertz stalker + attributes + +> renderers); **rockets** (items, tiers, launch logic); **quarry** (area mining); **structures** +> (station/village/meteor cores + events); **atmosphere/terraforming** (oxygen field, terraformer, +> monitor, hydration); **solar panel** (tiers + multiblock + BER); **star guide** (progression UI); +> **item-pipe** (item query seam in the universal pipe); **creative item/fluid/gas stores** +> (infinite-resource config — marginal). Recommended order: item-pipe → entities → dimensions → rockets → the rest. --- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/ModFluids.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/ModFluids.java new file mode 100644 index 0000000..7976097 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/ModFluids.java @@ -0,0 +1,35 @@ +package za.co.neroland.nerospace.fluid; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.material.Fluid; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.platform.FluidFactory; +import za.co.neroland.nerospace.registry.RegistrationProvider; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * The {@code rocket_fuel} fluid (Phase 7b), ported cross-loader. Registers a still + flowing + * {@link Fluid} into the vanilla fluid registry through the {@link RegistrationProvider} seam; the + * concrete instances come from the per-loader {@link FluidFactory} (NeoForge + * {@code BaseFlowingFluid} + {@code FluidType}; Fabric {@link RocketFuelFluid}). + * + *

Registered BEFORE blocks/items (see {@code ModRegistries}) because the liquid block and bucket + * resolve {@link #ROCKET_FUEL} at their own registration time on the eager (Fabric) loader. + */ +public final class ModFluids { + + public static final RegistrationProvider FLUIDS = + RegistrationProvider.get(Registries.FLUID, NerospaceCommon.MOD_ID); + + public static final RegistryEntry ROCKET_FUEL = + FLUIDS.register("rocket_fuel", key -> FluidFactory.INSTANCE.createSource()); + public static final RegistryEntry ROCKET_FUEL_FLOWING = + FLUIDS.register("flowing_rocket_fuel", key -> FluidFactory.INSTANCE.createFlowing()); + + private ModFluids() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelLiquidBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelLiquidBlock.java new file mode 100644 index 0000000..fb82353 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelLiquidBlock.java @@ -0,0 +1,18 @@ +package za.co.neroland.nerospace.fluid; + +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.FlowingFluid; + +/** + * Trivial public subclass of {@link LiquidBlock} for the {@code rocket_fuel} world block. Vanilla's + * {@code LiquidBlock} constructor is {@code protected}, so common (which compiles against vanilla on + * both loaders) cannot {@code new} it directly — a subclass in this package can, giving one + * cross-loader registration point with no per-loader access widener for the constructor. + */ +public class RocketFuelLiquidBlock extends LiquidBlock { + + public RocketFuelLiquidBlock(FlowingFluid fluid, BlockBehaviour.Properties properties) { + super(fluid, properties); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasResource.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasResource.java new file mode 100644 index 0000000..e044a50 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasResource.java @@ -0,0 +1,55 @@ +package za.co.neroland.nerospace.gas; + +import com.mojang.serialization.Codec; + +import net.minecraft.network.chat.Component; +import net.minecraft.util.StringRepresentable; + +/** + * A gas the mod's logistics can store and move — the resource side of the dedicated gas layer. Ported + * cross-loader as a plain vanilla enum (the root project built it on NeoForge's transfer + * {@code Resource} framework, which is loader-specific). Oxygen is the first gas; more (hydrogen, fuel + * vapour, ...) can be added as new constants without touching the transport. + */ +public enum GasResource implements StringRepresentable { + EMPTY("empty", 0x00000000), + OXYGEN("oxygen", 0xFF54D46A); + + public static final Codec CODEC = StringRepresentable.fromEnum(GasResource::values); + + private final String name; + private final int color; + + GasResource(String name, int color) { + this.name = name; + this.color = color; + } + + public boolean isEmpty() { + return this == EMPTY; + } + + /** Display colour (ARGB) for streams and gauges. */ + public int color() { + return this.color; + } + + public Component label() { + return Component.translatable("gas.nerospace." + this.name); + } + + @Override + public String getSerializedName() { + return this.name; + } + + /** Parse a serialized name back to a constant (defaults to {@link #EMPTY}). */ + public static GasResource byName(String name) { + for (GasResource gas : values()) { + if (gas.name.equals(name)) { + return gas; + } + } + return EMPTY; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasTank.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasTank.java new file mode 100644 index 0000000..b3fc392 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/GasTank.java @@ -0,0 +1,82 @@ +package za.co.neroland.nerospace.gas; + +/** Single-gas bounded tank (millibuckets) backing a block entity. Mirrors {@code FluidTank}. */ +public final class GasTank implements NerospaceGasStorage { + + private GasResource gas = GasResource.EMPTY; + private long amount; + private final long capacity; + private final Runnable onChanged; + + public GasTank(long capacity, Runnable onChanged) { + this.capacity = capacity; + this.onChanged = onChanged; + } + + @Override + public GasResource getGas() { + return this.gas; + } + + @Override + public long getAmount() { + return this.amount; + } + + @Override + public long getCapacity() { + return this.capacity; + } + + @Override + public long fill(GasResource gas, long amount, boolean simulate) { + if (amount <= 0 || gas.isEmpty()) { + return 0; + } + if (!this.gas.isEmpty() && this.gas != gas) { + return 0; + } + long filled = Math.min(amount, this.capacity - this.amount); + if (filled > 0 && !simulate) { + if (this.gas.isEmpty()) { + this.gas = gas; + } + this.amount += filled; + this.onChanged.run(); + } + return filled; + } + + @Override + public long drain(long amount, boolean simulate) { + if (amount <= 0 || this.amount == 0) { + return 0; + } + long drained = Math.min(amount, this.amount); + if (!simulate) { + this.amount -= drained; + if (this.amount == 0) { + this.gas = GasResource.EMPTY; + } + this.onChanged.run(); + } + return drained; + } + + // Raw accessors for NBT save/load. + public GasResource getRawGas() { + return this.gas; + } + + public int getRawAmount() { + return (int) this.amount; + } + + public void setRaw(GasResource gas, int amount) { + this.gas = gas; + this.amount = Math.max(0, Math.min((int) this.capacity, amount)); + if (this.amount == 0) { + this.gas = GasResource.EMPTY; + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/NerospaceGasStorage.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/NerospaceGasStorage.java new file mode 100644 index 0000000..f5932b4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/gas/NerospaceGasStorage.java @@ -0,0 +1,22 @@ +package za.co.neroland.nerospace.gas; + +/** + * Loader-neutral single-gas store (amount in millibuckets), exposed per loader via a mod-owned + * capability/lookup — the gas analogue of {@link za.co.neroland.nerospace.fluid.NerospaceFluidStorage} + * and {@link za.co.neroland.nerospace.energy.NerospaceEnergyStorage}. + */ +public interface NerospaceGasStorage { + + /** The stored gas, or {@link GasResource#EMPTY} if empty. */ + GasResource getGas(); + + long getAmount(); + + long getCapacity(); + + /** Fill with {@code gas} (must match the stored gas unless empty). @return mB filled. */ + long fill(GasResource gas, long amount, boolean simulate); + + /** Drain the stored gas. @return mB drained. */ + long drain(long amount, boolean simulate); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlock.java new file mode 100644 index 0000000..f730dc4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlock.java @@ -0,0 +1,52 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Oxygen Generator block — ticks its {@link OxygenGeneratorBlockEntity}. GUI-less for now. */ +public class OxygenGeneratorBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(OxygenGeneratorBlock::new); + + public OxygenGeneratorBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new OxygenGeneratorBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.OXYGEN_GENERATOR.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java new file mode 100644 index 0000000..fb85aeb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java @@ -0,0 +1,102 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.gas.GasResource; +import za.co.neroland.nerospace.gas.GasTank; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Oxygen Generator — a grid-powered electrolyser: each tick it spends energy from its internal buffer + * to synthesise {@link GasResource#OXYGEN} into its gas tank. Exposes the energy capability (insert + * only, fed by the pipe network) and the gas capability (extract only, tapped by the pipe network / + * adjacent gas tanks). GUI-less for now; the world oxygen-field effect is a deferred atmosphere system. + */ +public class OxygenGeneratorBlockEntity extends BlockEntity { + + public static final int ENERGY_CAPACITY = 50_000; + public static final int GAS_CAPACITY = 8_000; + public static final int MAX_INSERT = 1_000; + public static final int MB_PER_TICK = 4; + public static final int FE_PER_MB = 20; + + private final EnergyBuffer energy = new EnergyBuffer(ENERGY_CAPACITY, MAX_INSERT, 0, this::setChanged); + private final GasTank gas = new GasTank(GAS_CAPACITY, this::setChanged); + + public OxygenGeneratorBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.OXYGEN_GENERATOR.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + /** Extract-only view of the gas tank — the pipe/network may pull oxygen out but not push it back. */ + public NerospaceGasStorage getGas() { + return new NerospaceGasStorage() { + @Override + public GasResource getGas() { + return gas.getGas(); + } + + @Override + public long getAmount() { + return gas.getAmount(); + } + + @Override + public long getCapacity() { + return gas.getCapacity(); + } + + @Override + public long fill(GasResource g, long amount, boolean simulate) { + return 0; + } + + @Override + public long drain(long amount, boolean simulate) { + return gas.drain(amount, simulate); + } + }; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + long room = this.gas.getCapacity() - this.gas.getAmount(); + int produce = (int) Math.min(MB_PER_TICK, room); + if (produce <= 0) { + return; + } + int cost = produce * FE_PER_MB; + if (this.energy.getAmount() >= cost) { + this.energy.consume(cost); + this.gas.fill(GasResource.OXYGEN, produce, false); + } + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + output.putString("Gas", this.gas.getRawGas().getSerializedName()); + output.putInt("GasAmount", this.gas.getRawAmount()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + this.gas.setRaw(GasResource.byName(input.getStringOr("Gas", "empty")), input.getIntOr("GasAmount", 0)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java new file mode 100644 index 0000000..5cc74ca --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java @@ -0,0 +1,52 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Solar Panel block — ticks its {@link SolarPanelBlockEntity}. GUI-less, single-tier. */ +public class SolarPanelBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(SolarPanelBlock::new); + + public SolarPanelBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new SolarPanelBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.SOLAR_PANEL.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java new file mode 100644 index 0000000..947eadd --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java @@ -0,0 +1,63 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Solar Panel — a daylight generator: while it is day and the panel can see the sky it trickles energy + * into its buffer (reduced in rain). Exposes the energy capability (extract-only, drained by the pipe + * network). Single-tier and GUI-less here; the root project's tiered sun-tracking array + renderer are + * a deferred enhancement. + */ +public class SolarPanelBlockEntity extends BlockEntity { + + public static final int CAPACITY = 40_000; + public static final int MAX_EXTRACT = 256; + public static final int FE_PER_TICK = 16; + + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, 0, MAX_EXTRACT, this::setChanged); + + public SolarPanelBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.SOLAR_PANEL.get(), pos, state); + } + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + long dayTime = level.getDayTime() % 24000L; + boolean day = dayTime < 12300L || dayTime > 23850L; + if (!day || !level.canSeeSky(pos.above())) { + return; + } + int rate = FE_PER_TICK; + if (level.isRaining() || level.isThundering()) { + rate /= 2; + } + this.energy.generate(rate); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java index 8ba1335..09543c9 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -10,21 +10,29 @@ import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.gas.GasResource; +import za.co.neroland.nerospace.gas.GasTank; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.platform.EnergyLookup; +import za.co.neroland.nerospace.platform.GasLookup; import za.co.neroland.nerospace.registry.ModBlockEntities; /** - * Universal Pipe — relays energy between adjacent storages. Each tick it pulls from neighbours that - * allow extraction (generators, other pipes) into its small buffer, then pushes into neighbours that - * allow insertion (machines, batteries). Direction is enforced naturally by each storage's own - * insert/extract limits. Uses {@link EnergyLookup} — the query side of the cross-loader energy seam. + * Universal Pipe — relays energy AND gas between adjacent storages. Each tick it pulls from + * neighbours that allow extraction (generators, other pipes) into its small buffers, then pushes into + * neighbours that allow insertion (machines, tanks, batteries). Direction is enforced naturally by + * each storage's own insert/extract limits. Uses {@link EnergyLookup} / {@link GasLookup} — the query + * sides of the cross-loader seams. The gas buffer holds one gas type at a time (only oxygen exists yet). */ public class UniversalPipeBlockEntity extends BlockEntity { public static final int CAPACITY = 8_000; public static final int MAX_IO = 1_000; + public static final int GAS_CAPACITY = 8_000; + public static final int GAS_MAX_IO = 1_000; private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_IO, MAX_IO, this::setChanged); + private final GasTank gas = new GasTank(GAS_CAPACITY, this::setChanged); public UniversalPipeBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.UNIVERSAL_PIPE.get(), pos, state); @@ -34,10 +42,19 @@ public NerospaceEnergyStorage getEnergy() { return this.energy; } + public NerospaceGasStorage getGas() { + return this.gas; + } + public void tick(Level level, BlockPos pos, BlockState state) { if (level.isClientSide()) { return; } + relayEnergy(level, pos); + relayGas(level, pos); + } + + private void relayEnergy(Level level, BlockPos pos) { // Pull from extractable neighbours into the buffer. for (Direction dir : Direction.values()) { NerospaceEnergyStorage neighbour = EnergyLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); @@ -69,15 +86,57 @@ public void tick(Level level, BlockPos pos, BlockState state) { } } + private void relayGas(Level level, BlockPos pos) { + // Pull a (single) gas type from extractable neighbours into the buffer. + for (Direction dir : Direction.values()) { + long room = this.gas.getCapacity() - this.gas.getAmount(); + if (room <= 0) { + break; + } + NerospaceGasStorage neighbour = GasLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); + if (neighbour == null) { + continue; + } + GasResource ngas = neighbour.getGas(); + if (ngas.isEmpty() || (!this.gas.getGas().isEmpty() && this.gas.getGas() != ngas)) { + continue; + } + long available = neighbour.drain(Math.min(room, GAS_MAX_IO), true); + long moved = this.gas.fill(ngas, available, false); + if (moved > 0) { + neighbour.drain(moved, false); + } + } + // Push the buffered gas into insertable neighbours. + for (Direction dir : Direction.values()) { + if (this.gas.getAmount() <= 0) { + break; + } + NerospaceGasStorage neighbour = GasLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); + if (neighbour == null) { + continue; + } + GasResource g = this.gas.getGas(); + long offered = this.gas.drain(Math.min(this.gas.getAmount(), GAS_MAX_IO), true); + long accepted = neighbour.fill(g, offered, false); + if (accepted > 0) { + this.gas.drain(accepted, false); + } + } + } + @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); output.putInt("Energy", this.energy.getRaw()); + output.putString("Gas", this.gas.getRawGas().getSerializedName()); + output.putInt("GasAmount", this.gas.getRawAmount()); } @Override protected void loadAdditional(ValueInput input) { super.loadAdditional(input); this.energy.setRaw(input.getIntOr("Energy", 0)); + this.gas.setRaw(GasResource.byName(input.getStringOr("Gas", "empty")), input.getIntOr("GasAmount", 0)); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidFactory.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidFactory.java new file mode 100644 index 0000000..19d0a28 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidFactory.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.world.level.material.Fluid; + +/** + * Creation side of the fluid seam: builds the still + flowing {@link Fluid} instances for + * {@code rocket_fuel}. The loaders diverge here because NeoForge requires every {@code Fluid} to + * carry a {@code FluidType} (so it uses {@code BaseFlowingFluid} built from a registered type), + * whereas Fabric/vanilla has no such concept (so it uses a hand-written {@code FlowingFluid} + * subclass). Common only ever sees plain {@link Fluid}, registered through + * {@link za.co.neroland.nerospace.fluid.ModFluids}. Resolved via {@link Services}. + */ +public interface FluidFactory { + + FluidFactory INSTANCE = Services.load(FluidFactory.class); + + Fluid createSource(); + + Fluid createFlowing(); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/GasLookup.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/GasLookup.java new file mode 100644 index 0000000..f978fb5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/GasLookup.java @@ -0,0 +1,22 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.gas.NerospaceGasStorage; + +/** + * Query side of the gas seam: find the gas storage exposed by the block at {@code pos} on + * {@code side}. Mirrors {@link EnergyLookup} — NeoForge implements it over {@code Level.getCapability}, + * Fabric over {@code BlockApiLookup.find}. Resolved via {@link Services}. + */ +public interface GasLookup { + + GasLookup INSTANCE = Services.load(GasLookup.class); + + @Nullable + NerospaceGasStorage find(Level level, BlockPos pos, @Nullable Direction side); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 0d4e927..cbd7d83 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -7,9 +7,12 @@ import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; +import za.co.neroland.nerospace.machine.OxygenGeneratorBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; +import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; +import za.co.neroland.nerospace.storage.GasTankBlockEntity; import za.co.neroland.nerospace.storage.TrashCanBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; @@ -61,6 +64,18 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("creative_battery", key -> new BlockEntityType<>(CreativeBatteryBlockEntity::new, java.util.Set.of(ModBlocks.CREATIVE_BATTERY.get()))); + public static final RegistryEntry> GAS_TANK = + BLOCK_ENTITIES.register("gas_tank", + key -> new BlockEntityType<>(GasTankBlockEntity::new, java.util.Set.of(ModBlocks.GAS_TANK.get()))); + + public static final RegistryEntry> OXYGEN_GENERATOR = + BLOCK_ENTITIES.register("oxygen_generator", + key -> new BlockEntityType<>(OxygenGeneratorBlockEntity::new, java.util.Set.of(ModBlocks.OXYGEN_GENERATOR.get()))); + + public static final RegistryEntry> SOLAR_PANEL = + BLOCK_ENTITIES.register("solar_panel", + key -> new BlockEntityType<>(SolarPanelBlockEntity::new, java.util.Set.of(ModBlocks.SOLAR_PANEL.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index f904030..cdf54e9 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -4,16 +4,23 @@ import net.minecraft.core.registries.Registries; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.LiquidBlock; import net.minecraft.world.level.block.SoundType; import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.material.FlowingFluid; import net.minecraft.world.level.material.MapColor; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.fluid.RocketFuelLiquidBlock; import za.co.neroland.nerospace.machine.CombustionGeneratorBlock; import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; +import za.co.neroland.nerospace.machine.OxygenGeneratorBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; +import za.co.neroland.nerospace.machine.SolarPanelBlock; import za.co.neroland.nerospace.pipe.UniversalPipeBlock; import za.co.neroland.nerospace.storage.CreativeBatteryBlock; +import za.co.neroland.nerospace.storage.GasTankBlock; import za.co.neroland.nerospace.storage.TrashCanBlock; import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; @@ -144,6 +151,29 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_PINK).strength(-1.0F, 3_600_000.0F) .sound(SoundType.METAL).noOcclusion())); + public static final RegistryEntry GAS_TANK = BLOCKS.register("gas_tank", + key -> new GasTankBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.0F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + + public static final RegistryEntry OXYGEN_GENERATOR = BLOCKS.register("oxygen_generator", + key -> new OxygenGeneratorBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL))); + + public static final RegistryEntry SOLAR_PANEL = BLOCKS.register("solar_panel", + key -> new SolarPanelBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + + // Rocket fuel world block (placed by the bucket). LiquidBlock holds the source fluid, resolved + // lazily on NeoForge / after ModFluids.init() on Fabric — hence ModFluids registers first. + public static final RegistryEntry ROCKET_FUEL_BLOCK = BLOCKS.register("rocket_fuel", + key -> new RocketFuelLiquidBlock((FlowingFluid) ModFluids.ROCKET_FUEL.get(), + BlockBehaviour.Properties.of().setId(key) + .mapColor(MapColor.COLOR_ORANGE).replaceable().noCollision() + .strength(100.0F).noLootTable())); + private static RegistryEntry block(String name, UnaryOperator props) { return BLOCKS.register(name, key -> new Block(props.apply(BlockBehaviour.Properties.of().setId(key)))); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index b156b2a..2f60762 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -11,10 +11,12 @@ import net.minecraft.tags.BlockTags; import net.minecraft.tags.TagKey; import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.BucketItem; import net.minecraft.world.item.CreativeModeTab; import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.Item; import net.minecraft.world.item.ToolMaterial; +import net.minecraft.world.level.material.Fluid; import net.minecraft.world.item.equipment.ArmorMaterial; import net.minecraft.world.item.equipment.ArmorType; import net.minecraft.world.item.equipment.EquipmentAsset; @@ -23,6 +25,7 @@ import net.minecraft.world.level.block.Block; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.fluid.ModFluids; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** @@ -66,6 +69,9 @@ public final class ModItems { public static final RegistryEntry UNIVERSAL_PIPE_ITEM = blockItem("universal_pipe", ModBlocks.UNIVERSAL_PIPE); public static final RegistryEntry TRASH_CAN_ITEM = blockItem("trash_can", ModBlocks.TRASH_CAN); public static final RegistryEntry CREATIVE_BATTERY_ITEM = blockItem("creative_battery", ModBlocks.CREATIVE_BATTERY); + public static final RegistryEntry GAS_TANK_ITEM = blockItem("gas_tank", ModBlocks.GAS_TANK); + public static final RegistryEntry OXYGEN_GENERATOR_ITEM = blockItem("oxygen_generator", ModBlocks.OXYGEN_GENERATOR); + public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -80,6 +86,9 @@ public final class ModItems { public static final RegistryEntry ALIEN_TECH_SCRAP = item("alien_tech_scrap"); public static final RegistryEntry ALIEN_CORE = item("alien_core"); public static final RegistryEntry ROCKET_FUEL_CANISTER = item("rocket_fuel_canister"); + /** A real bucket of the {@code rocket_fuel} fluid; places the liquid block / fills tanks. */ + public static final RegistryEntry ROCKET_FUEL_BUCKET = ITEMS.register("rocket_fuel_bucket", + key -> new BucketItem((Fluid) ModFluids.ROCKET_FUEL.get(), new Item.Properties().stacksTo(1).setId(key))); public static final RegistryEntry FRAME_CASING = item("frame_casing"); public static final RegistryEntry GRAV_STRIDERS = item("grav_striders"); public static final RegistryEntry DRIFT_FLEECE = item("drift_fleece"); @@ -173,7 +182,7 @@ public static Map, List> creativeTabItems NEROSIUM_DUST.get(), ALIEN_FRAGMENT.get(), ALIEN_TECH_SCRAP.get(), ALIEN_CORE.get(), ROCKET_FUEL_CANISTER.get(), FRAME_CASING.get(), GRAV_STRIDERS.get(), DRIFT_FLEECE.get()), CreativeModeTabs.TOOLS_AND_UTILITIES, - List.of(NEROSIUM_PICKAXE.get()), + List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get()), CreativeModeTabs.COMBAT, List.of( OXYGEN_SUIT_HELMET.get(), OXYGEN_SUIT_CHESTPLATE.get(), OXYGEN_SUIT_LEGGINGS.get(), OXYGEN_SUIT_BOOTS.get(), @@ -181,7 +190,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index ba2cc30..4593635 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -2,8 +2,9 @@ /** * Aggregates the cross-loader content registries. Called once from - * {@link za.co.neroland.nerospace.NerospaceCommon#init()}. Order matters: - * blocks before items (the block item references its block). + * {@link za.co.neroland.nerospace.NerospaceCommon#init()}. Order matters on the eager (Fabric) + * loader: fluids before blocks/items (the liquid block + bucket resolve the fluid at construction), + * and blocks before items (the block item references its block). */ public final class ModRegistries { @@ -11,6 +12,7 @@ private ModRegistries() { } public static void init() { + za.co.neroland.nerospace.fluid.ModFluids.init(); ModBlocks.init(); ModItems.init(); ModBlockEntities.init(); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlock.java new file mode 100644 index 0000000..e8d5cf7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlock.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Gas Tank block — holds a {@link GasTankBlockEntity}. */ +public class GasTankBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(GasTankBlock::new); + + public GasTankBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new GasTankBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlockEntity.java new file mode 100644 index 0000000..aaf13dd --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/GasTankBlockEntity.java @@ -0,0 +1,42 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.gas.GasResource; +import za.co.neroland.nerospace.gas.GasTank; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Gas Tank — a single-gas buffer block entity, exposed via the mod's gas capability/lookup. */ +public class GasTankBlockEntity extends BlockEntity { + + public static final int CAPACITY = 16_000; // mB + + private final GasTank tank = new GasTank(CAPACITY, this::setChanged); + + public GasTankBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.GAS_TANK.get(), pos, state); + } + + public NerospaceGasStorage getTank() { + return this.tank; + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putString("Gas", this.tank.getRawGas().getSerializedName()); + output.putInt("Amount", this.tank.getRawAmount()); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + GasResource gas = GasResource.byName(input.getStringOr("Gas", "empty")); + this.tank.setRaw(gas, input.getIntOr("Amount", 0)); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/gas_tank.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/gas_tank.json new file mode 100644 index 0000000..6132ec5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/gas_tank.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/gas_tank" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/oxygen_generator.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/oxygen_generator.json new file mode 100644 index 0000000..799a57e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/oxygen_generator.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/oxygen_generator" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_fuel.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_fuel.json new file mode 100644 index 0000000..cb44f4b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_fuel.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/rocket_fuel" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json new file mode 100644 index 0000000..cae7fc5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/solar_panel" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/gas_tank.json b/multiloader/common/src/main/resources/assets/nerospace/items/gas_tank.json new file mode 100644 index 0000000..cf0093a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/gas_tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/gas_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_generator.json b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_generator.json new file mode 100644 index 0000000..0af7005 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/oxygen_generator.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/oxygen_generator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_bucket.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_bucket.json new file mode 100644 index 0000000..c6050d7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_fuel_bucket.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/rocket_fuel_bucket" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel.json b/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel.json new file mode 100644 index 0000000..66e0644 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/solar_panel" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index a811022..443c1af 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -48,6 +48,14 @@ "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", "item.nerospace.alien_core": "Alien Core", "item.nerospace.rocket_fuel_canister": "Rocket Fuel Canister", + "item.nerospace.rocket_fuel_bucket": "Rocket Fuel Bucket", + "block.nerospace.rocket_fuel": "Rocket Fuel", + "fluid_type.nerospace.rocket_fuel": "Rocket Fuel", + "block.nerospace.gas_tank": "Gas Tank", + "block.nerospace.oxygen_generator": "Oxygen Generator", + "block.nerospace.solar_panel": "Solar Panel", + "gas.nerospace.empty": "Empty", + "gas.nerospace.oxygen": "Oxygen", "item.nerospace.frame_casing": "Frame Casing", "item.nerospace.grav_striders": "Grav Striders", "item.nerospace.drift_fleece": "Drift Fleece", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/gas_tank.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/gas_tank.json new file mode 100644 index 0000000..9812708 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/gas_tank.json @@ -0,0 +1,425 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#core" + }, + "east": { + "texture": "#core" + }, + "north": { + "texture": "#core" + }, + "south": { + "texture": "#core" + }, + "up": { + "texture": "#core" + }, + "west": { + "texture": "#core" + } + }, + "from": [ + 2, + 2, + 2 + ], + "to": [ + 14, + 14, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 2, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 14 + ], + "to": [ + 2, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 0 + ], + "to": [ + 14, + 2, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 14 + ], + "to": [ + 14, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 2 + ], + "to": [ + 2, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 2 + ], + "to": [ + 16, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 0 + ], + "to": [ + 14, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 14 + ], + "to": [ + 14, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "core": "nerospace:block/gas_tank_core", + "particle": "nerospace:block/gas_tank", + "side": "nerospace:block/gas_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/oxygen_generator.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/oxygen_generator.json new file mode 100644 index 0000000..8db8b86 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/oxygen_generator.json @@ -0,0 +1,105 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 11, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 3, + 11, + 3 + ], + "to": [ + 13, + 14, + 13 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 5, + 14, + 5 + ], + "to": [ + 11, + 16, + 11 + ] + } + ], + "textures": { + "particle": "nerospace:block/oxygen_generator", + "side": "nerospace:block/oxygen_generator", + "top": "nerospace:block/oxygen_generator_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_fuel.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_fuel.json new file mode 100644 index 0000000..f17a129 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_fuel.json @@ -0,0 +1,5 @@ +{ + "textures": { + "particle": "nerospace:block/rocket_fuel" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel.json new file mode 100644 index 0000000..9f88513 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cube_bottom_top", + "textures": { + "top": "nerospace:block/solar_panel", + "bottom": "nerospace:block/solar_panel_base", + "side": "nerospace:block/solar_panel_base" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_bucket.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_bucket.json new file mode 100644 index 0000000..2c657a0 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_fuel_bucket.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/rocket_fuel_bucket" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank.png new file mode 100644 index 0000000000000000000000000000000000000000..0c76d174c7f4c987a1995c81ae79a2ce47b01023 GIT binary patch literal 315 zcmV-B0mS}^P) zP#W1q1a0k6OfZ5|!BY=%A}eTX$LaGe3P?u6u)x8gigN8xRV-T_254^hkq3Jqv)|k+ zRmBbW|4E#GV+PCl5v-01W}l3Cz_=F?H2l2hePF|v^u)RZk?$*h`uj08B?9dUo>S=Kvn%0=dEZa}HN_iu zHnoP|6vWC@2b#TX67LlMGw)SyX&hR`vT(7}k%F+-UyEu~{ zvMdFlskTw-4FHljBFj>QP~;sU6c%4TQ7eQer)OkYN>gob6>A+sn)kgC`=VYZ*cWvs zR{>46&DWPFq~{*9<>xYI4YcC;>NUap3zo^EG${VI$<`cO2A=<&?qFEydh5H`9g_pg z;AYaO8l>x$yy!iDMt3m8v5bnrm?5{mOXBoo{9oaaZu8B3MQkrGXHNtWiuMe0=`BvaF!CG?F+1AoN{4pTABLN2o2WG)Uqo@3o}@z;yL3-@|l9 z=(`f9tN=LyTQ>=Pm+^Eag^$GrooGHQ6F~mDmWM~!lN{NYD#p-vF`7>~vJ7q}dBTs= z54l0{(he+xx91)m@r2NKD+ac1lEe{#)2lq|8stb0UmO!SJ%mtDTN<`*()zKBt((}o zNxEK1{(W!s=MF}1`f{^L9FeY9l6667nJmb%l*v^f&2}!Y?;gBUXFIQdn4cn4#$%j5 R8595j002ovPDHLkV1kVy?;8LB literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator_top.png new file mode 100644 index 0000000000000000000000000000000000000000..c64dbf37506e33317e3b73191b6fff06d9e28e21 GIT binary patch literal 444 zcmV;t0Ym8lO2`n3NQO=h{Q&&{;^fdzkjc?m9K>0iI(2Yx>L5=20L96{K^%${ zDWO9MK|*+BvpSS}d5JoC)4hB5|J}XIW#{(c<%@sV9>Cq?hDsL%VMJNv0Cc=AWs#Gm z2}*Tvm1jhiE^w8{;`Iqf0$P_W7tdsALZu4>az+ze<&mWc09l&gDvyrWtvhg)ht?&2 z&;uYJUQk(W=EDmdAP1PnWNBi6jwC77G0CTzXsrZ_1OT( zS?)K}xCY2X+a{tm%r-LvtxFS>Ds8bHmzg=RU0XlsnYJWZ1hx;*onI1!Q4Nrt_k$h> zFXKjwwh+Lbosp%9S@k{Geg&CBS>$Y{F;RcOa`B8any{J1_(9M7L_rvlu9r0+S-t%j m&eP*;>x_>9_{h^AIRM|4$+2b7oeXpU0000KHD4-20#8&L{z8P4+R;Wre8{GrJV3=b|b@Vz~T(-l01CJdk69%10*W@cb~ zS|#xx-Cv}H1q+`r8_~|i=0%oQkG_!ypk;V zA6Fa`6BaDwL>;mocp#FRfM9xa Wb;pMP8|FFy0000Y5S`r&cM35Z$r4bJfFSsLEK;Np@vDfMLd41Nj2AJo=T0y6{L<-xiop{1h8!99e>k$3 z&tMEP?i)GklG8x+p~O94+y};es5I6g5MgxgiVyM$ zBej8446zxea(xvZPA*`)h(N?{Rq7l~7;zt_0urD|pnx>8$LmihAW1b97CXIncLDkgsxP+iwnSgEP@#j&V0o6r>@kp5{+X88{!Orx8mMS@~0tQd&UT1*)C zzeP0-w1vHv7HA@$+Vbx3EOP^?Gr8ZYNE1h0a`#~xHSeP|!kGo<9}68d@5;*c)kKQ( zX92q+v=$S_J)oxAr)zieaWpG!_oH8tG;|S@U)j=VR?y;$D0x4akt*ZDUJKr?E;9*h z{#zQg*$DT+< literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_still.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_still.png new file mode 100644 index 0000000000000000000000000000000000000000..f1a6d5b7e1800f4666096c14f5ae1223f47bbdba GIT binary patch literal 574 zcmV-E0>S->P)D08%r zn|4JK9amShnm(PJ^YT>WVz6^ zk8((r2gbinnPU$WoaXtVr8O;Y_`C+_Zhb(gRL-gdv9WSTfP&lMUm7#f`^kvV z#OHyrl__)o0Z_0?w9(3jw1J{{R6QO}6)~%UN(&XZJ-svoN=IL3tmAnVCt@#{R>B%6 zSmych%}p(DppUaLVLBV+yq@2e$)*z4K*74RfsC2xX>U*@@70bLv7Jq^)_tJhEYYTR zx0FK|de|;19~PCBM=KlVK|$@E6|r!;GzV43H$I1w=u z)h@<`>1>eqi2Yxl?gItuh&FS$D-{}v=VNhudTC_kyhrSS(h)QL092cxrU=$CO#lD@ M07*qoM6N<$f`B6nS^xk5 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel.png new file mode 100644 index 0000000000000000000000000000000000000000..33fc25c6266680d4be66aeffe5dbf81452b51f8f GIT binary patch literal 262 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`XFXjULn`JZ|4DrKVSXdifg=YF z97sr*cv#{1qJs)0kL8-LnlQ7i&Sc|pGJm(QJTWPu^f99ck;PqhOtX_a}42+}<_wtl|{ZwoC-F+Bol4 z^6=bnmJUx^T<4^8?54y9<|Q(jPY2cypk zHQ~iSfqad1iH^@|etzm)xMCUq`p@aU83%G1`1$3X+}rLn`JZCrC_5xV1&}|8n2x zkMiR47o-I}nQ@{>NhrbP*`A7zts3cG9v=>T-MGNJYrS!!fee#`1Ygd9gx&cUoCEt4 z50tdAu`N!vd756v8$n%X}e?biPvI91_58}Dn0vbRyKk8-=1 zC;npd@c3ZB#`-fs GAS = + BlockApiLookup.get( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "gas"), + NerospaceGasStorage.class, Direction.class); + @Override public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); @@ -89,6 +96,24 @@ public void onInitialize() { ENERGY.registerForBlockEntity( (be, direction) -> be.getEnergy(), ModBlockEntities.UNIVERSAL_PIPE.get()); + GAS.registerForBlockEntity( + (be, direction) -> be.getGas(), + ModBlockEntities.UNIVERSAL_PIPE.get()); + + GAS.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.GAS_TANK.get()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.OXYGEN_GENERATOR.get()); + GAS.registerForBlockEntity( + (be, direction) -> be.getGas(), + ModBlockEntities.OXYGEN_GENERATOR.get()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.SOLAR_PANEL.get()); ItemStorage.SIDED.registerForBlockEntity( (be, direction) -> ContainerStorage.of(be, direction), diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelFluid.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelFluid.java new file mode 100644 index 0000000..7b950a5 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fluid/RocketFuelFluid.java @@ -0,0 +1,119 @@ +package za.co.neroland.nerospace.fluid; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.material.FlowingFluid; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; + +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Fabric implementation of {@code rocket_fuel} as a vanilla {@link FlowingFluid} subclass (NeoForge + * uses {@code BaseFlowingFluid} instead). The override set mirrors vanilla {@code WaterFluid}; the + * fluid/bucket/block references resolve lazily through the common {@code ModFluids}/{@code ModItems}/ + * {@code ModBlocks} holders, so this works regardless of registration order. NeoForge's per-fluid + * {@code getFluidType()} requirement does not exist here, which is exactly why this class is + * Fabric-only and common never sees it. + */ +public abstract class RocketFuelFluid extends FlowingFluid { + + @Override + public Fluid getFlowing() { + return ModFluids.ROCKET_FUEL_FLOWING.get(); + } + + @Override + public Fluid getSource() { + return ModFluids.ROCKET_FUEL.get(); + } + + @Override + public Item getBucket() { + return ModItems.ROCKET_FUEL_BUCKET.get(); + } + + @Override + public boolean isSame(Fluid fluid) { + return fluid == ModFluids.ROCKET_FUEL.get() || fluid == ModFluids.ROCKET_FUEL_FLOWING.get(); + } + + @Override + protected boolean canConvertToSource(ServerLevel level) { + return false; + } + + @Override + protected void beforeDestroyingBlock(LevelAccessor level, BlockPos pos, BlockState state) { + } + + @Override + protected int getSlopeFindDistance(LevelReader level) { + return 2; + } + + @Override + protected int getDropOff(LevelReader level) { + return 2; + } + + @Override + public int getTickDelay(LevelReader level) { + return 20; + } + + @Override + protected float getExplosionResistance() { + return 100.0F; + } + + @Override + public boolean canBeReplacedWith(FluidState state, BlockGetter level, BlockPos pos, Fluid fluid, Direction direction) { + return direction == Direction.DOWN && !isSame(fluid); + } + + @Override + protected BlockState createLegacyBlock(FluidState state) { + return ModBlocks.ROCKET_FUEL_BLOCK.get().defaultBlockState() + .setValue(LiquidBlock.LEVEL, getLegacyLevel(state)); + } + + public static final class Source extends RocketFuelFluid { + @Override + public int getAmount(FluidState state) { + return 8; + } + + @Override + public boolean isSource(FluidState state) { + return true; + } + } + + public static final class Flowing extends RocketFuelFluid { + @Override + protected void createFluidStateDefinition(StateDefinition.Builder builder) { + super.createFluidStateDefinition(builder); + builder.add(LEVEL); + } + + @Override + public int getAmount(FluidState state) { + return state.getValue(LEVEL); + } + + @Override + public boolean isSource(FluidState state) { + return false; + } + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidFactory.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidFactory.java new file mode 100644 index 0000000..08fd280 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidFactory.java @@ -0,0 +1,19 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.world.level.material.Fluid; + +import za.co.neroland.nerospace.fluid.RocketFuelFluid; + +/** Fabric {@link FluidFactory}: hand-written vanilla {@link RocketFuelFluid} still + flowing. */ +public final class FabricFluidFactory implements FluidFactory { + + @Override + public Fluid createSource() { + return new RocketFuelFluid.Source(); + } + + @Override + public Fluid createFlowing() { + return new RocketFuelFluid.Flowing(); + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricGasLookup.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricGasLookup.java new file mode 100644 index 0000000..56674a6 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricGasLookup.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.fabric.NerospaceFabric; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; + +/** Fabric query of the mod's gas block-api lookup. */ +public final class FabricGasLookup implements GasLookup { + + @Nullable + @Override + public NerospaceGasStorage find(Level level, BlockPos pos, @Nullable Direction side) { + return NerospaceFabric.GAS.find(level, pos, side); + } +} diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory new file mode 100644 index 0000000..7519886 --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.FabricFluidFactory diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup new file mode 100644 index 0000000..8c78335 --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.FabricGasLookup diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 90efb7a..e1b6720 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -12,6 +12,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; /** @@ -37,6 +38,12 @@ public final class NeoForgeCapabilities { Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "fluid"), NerospaceFluidStorage.class); + /** Mod-owned gas capability; mirrors the Fabric {@code BlockApiLookup} of the same id. */ + public static final BlockCapability GAS = + BlockCapability.createSided( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "gas"), + NerospaceGasStorage.class); + private NeoForgeCapabilities() { } @@ -99,6 +106,29 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.UNIVERSAL_PIPE.get(), (be, side) -> be.getEnergy()); + event.registerBlockEntity( + GAS, + ModBlockEntities.UNIVERSAL_PIPE.get(), + (be, side) -> be.getGas()); + + event.registerBlockEntity( + GAS, + ModBlockEntities.GAS_TANK.get(), + (be, side) -> be.getTank()); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.OXYGEN_GENERATOR.get(), + (be, side) -> be.getEnergy()); + event.registerBlockEntity( + GAS, + ModBlockEntities.OXYGEN_GENERATOR.get(), + (be, side) -> be.getGas()); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.SOLAR_PANEL.get(), + (be, side) -> be.getEnergy()); event.registerBlockEntity( Capabilities.Item.BLOCK, diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 91f9410..a8fb27f 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -1,14 +1,21 @@ package za.co.neroland.nerospace.neoforge; +import net.minecraft.client.renderer.block.FluidModel; +import net.minecraft.client.resources.model.sprite.Material; +import net.minecraft.resources.Identifier; import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.RegisterFluidModelsEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; +import net.neoforged.neoforge.client.fluid.FluidTintSources; +import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.fluid.ModFluids; import za.co.neroland.nerospace.registry.ModMenuTypes; -/** NeoForge client-only wiring (screen registration). Loaded only behind a Dist.CLIENT guard. */ +/** NeoForge client-only wiring (screen + fluid-model registration). Loaded only behind Dist.CLIENT. */ public final class NeoForgeClientSetup { private NeoForgeClientSetup() { @@ -16,6 +23,7 @@ private NeoForgeClientSetup() { public static void init(IEventBus modEventBus) { modEventBus.addListener(NeoForgeClientSetup::onRegisterScreens); + modEventBus.addListener(NeoForgeClientSetup::onRegisterFluidModels); } private static void onRegisterScreens(RegisterMenuScreensEvent event) { @@ -23,4 +31,13 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); event.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); } + + /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ + private static void onRegisterFluidModels(RegisterFluidModelsEvent event) { + Material still = new Material(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "block/rocket_fuel_still")); + Material flow = new Material(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "block/rocket_fuel_flow")); + event.register( + new FluidModel.Unbaked(still, flow, still, FluidTintSources.constant(0xFFFFFFFF)), + ModFluids.ROCKET_FUEL, ModFluids.ROCKET_FUEL_FLOWING); + } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 72a692c..9516e24 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -11,6 +11,7 @@ import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; import za.co.neroland.nerospace.registry.ModItems; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; @@ -25,6 +26,7 @@ public final class NerospaceNeoForge { public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NerospaceCommon.LOGGER.info("[Nerospace] NeoForge bootstrap"); NerospaceCommon.init(); + NeoForgeFluidFactory.registerFluidTypes(modEventBus); NeoForgeRegistrationFactory.registerAll(modEventBus); NeoForgeCapabilities.register(modEventBus); if (FMLEnvironment.getDist() == Dist.CLIENT) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidFactory.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidFactory.java new file mode 100644 index 0000000..49b6af7 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidFactory.java @@ -0,0 +1,56 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.world.level.material.Fluid; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.fluids.BaseFlowingFluid; +import net.neoforged.neoforge.fluids.FluidType; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * NeoForge {@link FluidFactory}: the rocket-fuel fluid as a {@link BaseFlowingFluid} backed by a + * registered {@link FluidType} (NeoForge's per-fluid metadata carrier). The {@code FluidType} + * DeferredRegister is attached to the mod bus from the loader entry point via + * {@link #registerFluidTypes(IEventBus)}. The {@link BaseFlowingFluid.Properties} reference the + * common fluid/bucket/block holders (all lazily-resolved {@code Supplier}s), so registration order + * across the separate registries is not a concern. + */ +public final class NeoForgeFluidFactory implements FluidFactory { + + private static final DeferredRegister FLUID_TYPES = + DeferredRegister.create(NeoForgeRegistries.Keys.FLUID_TYPES, NerospaceCommon.MOD_ID); + + public static final DeferredHolder ROCKET_FUEL_TYPE = FLUID_TYPES.register( + "rocket_fuel", () -> new FluidType(FluidType.Properties.create() + .density(1200) + .viscosity(1500) + .canConvertToSource(false))); + + private static final BaseFlowingFluid.Properties PROPERTIES = new BaseFlowingFluid.Properties( + ROCKET_FUEL_TYPE, ModFluids.ROCKET_FUEL, ModFluids.ROCKET_FUEL_FLOWING) + .bucket(ModItems.ROCKET_FUEL_BUCKET) + .block(ModBlocks.ROCKET_FUEL_BLOCK) + .slopeFindDistance(2) + .levelDecreasePerBlock(2); + + /** Attach the FluidType DeferredRegister to the mod event bus (call from the entry point). */ + public static void registerFluidTypes(IEventBus modEventBus) { + FLUID_TYPES.register(modEventBus); + } + + @Override + public Fluid createSource() { + return new BaseFlowingFluid.Source(PROPERTIES); + } + + @Override + public Fluid createFlowing() { + return new BaseFlowingFluid.Flowing(PROPERTIES); + } +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeGasLookup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeGasLookup.java new file mode 100644 index 0000000..e63590a --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeGasLookup.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.neoforge.NeoForgeCapabilities; + +/** NeoForge query of the mod's gas capability. */ +public final class NeoForgeGasLookup implements GasLookup { + + @Nullable + @Override + public NerospaceGasStorage find(Level level, BlockPos pos, @Nullable Direction side) { + return level.getCapability(NeoForgeCapabilities.GAS, pos, side); + } +} diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory new file mode 100644 index 0000000..c5c33ea --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidFactory @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.NeoForgeFluidFactory diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup new file mode 100644 index 0000000..3c4b6e0 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.GasLookup @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.NeoForgeGasLookup From 0807fabefe92955ea06b8d9c3c82ca4e0080ada7 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:15:07 +0200 Subject: [PATCH 29/82] Add item relay to Universal Pipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce item transport to the Universal Pipe: the pipe now implements WorldlyContainer with a 3-slot buffer, item serialization, and per-tick relay logic that pulls from non-pipe neighbours and pushes to any neighbour (vanilla Container adjacency, so it interoperates with vanilla chests/furnaces and mod machines). Added helper methods for slot/face handling and single-item transfer/insert. Register item storage capabilities on Fabric (ItemStorage.SIDED → ContainerStorage.of) and NeoForge (Capabilities.Item.BLOCK → WorldlyContainerWrapper/VanillaContainerWrapper). Also tweak SolarPanel illumination logic to use getSkyDarken() + open-sky check, and update docs to mention the new item relay and solar panel port. --- docs/MULTILOADER_MIGRATION.md | 17 +- .../machine/SolarPanelBlockEntity.java | 6 +- .../pipe/UniversalPipeBlockEntity.java | 186 +++++++++++++++++- .../nerospace/fabric/NerospaceFabric.java | 3 + .../neoforge/NeoForgeCapabilities.java | 6 + 5 files changed, 202 insertions(+), 16 deletions(-) diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index 954f499..fee7b59 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -39,13 +39,24 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > end-to-end: generator → pipe → oxygen generator → pipe → gas tank. (The world oxygen-field effect + > HUD + the generator GUI are a deferred atmosphere subsystem.) > +> **2026-06-20 (later still): item relay added to the universal pipe** — all 4 cells green. The pipe is +> now a `WorldlyContainer` (3-slot buffer) and moves items by plain vanilla `Container` adjacency (no new +> seam — works with vanilla chests/furnaces and the mod's machines on both loaders): it pulls from +> non-pipe neighbours and pushes to any neighbour, so the single pipe carries **energy + gas + items**. +> Item cap exposed on both loaders (NeoForge `Capabilities.Item.BLOCK`, Fabric `ItemStorage.SIDED`). +> +> **2026-06-20 (later still): solar_panel ported** — all 4 cells green. Single-tier GUI-less daylight +> generator (`getSkyDarken()` + open-sky check → energy; halved in storms), exposes the energy +> capability (extract-only) so the pipe network drains it. Root's tiered sun-tracking array + BER are +> a deferred enhancement. +> > Remaining, by subsystem (rough size): **dimensions** (Greenxertz/Cindara/Glacira biomes+dims+travel; > unblocks the planet ores' worldgen); **entities** (alien villager, xertz stalker + attributes + > renderers); **rockets** (items, tiers, launch logic); **quarry** (area mining); **structures** > (station/village/meteor cores + events); **atmosphere/terraforming** (oxygen field, terraformer, -> monitor, hydration); **solar panel** (tiers + multiblock + BER); **star guide** (progression UI); -> **item-pipe** (item query seam in the universal pipe); **creative item/fluid/gas stores** -> (infinite-resource config — marginal). Recommended order: item-pipe → entities → dimensions → rockets → the rest. +> monitor, hydration); **solar panel tiers/array/BER** (single-tier base is done); **star guide** +> (progression UI); **creative item/fluid/gas stores** (infinite-resource config — marginal). +> Recommended order: entities → dimensions → rockets → quarry → structures → atmosphere → the rest. --- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java index 947eadd..100c88d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java @@ -37,9 +37,9 @@ public void tick(Level level, BlockPos pos, BlockState state) { if (level.isClientSide()) { return; } - long dayTime = level.getDayTime() % 24000L; - boolean day = dayTime < 12300L || dayTime > 23850L; - if (!day || !level.canSeeSky(pos.above())) { + // getSkyDarken() is 0 in full daylight and ramps toward ~11 at night (and in storms); a low + // value plus an open sky above means the panel is illuminated. + if (level.getSkyDarken() >= 4 || !level.canSeeSky(pos.above())) { return; } int rate = FE_PER_TICK; diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java index 09543c9..55e33ae 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -2,12 +2,19 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; +import org.jetbrains.annotations.Nullable; + import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.gas.GasResource; @@ -18,21 +25,26 @@ import za.co.neroland.nerospace.registry.ModBlockEntities; /** - * Universal Pipe — relays energy AND gas between adjacent storages. Each tick it pulls from - * neighbours that allow extraction (generators, other pipes) into its small buffers, then pushes into - * neighbours that allow insertion (machines, tanks, batteries). Direction is enforced naturally by - * each storage's own insert/extract limits. Uses {@link EnergyLookup} / {@link GasLookup} — the query - * sides of the cross-loader seams. The gas buffer holds one gas type at a time (only oxygen exists yet). + * Universal Pipe — relays energy, gas AND items between adjacent storages. Energy/gas use the + * cross-loader {@link EnergyLookup}/{@link GasLookup} seams; items use plain vanilla {@link Container} + * adjacency (so it interoperates with vanilla chests/furnaces and the mod's machines on both loaders + * with no extra seam). The pipe is itself a {@link WorldlyContainer} (small buffer), so it is exposed + * as the item capability and chains pipe-to-pipe. Item flow is directed: pull only from non-pipe + * containers, push to any neighbour — sources feed the line, the line feeds sinks. */ -public class UniversalPipeBlockEntity extends BlockEntity { +public class UniversalPipeBlockEntity extends BlockEntity implements WorldlyContainer { public static final int CAPACITY = 8_000; public static final int MAX_IO = 1_000; public static final int GAS_CAPACITY = 8_000; public static final int GAS_MAX_IO = 1_000; + public static final int ITEM_SLOTS = 3; + + private static final int[] ALL_SLOTS = {0, 1, 2}; private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_IO, MAX_IO, this::setChanged); private final GasTank gas = new GasTank(GAS_CAPACITY, this::setChanged); + private final NonNullList items = NonNullList.withSize(ITEM_SLOTS, ItemStack.EMPTY); public UniversalPipeBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.UNIVERSAL_PIPE.get(), pos, state); @@ -52,10 +64,10 @@ public void tick(Level level, BlockPos pos, BlockState state) { } relayEnergy(level, pos); relayGas(level, pos); + relayItems(level, pos); } private void relayEnergy(Level level, BlockPos pos) { - // Pull from extractable neighbours into the buffer. for (Direction dir : Direction.values()) { NerospaceEnergyStorage neighbour = EnergyLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); if (neighbour == null) { @@ -69,7 +81,6 @@ private void relayEnergy(Level level, BlockPos pos) { } } } - // Push from the buffer into insertable neighbours. for (Direction dir : Direction.values()) { if (this.energy.getAmount() <= 0) { break; @@ -87,7 +98,6 @@ private void relayEnergy(Level level, BlockPos pos) { } private void relayGas(Level level, BlockPos pos) { - // Pull a (single) gas type from extractable neighbours into the buffer. for (Direction dir : Direction.values()) { long room = this.gas.getCapacity() - this.gas.getAmount(); if (room <= 0) { @@ -107,7 +117,6 @@ private void relayGas(Level level, BlockPos pos) { neighbour.drain(moved, false); } } - // Push the buffered gas into insertable neighbours. for (Direction dir : Direction.values()) { if (this.gas.getAmount() <= 0) { break; @@ -125,12 +134,93 @@ private void relayGas(Level level, BlockPos pos) { } } + private void relayItems(Level level, BlockPos pos) { + // Pull one item per tick from each non-pipe neighbour container into the buffer. + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(pos.relative(dir)); + if (be instanceof Container src && !(be instanceof UniversalPipeBlockEntity)) { + moveOne(src, dir.getOpposite(), this, dir); + } + } + // Push one item per tick from the buffer into each neighbour container (incl. other pipes). + for (Direction dir : Direction.values()) { + BlockEntity be = level.getBlockEntity(pos.relative(dir)); + if (be instanceof Container dst) { + moveOne(this, dir, dst, dir.getOpposite()); + } + } + } + + private static int[] slotsFor(Container c, Direction face) { + if (c instanceof WorldlyContainer wc) { + return wc.getSlotsForFace(face); + } + int[] all = new int[c.getContainerSize()]; + for (int i = 0; i < all.length; i++) { + all[i] = i; + } + return all; + } + + private static boolean placeable(Container into, int slot, ItemStack stack, Direction face) { + if (!into.canPlaceItem(slot, stack)) { + return false; + } + return !(into instanceof WorldlyContainer wc) || wc.canPlaceItemThroughFace(slot, stack, face); + } + + /** Move a single item from {@code from} (extracted through {@code fromFace}) into {@code into}. */ + private static boolean moveOne(Container from, Direction fromFace, Container into, Direction intoFace) { + for (int fs : slotsFor(from, fromFace)) { + ItemStack stack = from.getItem(fs); + if (stack.isEmpty()) { + continue; + } + if (from instanceof WorldlyContainer wc && !wc.canTakeItemThroughFace(fs, stack, fromFace)) { + continue; + } + ItemStack one = stack.copyWithCount(1); + if (insertOne(into, one, intoFace)) { + from.removeItem(fs, 1); + from.setChanged(); + return true; + } + } + return false; + } + + private static boolean insertOne(Container into, ItemStack one, Direction face) { + for (int s : slotsFor(into, face)) { + ItemStack ex = into.getItem(s); + int max = Math.min(into.getMaxStackSize(), one.getMaxStackSize()); + if (!ex.isEmpty() && ex.getCount() < max + && ItemStack.isSameItemSameComponents(ex, one) && placeable(into, s, one, face)) { + ex.grow(1); + into.setChanged(); + return true; + } + } + for (int s : slotsFor(into, face)) { + if (into.getItem(s).isEmpty() && placeable(into, s, one, face)) { + into.setItem(s, one); + into.setChanged(); + return true; + } + } + return false; + } + @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); output.putInt("Energy", this.energy.getRaw()); output.putString("Gas", this.gas.getRawGas().getSerializedName()); output.putInt("GasAmount", this.gas.getRawAmount()); + for (int i = 0; i < ITEM_SLOTS; i++) { + if (!this.items.get(i).isEmpty()) { + output.store("Item" + i, ItemStack.OPTIONAL_CODEC, this.items.get(i)); + } + } } @Override @@ -138,5 +228,81 @@ protected void loadAdditional(ValueInput input) { super.loadAdditional(input); this.energy.setRaw(input.getIntOr("Energy", 0)); this.gas.setRaw(GasResource.byName(input.getStringOr("Gas", "empty")), input.getIntOr("GasAmount", 0)); + for (int i = 0; i < ITEM_SLOTS; i++) { + this.items.set(i, input.read("Item" + i, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + } + + // --- WorldlyContainer (item buffer) ------------------------------------- + + @Override + public int[] getSlotsForFace(Direction side) { + return ALL_SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return true; + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return true; + } + + @Override + public int getContainerSize() { + return ITEM_SLOTS; + } + + @Override + public boolean isEmpty() { + for (ItemStack stack : this.items) { + if (!stack.isEmpty()) { + return false; + } + } + return true; + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack stack = this.items.get(slot); + if (stack.isEmpty() || amount <= 0) { + return ItemStack.EMPTY; + } + ItemStack split = stack.split(amount); + if (!split.isEmpty()) { + this.setChanged(); + } + return split; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + ItemStack stack = this.items.get(slot); + this.items.set(slot, ItemStack.EMPTY); + return stack; + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + this.items.clear(); } } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 4b72089..53a20a9 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -99,6 +99,9 @@ public void onInitialize() { GAS.registerForBlockEntity( (be, direction) -> be.getGas(), ModBlockEntities.UNIVERSAL_PIPE.get()); + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.UNIVERSAL_PIPE.get()); GAS.registerForBlockEntity( (be, direction) -> be.getTank(), diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index e1b6720..e0a13d4 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -110,6 +110,12 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { GAS, ModBlockEntities.UNIVERSAL_PIPE.get(), (be, side) -> be.getGas()); + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.UNIVERSAL_PIPE.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); event.registerBlockEntity( GAS, From 6722554e0cd02523ec58f16dd6b5ecd5fd602e06 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:27:31 +0200 Subject: [PATCH 30/82] Add entities, models, renderers, and sounds Introduce three new mobs (Xertz Stalker, Quartz Crawler, Greenling) with server-side classes, AI goals and attribute builders; register cross-loader EntityType entries via ModEntities and expose attribute registration via ModEntityAttributes. Add client-side model classes, a shared GreenxertzMobModel base, renderers (GreenxertzCreatureRenderer, ClientEntityRenderers) and an emissive GlowEyesLayer, plus entity textures and glow textures. Add sound event registrations (ModSounds) and a sounds.json resource aliasing vanilla events; update ModRegistries to initialize sounds and entities. Also add a temporary build.gradle 'probeEntity' task to inspect EntityType.Builder signatures. --- docs/MULTILOADER_MIGRATION.md | 13 ++ multiloader/common/build.gradle | 12 ++ .../client/ClientEntityRenderers.java | 47 ++++++ .../nerospace/client/GlowEyesLayer.java | 30 ++++ .../nerospace/client/GreenlingModel.java | 88 +++++++++++ .../client/GreenxertzCreatureRenderer.java | 60 ++++++++ .../nerospace/client/GreenxertzMobModel.java | 145 ++++++++++++++++++ .../nerospace/client/QuartzCrawlerModel.java | 82 ++++++++++ .../nerospace/client/XertzStalkerModel.java | 111 ++++++++++++++ .../neroland/nerospace/entity/Greenling.java | 60 ++++++++ .../nerospace/entity/QuartzCrawler.java | 64 ++++++++ .../nerospace/entity/XertzStalker.java | 65 ++++++++ .../nerospace/registry/ModEntities.java | 46 ++++++ .../registry/ModEntityAttributes.java | 32 ++++ .../nerospace/registry/ModRegistries.java | 2 + .../nerospace/registry/ModSounds.java | 42 +++++ .../assets/nerospace/lang/en_us.json | 3 + .../resources/assets/nerospace/sounds.json | 110 +++++++++++++ .../nerospace/textures/entity/greenling.png | Bin 0 -> 2213 bytes .../textures/entity/greenling_glow.png | Bin 0 -> 204 bytes .../textures/entity/quartz_crawler.png | Bin 0 -> 1923 bytes .../textures/entity/quartz_crawler_glow.png | Bin 0 -> 370 bytes .../textures/entity/xertz_stalker.png | Bin 0 -> 2112 bytes .../textures/entity/xertz_stalker_glow.png | Bin 0 -> 428 bytes .../nerospace/fabric/NerospaceFabric.java | 5 + .../fabric/NerospaceFabricClient.java | 14 +- .../neoforge/NeoForgeClientSetup.java | 15 ++ .../nerospace/neoforge/NerospaceNeoForge.java | 7 + 28 files changed, 1052 insertions(+), 1 deletion(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/GlowEyesLayer.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenlingModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzCreatureRenderer.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzMobModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuartzCrawlerModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/XertzStalkerModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/Greenling.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/QuartzCrawler.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/XertzStalker.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/sounds.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker_glow.png diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index fee7b59..d471b36 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -39,6 +39,19 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > end-to-end: generator → pipe → oxygen generator → pipe → gas tank. (The world oxygen-field effect + > HUD + the generator GUI are a deferred atmosphere subsystem.) > +> **2026-06-20 (later still): ENTITY seam + Greenxertz creatures ported** — all 4 cells green. New +> cross-loader seam: entity types via `RegistrationProvider` over `ENTITY_TYPE` (`EntityType.Builder… +> build(key)`); **attributes** via `ModEntityAttributes` applied per loader (NeoForge +> `EntityAttributeCreationEvent`, Fabric `FabricDefaultAttributeRegistry`); **renderers** via a common +> `ClientEntityRenderers` sink (NeoForge `RegisterRenderers`, Fabric `EntityRendererRegistry`). Models +> are baked directly (`createBodyLayer().bakeRoot()`), so **no model-layer registry** is needed on +> either loader (Fabric's `EntityModelLayerRegistry` isn't on the de-obf classpath). Ported: `xertz_stalker`, +> `quartz_crawler`, `greenling` (full vanilla AI) + their shared `GreenxertzMobModel`/renderer/glow-eyes +> layer, the three distinct geometry models, `ModSounds` (vanilla-aliased via `sounds.json`), and entity +> textures. Natural-spawn placement + spawn eggs are deferred (mobs are summonable; spawning waits on +> the planet dimensions). The remaining mobs (alien villager, ruin warden, cinder/frost striders, +> terraform livestock) follow this same seam. +> > **2026-06-20 (later still): item relay added to the universal pipe** — all 4 cells green. The pipe is > now a `WorldlyContainer` (3-slot buffer) and moves items by plain vanilla `Container` adjacency (no new > seam — works with vanilla chests/furnaces and the mod's machines on both loaders): it pulls from diff --git a/multiloader/common/build.gradle b/multiloader/common/build.gradle index 46c4075..be8c0bd 100644 --- a/multiloader/common/build.gradle +++ b/multiloader/common/build.gradle @@ -18,3 +18,15 @@ neoForge { accessTransformers.from(at.absolutePath) } } + +// TEMP probe (remove after): EntityType.Builder.build signature in 26.x. +def probeCp = configurations.compileClasspath +tasks.register('probeEntity') { + doLast { + def cp = probeCp.asPath + def p = ['javap', '-p', '-classpath', cp, 'net.minecraft.world.entity.EntityType$Builder'].execute() + def out = new StringBuilder(); def err = new StringBuilder() + p.consumeProcessOutput(out, err); p.waitFor() + out.toString().readLines().findAll { it.contains('build') || it.contains(' of(') }.each { println it } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java new file mode 100644 index 0000000..209196b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java @@ -0,0 +1,47 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.ModEntities; + +/** + * Cross-loader entity-renderer wiring. The renderer set is identical on both loaders, so it lives + * here once and each loader passes its own registration function ({@link Sink}) — NeoForge's + * {@code RegisterRenderers} event, Fabric's {@code EntityRendererRegistry}. Models are baked directly + * via {@code createBodyLayer().bakeRoot()} so no model-layer registry is needed on either loader + * (Fabric's {@code EntityModelLayerRegistry} isn't on the de-obf classpath here). + */ +public final class ClientEntityRenderers { + + /** A loader's renderer-registration entry point. */ + public interface Sink { + void register(EntityType type, EntityRendererProvider provider); + } + + public static void registerAll(Sink sink) { + sink.register(ModEntities.XERTZ_STALKER.get(), context -> new GreenxertzCreatureRenderer(context, + new XertzStalkerModel(XertzStalkerModel.createBodyLayer().bakeRoot()), + tex("xertz_stalker"), 1.0F, 1.0F, 1.0F, 0.5F, glow("xertz_stalker"))); + sink.register(ModEntities.QUARTZ_CRAWLER.get(), context -> new GreenxertzCreatureRenderer(context, + new QuartzCrawlerModel(QuartzCrawlerModel.createBodyLayer().bakeRoot()), + tex("quartz_crawler"), 1.0F, 1.0F, 1.0F, 0.5F, glow("quartz_crawler"))); + sink.register(ModEntities.GREENLING.get(), context -> new GreenxertzCreatureRenderer(context, + new GreenlingModel(GreenlingModel.createBodyLayer().bakeRoot()), + tex("greenling"), 1.0F, 1.0F, 1.0F, 0.3F, glow("greenling"))); + } + + private static Identifier tex(String name) { + return Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/" + name + ".png"); + } + + private static Identifier glow(String name) { + return Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/" + name + "_glow.png"); + } + + private ClientEntityRenderers() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GlowEyesLayer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GlowEyesLayer.java new file mode 100644 index 0000000..81be4e6 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GlowEyesLayer.java @@ -0,0 +1,30 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.RenderLayerParent; +import net.minecraft.client.renderer.entity.layers.EyesLayer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.resources.Identifier; + +/** + * Emissive glow layer for the Greenxertz/Cindara creatures: re-renders the model with a full-bright + * {@code eyes} render type using a per-creature glow texture (transparent except the eyes / crystal / + * ember accents), so those pixels glow in the dark regardless of light level. + */ +public class GlowEyesLayer extends EyesLayer> { + + private final RenderType type; + + public GlowEyesLayer(RenderLayerParent> parent, + Identifier glowTexture) { + super(parent); + this.type = RenderTypes.eyes(glowTexture); + } + + @Override + public RenderType renderType() { + return this.type; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenlingModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenlingModel.java new file mode 100644 index 0000000..34f6458 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenlingModel.java @@ -0,0 +1,88 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Greenling — "Sprout" (Phase 10d). A small grounded biped: chubby body, oversized cheeky head, a + * leaf crest, little arms and two stubby legs. The legs toddle and the arms swing as it walks. + * Idle (10f): a curious head sway (cheeks track the head) and its signature — the three-frond leaf + * crest wiggles, each frond out of phase, like leaves in a light breeze. + */ +public class GreenlingModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "greenling"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public GreenlingModel(ModelPart root) { + super(root); + swingLimb("leg_left", 0F, 0.5F); + swingLimb("leg_right", Mth.PI, 0.5F); + swingLimb("arm_left", Mth.PI, 0.3F); + swingLimb("arm_right", 0F, 0.3F); + // Idle: curious head sway (the cheek band tracks the head)… + ambient("head", Direction.Axis.Y, 0.06F, 0F, 0.07F); + ambient("cheeks", Direction.Axis.Y, 0.06F, 0F, 0.07F); + // …and the signature leaf-crest wiggle: three fronds swaying out of phase. + ambient("frond_mid", Direction.Axis.Z, 0.14F, 0F, 0.09F); + ambient("frond_left", Direction.Axis.Z, 0.14F, 0.9F, 0.09F); + ambient("frond_right", Direction.Axis.Z, 0.14F, 1.8F, 0.09F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-3.5F, 15F, -3F, 7F, 6F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("belly", + CubeListBuilder.create().texOffs(0, 0).addBox(-3F, 19F, -2.5F, 6F, 3F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-4F, 7F, -4F, 8F, 8F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("cheeks", + CubeListBuilder.create().texOffs(0, 28).addBox(-4.5F, 10F, -3.5F, 9F, 3F, 7F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("frond_mid", + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, 1F, -0.5F, 1F, 6F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_left", + CubeListBuilder.create().texOffs(44, 0).addBox(-2.5F, 21F, -1.5F, 2.5F, 3F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_right", + CubeListBuilder.create().texOffs(44, 0).addBox(0F, 21F, -1.5F, 2.5F, 3F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (side fronds + arms are rotated — Java-authoritative) + root.addOrReplaceChild("frond_left", + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, -5F, -0.5F, 1F, 5F, 1F), + PartPose.offsetAndRotation(-1.5F, 7F, 0F, 0F, 0F, 0.5F)); + root.addOrReplaceChild("frond_right", + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, -5F, -0.5F, 1F, 5F, 1F), + PartPose.offsetAndRotation(1.5F, 7F, 0F, 0F, 0F, -0.5F)); + + // Hip/shoulder-pivoted limbs. + root.addOrReplaceChild("arm_left", + CubeListBuilder.create().texOffs(44, 0).addBox(-1.5F, 0F, -1F, 2F, 5F, 2F), + PartPose.offsetAndRotation(-3.5F, 15.5F, 0F, 0F, 0F, 0.15F)); + root.addOrReplaceChild("arm_right", + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, 0F, -1F, 2F, 5F, 2F), + PartPose.offsetAndRotation(3.5F, 15.5F, 0F, 0F, 0F, -0.15F)); + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzCreatureRenderer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzCreatureRenderer.java new file mode 100644 index 0000000..47216aa --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzCreatureRenderer.java @@ -0,0 +1,60 @@ +package za.co.neroland.nerospace.client; + +import com.mojang.blaze3d.vertex.PoseStack; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.MobRenderer; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Mob; + +/** + * Shared renderer for the Greenxertz/Cindara creatures. As of Phase 10 each creature has its OWN + * model geometry (a distinct {@link EntityModel} passed in — tall stalker, low six-legged crawler, + * small greenling, horned cinder brute); this renderer just carries the per-creature texture plus a + * fine-tuning scale + shadow. (Walk/idle animation is the next slice.) + */ +public class GreenxertzCreatureRenderer extends MobRenderer> { + + private final Identifier texture; + private final float scaleX; + private final float scaleY; + private final float scaleZ; + + public GreenxertzCreatureRenderer(EntityRendererProvider.Context context, + EntityModel model, Identifier texture, + float scaleX, float scaleY, float scaleZ, float shadow) { + this(context, model, texture, scaleX, scaleY, scaleZ, shadow, null); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public GreenxertzCreatureRenderer(EntityRendererProvider.Context context, + EntityModel model, Identifier texture, + float scaleX, float scaleY, float scaleZ, float shadow, + Identifier glowTexture) { + super(context, model, shadow); + this.texture = texture; + this.scaleX = scaleX; + this.scaleY = scaleY; + this.scaleZ = scaleZ; + if (glowTexture != null) { + this.addLayer(new GlowEyesLayer(this, glowTexture)); + } + } + + @Override + protected void scale(LivingEntityRenderState state, PoseStack poseStack) { + poseStack.scale(this.scaleX, this.scaleY, this.scaleZ); + } + + @Override + public LivingEntityRenderState createRenderState() { + return new LivingEntityRenderState(); + } + + @Override + public Identifier getTextureLocation(LivingEntityRenderState state) { + return this.texture; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzMobModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzMobModel.java new file mode 100644 index 0000000..d342bc3 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/GreenxertzMobModel.java @@ -0,0 +1,145 @@ +package za.co.neroland.nerospace.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; + +/** + * Base for the Nerospace creature models (Phase 10d/10f). Subclasses build their geometry with + * hip-pivoted limb parts (a {@link ModelPart} whose pivot sits at the joint, with the cubes + * hanging below it) and register them with {@link #swingLimb}: every registered limb swings fore/aft + * (xRot) with the walk cycle, scaled by walk speed — so the mobs actually stride/skitter as they move. + * + *

On top of the walk cycle this base drives idle/ambient motion from + * {@code state.ageInTicks} (Phase 10f), all of it fading out as the walk speed rises so it never + * fights the stride: + *

    + *
  • Breathing bob — every part except the planted (swing-registered) legs bobs gently on + * Y; rate/depth tunable per creature via {@link #breathing} (e.g. the Magma Hulk breathes slow + * and heavy).
  • + *
  • Ambient oscillators — {@link #ambient} registers a per-part sine on a chosen rotation + * axis (head sway, blade-arm flex, frond wiggle, at-rest leg ripple…). Oscillators on the walk + * axis of a swing-registered limb stack additively on the walk pose; everything else is posed + * absolutely from the part's build-time rotation.
  • + *
+ */ +public abstract class GreenxertzMobModel extends EntityModel { + + private record Swing(ModelPart part, float baseXRot, float phase, float amp) { + } + + /** An idle sine on one rotation axis of a part: rot = base + sin(age * freq + phase) * amp. */ + private record Ambient(ModelPart part, Direction.Axis axis, float baseRot, float freq, float phase, float amp) { + } + + private final List swings = new ArrayList<>(); + private final List ambients = new ArrayList<>(); + /** The walk-swung (planted) limb parts — their xRot is owned by the walk cycle. */ + private final Set swungParts = Collections.newSetFromMap(new IdentityHashMap<>()); + /** Body parts that bob with the idle breathing (everything except the planted legs) → base Y. */ + private final Map breatheBaseY = new IdentityHashMap<>(); + private boolean breatheCaptured; + /** Idle breathing rate (radians per tick) and bob depth (pixels); see {@link #breathing}. */ + private float breatheFreq = 0.08F; + private float breatheAmp = 0.5F; + + protected GreenxertzMobModel(ModelPart root) { + super(root); + } + + /** + * Registers a hip-pivoted limb to swing with the walk cycle. + * + * @param name the child part name (from {@code createBodyLayer}) + * @param phase phase offset in radians (e.g. {@code Mth.PI} to oppose another limb) + * @param amp swing amplitude in radians + */ + protected final void swingLimb(String name, float phase, float amp) { + ModelPart part = root().getChild(name); + this.swings.add(new Swing(part, part.xRot, phase, amp)); + this.swungParts.add(part); + } + + /** + * Registers an idle/ambient oscillator on one rotation axis of a part, driven by + * {@code ageInTicks} and faded out by walk speed. + * + * @param name the child part name (from {@code createBodyLayer}) + * @param axis rotation axis to oscillate + * @param freq oscillation rate in radians per tick (~0.04 slow … ~0.14 lively) + * @param phase phase offset in radians (stagger siblings for ripple/wiggle effects) + * @param amp oscillation amplitude in radians (keep subtle: ~0.03–0.1) + */ + protected final void ambient(String name, Direction.Axis axis, float freq, float phase, float amp) { + ModelPart part = root().getChild(name); + float base = switch (axis) { + case X -> part.xRot; + case Y -> part.yRot; + case Z -> part.zRot; + }; + this.ambients.add(new Ambient(part, axis, base, freq, phase, amp)); + } + + /** + * Tunes the idle breathing bob (default {@code freq=0.08, amp=0.5}) — e.g. the Magma Hulk + * breathes slower and deeper. + */ + protected final void breathing(float freq, float amp) { + this.breatheFreq = freq; + this.breatheAmp = amp; + } + + @Override + public void setupAnim(S state) { + super.setupAnim(state); + float pos = state.walkAnimationPos; + float speed = Math.min(1.0F, state.walkAnimationSpeed); + for (Swing s : this.swings) { + s.part().xRot = s.baseXRot() + Mth.cos(pos * 0.6662F + s.phase()) * s.amp() * speed; + } + captureBreatheParts(); + // All ambient motion settles as the mob walks so it never fights the stride. + float idle = 1.0F - speed; + // Idle breathing: bob the BODY parts only (legs are swing-registered and stay planted, so the + // feet don't lift off the ground). + float bob = Mth.sin(state.ageInTicks * this.breatheFreq) * this.breatheAmp * idle; + for (Map.Entry e : this.breatheBaseY.entrySet()) { + e.getKey().y = e.getValue() + bob; + } + // Ambient oscillators (head sway, blade-arm flex, frond wiggle, at-rest leg ripple…). + for (Ambient a : this.ambients) { + float delta = Mth.sin(state.ageInTicks * a.freq() + a.phase()) * a.amp() * idle; + ModelPart part = a.part(); + switch (a.axis()) { + // X is the walk axis: stack on the walk pose for swung limbs (it already reset xRot + // absolutely above), pose absolutely otherwise. + case X -> part.xRot = (this.swungParts.contains(part) ? part.xRot : a.baseRot()) + delta; + case Y -> part.yRot = a.baseRot() + delta; + case Z -> part.zRot = a.baseRot() + delta; + } + } + } + + /** Lazily record which parts breathe: every part except the planted (swing-registered) legs. */ + private void captureBreatheParts() { + if (this.breatheCaptured) { + return; + } + ModelPart rootPart = root(); + rootPart.getAllParts().forEach(part -> { + if (part != rootPart && !this.swungParts.contains(part)) { + this.breatheBaseY.put(part, part.y); + } + }); + this.breatheCaptured = true; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuartzCrawlerModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuartzCrawlerModel.java new file mode 100644 index 0000000..a893c00 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuartzCrawlerModel.java @@ -0,0 +1,82 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Quartz Crawler — "Geode Skitterer" (Phase 10d). A low domed carapace with a back crystal cluster, a + * sensor-head, and six hip-pivoted legs that ripple front-to-back as it skitters. Idle (10f): the + * sensor-head scans side to side and its signature — the six legs keep a faint front-to-back ripple + * even at rest, like an insect that never quite settles. + */ +public class QuartzCrawlerModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "quartz_crawler"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public QuartzCrawlerModel(ModelPart root) { + super(root); + for (int i = 0; i < 3; i++) { + swingLimb("leg_left_" + i, i * 2.1F, 0.3F); + swingLimb("leg_right_" + i, Mth.PI + i * 2.1F, 0.3F); + // Signature idle: a faint at-rest leg ripple, staggered front-to-back like the skitter. + ambient("leg_left_" + i, Direction.Axis.X, 0.12F, i * 2.1F, 0.05F); + ambient("leg_right_" + i, Direction.Axis.X, 0.12F, Mth.PI + i * 2.1F, 0.05F); + } + // The sensor-head scans slowly side to side. + ambient("head", Direction.Axis.Y, 0.05F, 0F, 0.08F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("dome", + CubeListBuilder.create().texOffs(0, 0).addBox(-4F, 12F, -4F, 8F, 3F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("shell", + CubeListBuilder.create().texOffs(0, 0).addBox(-5F, 15F, -5F, 10F, 4F, 10F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("rim", + CubeListBuilder.create().texOffs(0, 0).addBox(-5.5F, 17F, -5.5F, 11F, 2F, 11F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-3F, 15F, -9F, 6F, 4F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (crystals + splayed legs are rotated — Java-authoritative) + root.addOrReplaceChild("crystal_a", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -4F, -1F, 2F, 5F, 2F), + PartPose.offsetAndRotation(-1.5F, 12F, 0F, -0.2F, 0F, 0.3F)); + root.addOrReplaceChild("crystal_b", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -5F, -1F, 2F, 6F, 2F), PartPose.offset(1F, 12F, -1F)); + root.addOrReplaceChild("crystal_c", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -3F, -1F, 2F, 4F, 2F), + PartPose.offsetAndRotation(0F, 12F, 2.5F, -0.3F, 0F, -0.2F)); + + // Six hip-pivoted legs (roll outward, swing fore/aft for the skitter). + float[] zs = {-3.5F, 0F, 3.5F}; + for (int i = 0; i < zs.length; i++) { + root.addOrReplaceChild("leg_left_" + i, + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, 0F, -1F, 2F, 10F, 2F), + PartPose.offsetAndRotation(-5F, 16F, zs[i], 0F, 0F, 0.55F)); + root.addOrReplaceChild("leg_right_" + i, + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, 0F, -1F, 2F, 10F, 2F), + PartPose.offsetAndRotation(5F, 16F, zs[i], 0F, 0F, -0.55F)); + } + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/XertzStalkerModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/XertzStalkerModel.java new file mode 100644 index 0000000..765bac2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/XertzStalkerModel.java @@ -0,0 +1,111 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Xertz Stalker — "Crystal Hunter" (Phase 10d). The hero predator: a tall upright crystalline biped + * with a layered torso, broad shoulders, a browed head, a crest, a row of bladed back-fins, long + * blade-arms and two legs. Each arm and leg is a single hip-pivoted part (thigh+shin+foot / + * upper+forearm+blade) that swings as it stalks forward. Idle (10f): subtle head sway plus its + * signature — the crystal blade-arms slowly flex out and back at the shoulder, like a hunter + * keeping its blades limber. + */ +public class XertzStalkerModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "xertz_stalker"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public XertzStalkerModel(ModelPart root) { + super(root); + swingLimb("leg_l", 0F, 0.5F); + swingLimb("leg_r", Mth.PI, 0.5F); + swingLimb("arm_l", Mth.PI, 0.22F); + swingLimb("arm_r", 0F, 0.22F); + // Idle: slow predatory head sway (jaw tracks the head)… + ambient("head", Direction.Axis.Y, 0.055F, 0F, 0.06F); + ambient("jaw", Direction.Axis.Y, 0.055F, 0F, 0.06F); + // …and the signature blade-arm flex: both blades roll outward together at the shoulder. + ambient("arm_l", Direction.Axis.Z, 0.09F, 0F, 0.05F); + ambient("arm_r", Direction.Axis.Z, 0.09F, Mth.PI, 0.05F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("pelvis", + CubeListBuilder.create().texOffs(0, 0).addBox(-3F, 12F, -2.5F, 6F, 4F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("torso", + CubeListBuilder.create().texOffs(0, 0).addBox(-3.5F, 4F, -3F, 7F, 9F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("chest", + CubeListBuilder.create().texOffs(0, 0).addBox(-2.5F, 5F, -4F, 5F, 5F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("shoulder_left", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, 4F, -2.5F, 3F, 4F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("shoulder_right", + CubeListBuilder.create().texOffs(0, 0).addBox(3F, 4F, -2.5F, 3F, 4F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("neck", + CubeListBuilder.create().texOffs(0, 0).addBox(-2F, 1F, -2F, 4F, 4F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-3F, -3F, -6F, 6F, 5F, 7F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("jaw", + CubeListBuilder.create().texOffs(0, 28).addBox(-2.5F, 2F, -6F, 5F, 2F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (crest/fins are rotated and the limbs are multi-cube — Java-authoritative) + root.addOrReplaceChild("crest", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -7F, -1F, 2F, 6F, 3F), + PartPose.offsetAndRotation(0F, -2F, 1F, -0.4F, 0F, 0F)); + float[] finZ = {1.5F, 4F, 7F}; + float[] finH = {7F, 6F, 4F}; + for (int i = 0; i < finZ.length; i++) { + root.addOrReplaceChild("fin_" + i, + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, -finH[i], -1F, 1F, finH[i], 3F), + PartPose.offsetAndRotation(0F, 5F, finZ[i], -0.25F, 0F, 0F)); + } + + // Arms: single hip-pivoted parts (upper + forearm + down-swept blade). + arm(root, "arm_l", -5F, 0.12F); + arm(root, "arm_r", 5F, -0.12F); + // Legs: single hip-pivoted parts (thigh + shin + foot). + leg(root, "leg_l", -2.5F); + leg(root, "leg_r", 2.5F); + + return LayerDefinition.create(mesh, 64, 64); + } + + private static void arm(PartDefinition root, String name, float x, float roll) { + root.addOrReplaceChild(name, CubeListBuilder.create() + .texOffs(44, 0).addBox(-1.5F, 0F, -1.5F, 3F, 7F, 3F) + .texOffs(44, 0).addBox(-1.5F, 7F, -1.5F, 3F, 6F, 3F) + .texOffs(44, 0).addBox(-0.5F, 11F, -2F, 1F, 10F, 5F), + PartPose.offsetAndRotation(x, 5F, 0F, 0F, 0F, roll)); + } + + private static void leg(PartDefinition root, String name, float x) { + root.addOrReplaceChild(name, CubeListBuilder.create() + .texOffs(44, 0).addBox(-2F, 0F, -2F, 4F, 6F, 4F) + .texOffs(44, 0).addBox(-1.5F, 6F, -1.5F, 3F, 3F, 3F) + .texOffs(44, 0).addBox(-1.5F, 7F, -5F, 3F, 2F, 6F), + PartPose.offset(x, 15F, 0F)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/Greenling.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/Greenling.java new file mode 100644 index 0000000..0040965 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/Greenling.java @@ -0,0 +1,60 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.AvoidEntityGoal; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.PanicGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Greenling (Phase 5) — a small, harmless ambient creature. It wanders the Greenxertz surface and + * flees from players and from being hurt; it has no attack. Pure flavor / life. + */ +public class Greenling extends PathfinderMob { + + public Greenling(EntityType type, Level level) { + super(type, level); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.GREENLING_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.GREENLING_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.GREENLING_DEATH.get(); + } + + public static AttributeSupplier.Builder createAttributes() { + return PathfinderMob.createMobAttributes() + .add(Attributes.MAX_HEALTH, 8.0D) + .add(Attributes.MOVEMENT_SPEED, 0.3D); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(1, new PanicGoal(this, 1.5D)); + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 8.0F, 1.2D, 1.5D)); + this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/QuartzCrawler.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/QuartzCrawler.java new file mode 100644 index 0000000..2ee9652 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/QuartzCrawler.java @@ -0,0 +1,64 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Quartz Crawler (Phase 5) — a neutral creature. It grazes the Greenxertz surface peacefully and + * ignores players, but retaliates with a melee attack when struck (via {@link HurtByTargetGoal}, with + * no player-seeking target goal). + */ +public class QuartzCrawler extends PathfinderMob { + + public QuartzCrawler(EntityType type, Level level) { + super(type, level); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.QUARTZ_CRAWLER_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.QUARTZ_CRAWLER_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.QUARTZ_CRAWLER_DEATH.get(); + } + + public static AttributeSupplier.Builder createAttributes() { + return PathfinderMob.createMobAttributes() + .add(Attributes.MAX_HEALTH, 14.0D) + .add(Attributes.MOVEMENT_SPEED, 0.25D) + .add(Attributes.ATTACK_DAMAGE, 3.0D); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(1, new MeleeAttackGoal(this, 1.2D, true)); + this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + + // Retaliation only — no NearestAttackableTargetGoal, so it never hunts unprovoked. + this.targetSelector.addGoal(1, new HurtByTargetGoal(this)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/XertzStalker.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/XertzStalker.java new file mode 100644 index 0000000..b70fbe1 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/XertzStalker.java @@ -0,0 +1,65 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Xertz Stalker (Phase 5) — the hostile predator of Greenxertz. A crystalline monster that hunts + * players on sight, day or night (the planet is openly hostile). Server-authoritative AI. + */ +public class XertzStalker extends Monster { + + public XertzStalker(EntityType type, Level level) { + super(type, level); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.XERTZ_STALKER_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.XERTZ_STALKER_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.XERTZ_STALKER_DEATH.get(); + } + + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes() + .add(Attributes.MAX_HEALTH, 24.0D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.ATTACK_DAMAGE, 5.0D) + .add(Attributes.FOLLOW_RANGE, 24.0D); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + + this.targetSelector.addGoal(1, new HurtByTargetGoal(this)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java new file mode 100644 index 0000000..2619ebb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java @@ -0,0 +1,46 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.MobCategory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.entity.Greenling; +import za.co.neroland.nerospace.entity.QuartzCrawler; +import za.co.neroland.nerospace.entity.XertzStalker; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Entity types, ported cross-loader through {@link RegistrationProvider} over the vanilla + * {@code ENTITY_TYPE} registry (the root used NeoForge's {@code DeferredRegister.Entities}). The + * builder's {@code build(ResourceKey)} consumes the key the provider hands the factory. Attributes + * are applied per-loader from {@link ModEntityAttributes}; renderers from + * {@code client/ClientEntityRenderers}. Natural-spawn placement rules are deferred until the planet + * dimensions land (the creatures are summonable meanwhile). + */ +public final class ModEntities { + + public static final RegistrationProvider> ENTITY_TYPES = + RegistrationProvider.get(Registries.ENTITY_TYPE, NerospaceCommon.MOD_ID); + + public static final RegistryEntry> XERTZ_STALKER = ENTITY_TYPES.register( + "xertz_stalker", + key -> EntityType.Builder.of(XertzStalker::new, MobCategory.MONSTER) + .sized(0.7F, 1.9F).eyeHeight(1.6F).clientTrackingRange(8).build(key)); + + public static final RegistryEntry> QUARTZ_CRAWLER = ENTITY_TYPES.register( + "quartz_crawler", + key -> EntityType.Builder.of(QuartzCrawler::new, MobCategory.CREATURE) + .sized(0.9F, 0.8F).eyeHeight(0.6F).clientTrackingRange(8).build(key)); + + public static final RegistryEntry> GREENLING = ENTITY_TYPES.register( + "greenling", + key -> EntityType.Builder.of(Greenling::new, MobCategory.AMBIENT) + .sized(0.5F, 0.6F).eyeHeight(0.45F).clientTrackingRange(8).build(key)); + + private ModEntities() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java new file mode 100644 index 0000000..c2768c7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java @@ -0,0 +1,32 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; + +import za.co.neroland.nerospace.entity.Greenling; +import za.co.neroland.nerospace.entity.QuartzCrawler; +import za.co.neroland.nerospace.entity.XertzStalker; + +/** + * Cross-loader default-attribute table for the ported mobs. The loaders apply it differently + * (NeoForge {@code EntityAttributeCreationEvent#put(type, supplier)}; Fabric + * {@code FabricDefaultAttributeRegistry#register(type, builder)}), so this exposes the + * {@link AttributeSupplier.Builder}s and lets each loader consume them. + */ +public final class ModEntityAttributes { + + /** Receives each (entity type, attribute builder) pair for loader-specific registration. */ + public interface Sink { + void accept(EntityType type, AttributeSupplier.Builder builder); + } + + public static void forEach(Sink sink) { + sink.accept(ModEntities.XERTZ_STALKER.get(), XertzStalker.createAttributes()); + sink.accept(ModEntities.QUARTZ_CRAWLER.get(), QuartzCrawler.createAttributes()); + sink.accept(ModEntities.GREENLING.get(), Greenling.createAttributes()); + } + + private ModEntityAttributes() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index 4593635..ef17c16 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -12,10 +12,12 @@ private ModRegistries() { } public static void init() { + ModSounds.init(); za.co.neroland.nerospace.fluid.ModFluids.init(); ModBlocks.init(); ModItems.init(); ModBlockEntities.init(); ModMenuTypes.init(); + ModEntities.init(); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java new file mode 100644 index 0000000..52ad052 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java @@ -0,0 +1,42 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.sounds.SoundEvent; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Creature ambience sound events, ported cross-loader through {@link RegistrationProvider}. No + * {@code .ogg} files ship: {@code assets/nerospace/sounds.json} aliases a fitting vanilla event for + * each ({@code "type": "event"}); swapping in real audio later is a pure resource change. + */ +public final class ModSounds { + + public static final RegistrationProvider SOUND_EVENTS = + RegistrationProvider.get(Registries.SOUND_EVENT, NerospaceCommon.MOD_ID); + + public static final RegistryEntry XERTZ_STALKER_AMBIENT = register("entity.xertz_stalker.ambient"); + public static final RegistryEntry XERTZ_STALKER_HURT = register("entity.xertz_stalker.hurt"); + public static final RegistryEntry XERTZ_STALKER_DEATH = register("entity.xertz_stalker.death"); + + public static final RegistryEntry QUARTZ_CRAWLER_AMBIENT = register("entity.quartz_crawler.ambient"); + public static final RegistryEntry QUARTZ_CRAWLER_HURT = register("entity.quartz_crawler.hurt"); + public static final RegistryEntry QUARTZ_CRAWLER_DEATH = register("entity.quartz_crawler.death"); + + public static final RegistryEntry GREENLING_AMBIENT = register("entity.greenling.ambient"); + public static final RegistryEntry GREENLING_HURT = register("entity.greenling.hurt"); + public static final RegistryEntry GREENLING_DEATH = register("entity.greenling.death"); + + private static RegistryEntry register(String path) { + Identifier id = Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, path); + return SOUND_EVENTS.register(path, key -> SoundEvent.createVariableRangeEvent(id)); + } + + private ModSounds() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 443c1af..e19c24c 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -54,6 +54,9 @@ "block.nerospace.gas_tank": "Gas Tank", "block.nerospace.oxygen_generator": "Oxygen Generator", "block.nerospace.solar_panel": "Solar Panel", + "entity.nerospace.xertz_stalker": "Xertz Stalker", + "entity.nerospace.quartz_crawler": "Quartz Crawler", + "entity.nerospace.greenling": "Greenling", "gas.nerospace.empty": "Empty", "gas.nerospace.oxygen": "Oxygen", "item.nerospace.frame_casing": "Frame Casing", diff --git a/multiloader/common/src/main/resources/assets/nerospace/sounds.json b/multiloader/common/src/main/resources/assets/nerospace/sounds.json new file mode 100644 index 0000000..ae13aca --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/sounds.json @@ -0,0 +1,110 @@ +{ + "block.fuel_tank.pump": { + "subtitle": "subtitles.nerospace.fuel_tank.pump", + "sounds": [{ "name": "block.brewing_stand.brew", "type": "event" }] + }, + + "entity.xertz_stalker.ambient": { + "subtitle": "subtitles.nerospace.xertz_stalker.ambient", + "sounds": [{ "name": "entity.spider.ambient", "type": "event" }] + }, + "entity.xertz_stalker.hurt": { + "subtitle": "subtitles.nerospace.xertz_stalker.hurt", + "sounds": [{ "name": "entity.spider.hurt", "type": "event" }] + }, + "entity.xertz_stalker.death": { + "subtitle": "subtitles.nerospace.xertz_stalker.death", + "sounds": [{ "name": "entity.spider.death", "type": "event" }] + }, + + "entity.quartz_crawler.ambient": { + "subtitle": "subtitles.nerospace.quartz_crawler.ambient", + "sounds": [{ "name": "entity.silverfish.ambient", "type": "event" }] + }, + "entity.quartz_crawler.hurt": { + "subtitle": "subtitles.nerospace.quartz_crawler.hurt", + "sounds": [{ "name": "entity.silverfish.hurt", "type": "event" }] + }, + "entity.quartz_crawler.death": { + "subtitle": "subtitles.nerospace.quartz_crawler.death", + "sounds": [{ "name": "entity.silverfish.death", "type": "event" }] + }, + + "entity.greenling.ambient": { + "subtitle": "subtitles.nerospace.greenling.ambient", + "sounds": [{ "name": "entity.panda.ambient", "type": "event" }] + }, + "entity.greenling.hurt": { + "subtitle": "subtitles.nerospace.greenling.hurt", + "sounds": [{ "name": "entity.panda.hurt", "type": "event" }] + }, + "entity.greenling.death": { + "subtitle": "subtitles.nerospace.greenling.death", + "sounds": [{ "name": "entity.panda.death", "type": "event" }] + }, + + "entity.cinder_stalker.ambient": { + "subtitle": "subtitles.nerospace.cinder_stalker.ambient", + "sounds": [{ "name": "entity.blaze.ambient", "type": "event" }] + }, + "entity.cinder_stalker.hurt": { + "subtitle": "subtitles.nerospace.cinder_stalker.hurt", + "sounds": [{ "name": "entity.blaze.hurt", "type": "event" }] + }, + "entity.cinder_stalker.death": { + "subtitle": "subtitles.nerospace.cinder_stalker.death", + "sounds": [{ "name": "entity.blaze.death", "type": "event" }] + }, + + "entity.frost_strider.ambient": { + "subtitle": "subtitles.nerospace.frost_strider.ambient", + "sounds": [{ "name": "entity.stray.ambient", "type": "event" }] + }, + "entity.frost_strider.hurt": { + "subtitle": "subtitles.nerospace.frost_strider.hurt", + "sounds": [{ "name": "entity.stray.hurt", "type": "event" }] + }, + "entity.frost_strider.death": { + "subtitle": "subtitles.nerospace.frost_strider.death", + "sounds": [{ "name": "entity.stray.death", "type": "event" }] + }, + + "entity.meadow_loper.ambient": { + "subtitle": "subtitles.nerospace.meadow_loper.ambient", + "sounds": [{ "name": "entity.cow.ambient", "type": "event" }] + }, + "entity.meadow_loper.hurt": { + "subtitle": "subtitles.nerospace.meadow_loper.hurt", + "sounds": [{ "name": "entity.cow.hurt", "type": "event" }] + }, + "entity.meadow_loper.death": { + "subtitle": "subtitles.nerospace.meadow_loper.death", + "sounds": [{ "name": "entity.cow.death", "type": "event" }] + }, + + "entity.ember_strutter.ambient": { + "subtitle": "subtitles.nerospace.ember_strutter.ambient", + "sounds": [{ "name": "entity.chicken.ambient", "type": "event" }] + }, + "entity.ember_strutter.hurt": { + "subtitle": "subtitles.nerospace.ember_strutter.hurt", + "sounds": [{ "name": "entity.chicken.hurt", "type": "event" }] + }, + "entity.ember_strutter.death": { + "subtitle": "subtitles.nerospace.ember_strutter.death", + "sounds": [{ "name": "entity.chicken.death", "type": "event" }] + }, + + "entity.woolly_drift.ambient": { + "subtitle": "subtitles.nerospace.woolly_drift.ambient", + "sounds": [{ "name": "entity.sheep.ambient", "type": "event" }] + }, + "entity.woolly_drift.hurt": { + "subtitle": "subtitles.nerospace.woolly_drift.hurt", + "sounds": [{ "name": "entity.sheep.hurt", "type": "event" }] + }, + "entity.woolly_drift.death": { + "subtitle": "subtitles.nerospace.woolly_drift.death", + "sounds": [{ "name": "entity.sheep.death", "type": "event" }] + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling.png new file mode 100644 index 0000000000000000000000000000000000000000..4e26b2d4a840b25076c872a81ba784a913492e2e GIT binary patch literal 2213 zcmV;W2wL}vP)v!5gQMZ`0e=7WH_X1yaQWjU zHoFaGUuOV-``rVW)=z7!mRk&u53pKpD;_pxov3Ew>G!ln@?J8$lHx6B?J_=Z!Zj*(+A@ zUoE%jvW8a6ZDnx)n0=k$=AVx>KYo?>c5#QnXwU$u++2l0<}FHTjB)+6#$YtS zGd8#$U*LYU_slo_A*Ps1zKql6x;*KXpy+=0fWc@`^8zKjw@BOEz?Qdpw%plxg7woH z0Pu`WwRXCgH})G}_DFl~{4xDlOM)i2c>n+$_7Cy-%V){ll)_N7UAXFmt*J;46?cS}}^vgD_-J`1C2 zek3rDv(mc&D);toem0&|&)4INTBh}7$kSjn!1`(3n6qxNZAhpwRXMl=jLuLBHR{;k z0#S^p=L}E;Sh;?*-rl4BBEsUSt+IPO83~{^N9&@))Hw=dlnf(<@SMsAQMlA{0GFUP z?JWzc5>r;rGT{}qQ8X(d%O^cQ5S62n%c(NM#K}j2QGBwlF!D%M1tLD3^S^%i)<%e| zc4q+nSY|0cO99XF0q;}T7$5MG1Tz;e3H{mzr35xJ#A}3CywzBv3Y6R?ONto?&x3_3 z2_1QX;-_%&oafV3AXE)iU~Eijm;d=g)gI;*B`wCpYt()wkEhES(=UoZMi$E&4!<`G zwaCIzMe$>vKUXteXP@0RO@xYf<(8RcOXbazIycHG3Tp|2-zSq`=@2!?K-*;eQrRsj zAtSXThTL6eSp$%)Nva-!9n53j~n%Pr2v6FiPyz88G?Ji;OFM-SC^ zg$k@vSR2YL=jT;sk3vC}YER*x!$#-*liF8c)pvrZe;=saUnR>n<yjxE*Fj{@&03K;m(?!g>D$ zb1Ykjfl5$WY4w6cC65Izc^C8UdWN2i;hX*t_q&JcPT;(Mf;atPHLfVD{~kmsTo`QB zSb85jjJ)c&$F1fYcb0O_UW(7{GEZ{KV8hLy8=ZvS885Zs7NuMqIJx>b}{0~ zO5b_^q!JqVemi7)ri*#y5hj%19|^x&ZmT(EE&s{SUO;<}!XffL=ReCRw<%XmOIyX@4xw*UIt$(fBfSF zPt2F8RNA6S5P3tEIHUKj0W_iQPS6AX@Y4tU`TpEsgj_wrU%cz%e%UISMA}X$sys1Jwj#YQi%AyUeKrsioBuvV~B+} zAS!=9#@pnAb+8JK;RTGNOZ zH#UARXKqy~RF+rek=HS%@Mm(KAs=M#%Hq*GdzTVY^$1>B97d7X`u!0u84N{C7*1-jG#c|Hm|%jMVfSnxK9N(mp; z=4dPuA_K3^R|<*3NzM04+GUHC+$}4cS(x~B(Y{C+g+<{}p_nE?&c13~tye%ZFJnP6 z&^~1CoyXMr2v?&OW{@mksaI$dP7zPlGpO~^nXg?pJU&46J3&R6>tvZ-xo9+=t&Nlt z?QP@oY`|WdMWsVM;D|>XZ&1p}a*86xQ!-vy1tM*h!ljMiSs9DQ`F!?_j4xyJ`+7=K z>OigrQ6^XKQJs%?NBcPWX6I+Tys{cqAhKv%OR`neT$V(m@yMOo+t^5%A%@2X`w&n< zM5T%?rplKRM)8g^z2DCQsePHpM#?ij9reG*Ghvj_rMawPFd#)G#bybw#J8aUb#RFh ngUmp;t@Y7f9#fQ^%NYLy^@co=Xw(ub00000NkvXXu0mjfcTqSd literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling_glow.png new file mode 100644 index 0000000000000000000000000000000000000000..23517657f9e50f35e067898c19f99ec75265f904 GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=lRaG=Ln`LHy`n92$UuPM0&Aq~ zyQXIku3o*m*D>oL+trzu*leuduS`)-tM7a<lx z*(ZPZycOs5xPn#5w|CX*xR~8#a(VLh$HkQl(T{%D>N|Vx{=b~*jeC84{Ji2*{}=3L wO4V3?<5$$~%Zxw=fWVW0=-mD9AngAScqP@`tYnrjS%U;TUHx3vIVCg!0C^EpxBvhE literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler.png new file mode 100644 index 0000000000000000000000000000000000000000..233d187b1bfcac5b355277134917be5b912c13e8 GIT binary patch literal 1923 zcmV-}2YmR6P)6T5S_Pdtwttn;XFi8X@n5Mh*zdaAwmlO2379|7&mJD6IA5~q%uYr6)9AOMW}KS zwIRHySVYvg4m=mpUC*9(&dgO|kT00Gd+*$tGaq+mc2}Oge)D#14`Z=7GBdNjznWZs z?Xg%KjdGaWvW|3!4e1yS(zc$s2u2Xg<%yl0{W@L`!^*mDZWr6vc5u^K*UiVdVOZI_ zcfW^9;;1#aUqgebfpSq!!vtntFUlX^zZ|c-Za%KnjsgZj;XI>f1}oQ@*Nedyiz92) zf){xE;c|o7@%>A?*uEaG&CD*gug5tYk)ez1h?Xg+jtg63#xpZ(@CYyf&wu`Laz6~K zG5pau#TYo0H$5l>=W(F|EJxs;8bh`UY;+8|Zf?i-bFe-mKx{=^2(rF;$?8(mn}#L>cIjOFr#g7paxD?$;XGB+yCl3lG~kViy^#+)31r#Mt1!?{AsN~Gc;tH4tlDi^4V z;rFRtz^^}f1F#V_LS^1Jc2NWMO&EZAjwM8V5K_Y%Z0rL(14gVKme|9T0>u=HKq^mq zKIq+=a_Z+Aw5H#v%A#Mx(!)xYQoPVGUZ{+EmSE)&>9H&H*KO3IdSceWAzjpaM?HSz zWyHW}SQnd}{c_!?1*HHhHOe695!M1TnXENt^z5EJ1K{v@XRqJ9UDvj&-WXZEWpv6z z%IGMY(J*61(TNy08XMjs_YzrRr-Q_CPyQiXV#Q%TYDS9}#N z#1kEBvxiq=2uOt{^NKp3Ojr$g+@tofh{URb#$dT+_Wt~G9i6vqEz2J@XEWt|)5?81BVuC8H*(`njyMZ$v<(%wsA+l+>6JvL2v@Hqt@uZJ1Z1 zaAnO!cFh`<=$xv^Q%d@4W-yj?%l+hykY|Wtj5Np$1x6lc89Iv&HqUUg>!`eL7=RJ_ zdld=MIb*;gWjt^eaco`j%c5`cVPvs5viLyK zqh#oRBZHJV#1oZgGuuppMcMLhue|3k-}`=<|6u2dKPCg^OmYAwg@c=J^g8?CiAgI;VudFvf%AB- z*#$Tbr4+hr1ifn%Vdba2P&W*KJLH{djhJXYJwcWFEsVDr@T4 z_C)Uxxp_S3ZI7@4qlJqssUFmHqqty9o$ZdzIof**x0sy_!X) z$JF-^FGY*jedz!9{mj~b*5j`?O4n^Y(0>}4KD)iM!KU7hvTu!-vko&da&45&{$Aa! zKYsjkRLcMHga19a=_brD)EPhoRnrHk9KjN-qG7lcp4t2J%QY?_5CwoKuLg?@mLqCt z5Lbj3je$DKr@AX+T?RmSlE7+7ySbp)a$=l7$*Ri#n# zy$tyW5=tN|JCSFy`^>w_b5Yq;xIA6qKYka8hN=$HP{~o%XC_nOVv&dUsw{8x0p)2h zQ>>=$DsYb}sY+IjGK_}d@}u#g7LCkvY@KEHn(~U*NXv2wGiIjSqz_QpIZHNCs4{^F z3X`YjsS-5n32J%;)Ti&SzI+*qJjga9omY~ru~He8DrP7pn;jiyR2-8kfQm9}GU3I5 z%HnFMbFS`H`OBg;GJM|$A4emGs6=gILQk>CV_~jjDS5bG$?1Fd{{^N7M& zo{8kE9sn_-A()|*5$MyP@h5qudQtjZNkjxto1 zl%pe7=-NogW0INY!wiA^vSYy%RQV>7i38U*-U=%vnF3gLBDyRr(SXKta_j|RZ|EQdrD17$FJjKWA8ts`FCll26CG%92efKN)^I9@2y zFko3y!{o*kWDdXttS}($*a+1&4h%e_QDs$?zO1kCGF8%yup`Do*C4?1%dF~YGfzeR z1yf9Uo6Qxz$^onlGv!AEpl}tIo`F{?DrZVz?=fW`Mb};d79~tc z22v$4m3Oki9F-;R$q`Ut#RMQddWtFQ=hd%Q90yCnwGpTc@fk&Jo(a!P&oux5002ov JPDHLkV1i_5l(YZ< literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler_glow.png new file mode 100644 index 0000000000000000000000000000000000000000..2901aa4048fdf3d4ee5b09f413add0d18e71df32 GIT binary patch literal 370 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV6^mfaSW-L^Y*4=*C7LuV;|Qa zo$GyY?juc`mI@*MC!R~%UuZg|g}=CT*C9>O|I!wt+{9y^&;Q?EqNQ1@e_kX`rRMxk zwI$cjnLn)B-}+_ucbkVbPv>}^GhcY?iq+QR&r8>NYAo~XVBVr~WWMUlO?LkK-tX>N z)ld@3|2sD@H!Jqq^G6%*mo@bYPinpj(&qR!c?(N~=@QPLuiZi$rfyp@o$=PG=iAtO zyd3__-!}LDB%yG}KDGUws}{-OtB0f?NiS%2-L4LUKe)I>mB MPgg&ebxsLQ0Ks;si2wiq literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker.png new file mode 100644 index 0000000000000000000000000000000000000000..358503928415774ea8ca9bc5b9f3bd0558372b75 GIT binary patch literal 2112 zcmV-G2*3A+fL@Q{JQYM&st50LB=1P$;L99R$(aL_0?iY}6o z!Qsf%2fQ_@`TTv=ohvCCcXxIDb^UqT8BD%@*_)YN+~3;r^NF>u&lcg=<-$g@sqHpf z8_lM6aesR>uK?F09S0xdd<3Ks_r>){Cq7Z8xWCzLwjB~Gzx;f1#LUd>@%sIY$Hd||4a0cz`b8nLq9-q$b z*X6=4?r(d~v*jW^8KxlOA|6vn8B2K*8~dKYFs(9@7r(>vQM&{+wK&+ttIWbNlF$ z4z4_|`3a#QN?;~rq{l)ihDJ0H8Y#~Ce6wy>_V)B*>-M1Bx?NfO`rJJoeEsd|*nc3NLaa0Y~?WzkAG{E?D-rM{5{fF%~Tbo>; zcepASpipWp3kBboRRCp7Gqb^`n{WHKr@%s8bo%b_jgUdi4GwGsl>&U%*rX7>l&f) z{A7=eOTt9FkDew=W=uicr{u}{3MEs-9r*izl3>VKF=5RL+naS~naALM=1Iz<5Z~-~ z6~Nv)j#Y235=)kFFmp4(7(VWG{7lhEGd|I9y%4BrQsU%`EF(E92mm{Ka{kq+`#xXcqX4;oCqe%WWuNTviTqt+O(yrM!b>jBi~TG@sr z3nC%r;;b2A%9iM4LT5CUM`XtI^uN^&rkMDsQ4Y;zxldW&*k5__fz@ z81s+Bd#!@V6H8lG$+8~6%noHx5sdE%{4H_3N8D|;N8=h`if3gE$5Bqi!Ftb1cU&8) zQ$X;SBX}>!?g=A-O+F0{YQP8AwM}6r|(h#_atv#NYQj_^B^HpQINh(TL;IxxGET$gs+U zi{p|~iho=me7gB|^itH!jxI?e;5|WnjqZ1gQK0eZyyqoZX;kpho*+xE5x4UI zJmGlj8-Fl?iP3M5qvxm$tg%q!_(%%{XB7dnJca@@&$%_ zPKKX_z|of>MUcf{mT@zMqfjjCGodrw8Y8OtAueRumN937D7;eAV|jsIr^Si;C z9FT#^?xj6UhEo1uMvwR^6oBlB3)A@1L8wuk>0U;;)VaT8-!>JJbX@l*whe~OEG@HJMfNFqH!MGNM&fKr2A5db)7W0|>HF<;nP_iT(V}^pLEY1`_x-8sSZYbq|njDp0 zjAX*821z9`rZl4;0eH&Ee zCOhd;Nz4>LVVR0{Q*xk@N3&`Fa+EQS8(AEydd*5>si8t8ROu=;M~qLDA@Uj`LPit? qWluDSYDh8#B2KA%sx@^mxb`1T%Qrou<^>=C0000EK~p_7MU z8q;C3ljHRw=(OE;cs@*HvObTh#}i%o!)9j|I&Jq|PJ}*2(|dvIgrJ-ReOx5UHRRyW z>6V6RoL{e&kLE`YI9@Mv*T*4HOGqnof{2vIb<|dNS(UVg!84U_B0(QnX$^b9v)AkQ zVQ!CBfVvl0=e07{@f~#{_W(;PJ`b(|mI`n+R;$$}!w4;0Gp_@z6TQyUh=_=Yh$8vTu5eRodw{Lzy)X6t!1Cd& z&rJaG9+XDrCIFueHnH<-a1(&7-ts&!t8IVl_f5lO=uOf8jqGo^n;jJs@O-iHL}Zh=_=Yh=_>jV|@W; WPVIfpO|Mk|0000 void register(EntityType type, EntityRendererProvider provider) { + EntityRendererRegistry.register(type, provider); + } + }); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index a8fb27f..3e3a3b9 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -1,14 +1,19 @@ package za.co.neroland.nerospace.neoforge; import net.minecraft.client.renderer.block.FluidModel; +import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.resources.model.sprite.Material; import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.EntityRenderersEvent; import net.neoforged.neoforge.client.event.RegisterFluidModelsEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.client.fluid.FluidTintSources; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.client.ClientEntityRenderers; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; @@ -24,6 +29,16 @@ private NeoForgeClientSetup() { public static void init(IEventBus modEventBus) { modEventBus.addListener(NeoForgeClientSetup::onRegisterScreens); modEventBus.addListener(NeoForgeClientSetup::onRegisterFluidModels); + modEventBus.addListener(NeoForgeClientSetup::onRegisterEntityRenderers); + } + + private static void onRegisterEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { + ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { + @Override + public void register(EntityType type, EntityRendererProvider provider) { + event.registerEntityRenderer(type, provider); + } + }); } private static void onRegisterScreens(RegisterMenuScreensEvent event) { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 9516e24..556b0bf 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -9,9 +9,11 @@ import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; +import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; +import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModItems; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; @@ -33,6 +35,7 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NeoForgeClientSetup.init(modEventBus); } modEventBus.addListener(this::onBuildCreativeTabs); + modEventBus.addListener(this::onCreateEntityAttributes); } private void onBuildCreativeTabs(BuildCreativeModeTabContentsEvent event) { @@ -41,4 +44,8 @@ private void onBuildCreativeTabs(BuildCreativeModeTabContentsEvent event) { items.forEach(event::accept); } } + + private void onCreateEntityAttributes(EntityAttributeCreationEvent event) { + ModEntityAttributes.forEach((type, builder) -> event.put(type, builder.build())); + } } From 213a4169f6732f563c4fef6cd90930cf5fb78da0 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 16:31:42 +0200 Subject: [PATCH 31/82] Add six new creatures, models, and assets Introduce six new fauna: CinderStalker, EmberStrutter, FrostStrider, MeadowLoper, RuinWarden and WoollyDrift. Adds entity classes with attributes, AI and sound hooks, client-side model classes and LayerDefinitions, and textures (including glow maps). Registers renderers in ClientEntityRenderers and updates ModEntities, ModEntityAttributes and ModSounds; also adds localization entries in en_us.json. Hostile predators (CinderStalker, FrostStrider, RuinWarden) and livestock (EmberStrutter, MeadowLoper, WoollyDrift) include basic breeding/food handling, sounds, and animations. --- .../nerospace/client/CinderStalkerModel.java | 94 +++++++++++++++++ .../client/ClientEntityRenderers.java | 18 ++++ .../nerospace/client/EmberStrutterModel.java | 83 +++++++++++++++ .../nerospace/client/FrostStriderModel.java | 98 ++++++++++++++++++ .../nerospace/client/MeadowLoperModel.java | 84 +++++++++++++++ .../nerospace/client/RuinWardenModel.java | 73 +++++++++++++ .../nerospace/client/WoollyDriftModel.java | 88 ++++++++++++++++ .../nerospace/entity/CinderStalker.java | 66 ++++++++++++ .../nerospace/entity/EmberStrutter.java | 59 +++++++++++ .../nerospace/entity/FrostStrider.java | 74 +++++++++++++ .../nerospace/entity/MeadowLoper.java | 58 +++++++++++ .../neroland/nerospace/entity/RuinWarden.java | 69 ++++++++++++ .../nerospace/entity/TerraformLivestock.java | 49 +++++++++ .../nerospace/entity/WoollyDrift.java | 65 ++++++++++++ .../nerospace/registry/ModEntities.java | 36 +++++++ .../registry/ModEntityAttributes.java | 12 +++ .../nerospace/registry/ModSounds.java | 20 ++++ .../assets/nerospace/lang/en_us.json | 6 ++ .../textures/entity/cinder_stalker.png | Bin 0 -> 2438 bytes .../textures/entity/cinder_stalker_glow.png | Bin 0 -> 396 bytes .../textures/entity/ember_strutter.png | Bin 0 -> 2143 bytes .../textures/entity/ember_strutter_glow.png | Bin 0 -> 421 bytes .../textures/entity/frost_strider.png | Bin 0 -> 2045 bytes .../textures/entity/frost_strider_glow.png | Bin 0 -> 526 bytes .../textures/entity/meadow_loper.png | Bin 0 -> 2332 bytes .../textures/entity/meadow_loper_glow.png | Bin 0 -> 279 bytes .../nerospace/textures/entity/ruin_warden.png | Bin 0 -> 2764 bytes .../textures/entity/ruin_warden_glow.png | Bin 0 -> 1074 bytes .../textures/entity/woolly_drift.png | Bin 0 -> 888 bytes .../textures/entity/woolly_drift_glow.png | Bin 0 -> 724 bytes 30 files changed, 1052 insertions(+) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/CinderStalkerModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/EmberStrutterModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/FrostStriderModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeadowLoperModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RuinWardenModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/WoollyDriftModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/CinderStalker.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/EmberStrutter.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/FrostStrider.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/MeadowLoper.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/RuinWarden.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/TerraformLivestock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/WoollyDrift.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift_glow.png diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/CinderStalkerModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/CinderStalkerModel.java new file mode 100644 index 0000000..af4a575 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/CinderStalkerModel.java @@ -0,0 +1,94 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Cinder Stalker — "Magma Hulk" (Phase 10d). Grounded volcanic quadruped with a layered body, big + * browed head, horns, an obsidian back-ridge, and four hip-pivoted legs that trot diagonally. + * Idle (10f): its signature is slow, heavy breathing — a deep bob at roughly half the default rate — + * under a ponderous side-to-side sweep of the big browed head. + */ +public class CinderStalkerModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "cinder_stalker"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public CinderStalkerModel(ModelPart root) { + super(root); + swingLimb("leg_fl", 0F, 0.6F); + swingLimb("leg_br", 0F, 0.6F); + swingLimb("leg_fr", Mth.PI, 0.6F); + swingLimb("leg_bl", Mth.PI, 0.6F); + // Signature idle: slow, HEAVY breathing — half the default rate, nearly double the depth. + breathing(0.045F, 0.9F); + // Ponderous head sweep (brow + jaw track the head). + ambient("head", Direction.Axis.Y, 0.04F, 0F, 0.05F); + ambient("brow", Direction.Axis.Y, 0.04F, 0F, 0.05F); + ambient("jaw", Direction.Axis.Y, 0.04F, 0F, 0.05F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, 8F, -6F, 12F, 9F, 11F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("shoulders", + CubeListBuilder.create().texOffs(0, 0).addBox(-5F, 5F, -5F, 10F, 4F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("belly", + CubeListBuilder.create().texOffs(0, 0).addBox(-5F, 16F, -5F, 10F, 3F, 9F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-4F, 9F, -13F, 8F, 8F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("brow", + CubeListBuilder.create().texOffs(0, 28).addBox(-4.5F, 8F, -11F, 9F, 2F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("jaw", + CubeListBuilder.create().texOffs(0, 28).addBox(-3.5F, 15F, -13F, 7F, 2F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_fl", + CubeListBuilder.create().texOffs(44, 0).addBox(-6F, 16F, -5F, 4F, 8F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_fr", + CubeListBuilder.create().texOffs(44, 0).addBox(2F, 16F, -5F, 4F, 8F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_bl", + CubeListBuilder.create().texOffs(44, 0).addBox(-6F, 16F, 1F, 4F, 8F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_br", + CubeListBuilder.create().texOffs(44, 0).addBox(2F, 16F, 1F, 4F, 8F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (horns + back plates are rotated — Java-authoritative) + root.addOrReplaceChild("horn_left", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -4F, -1F, 2F, 5F, 2F), + PartPose.offsetAndRotation(-3F, 9F, -9F, -0.5F, 0F, 0.25F)); + root.addOrReplaceChild("horn_right", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -4F, -1F, 2F, 5F, 2F), + PartPose.offsetAndRotation(3F, 9F, -9F, -0.5F, 0F, -0.25F)); + float[] plateZ = {-3F, 1F, 5F}; + for (int i = 0; i < plateZ.length; i++) { + root.addOrReplaceChild("plate_" + i, + CubeListBuilder.create().texOffs(44, 0).addBox(-3F, -4F, -1F, 6F, 5F, 2F), + PartPose.offsetAndRotation(0F, 6F, plateZ[i], -0.35F, 0F, 0F)); + } + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java index 209196b..af2639a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java @@ -32,6 +32,24 @@ public static void registerAll(Sink sink) { sink.register(ModEntities.GREENLING.get(), context -> new GreenxertzCreatureRenderer(context, new GreenlingModel(GreenlingModel.createBodyLayer().bakeRoot()), tex("greenling"), 1.0F, 1.0F, 1.0F, 0.3F, glow("greenling"))); + sink.register(ModEntities.RUIN_WARDEN.get(), context -> new GreenxertzCreatureRenderer(context, + new RuinWardenModel(RuinWardenModel.createBodyLayer().bakeRoot()), + tex("ruin_warden"), 1.4F, 1.4F, 1.4F, 0.9F, glow("ruin_warden"))); + sink.register(ModEntities.CINDER_STALKER.get(), context -> new GreenxertzCreatureRenderer(context, + new CinderStalkerModel(CinderStalkerModel.createBodyLayer().bakeRoot()), + tex("cinder_stalker"), 1.0F, 1.0F, 1.0F, 0.6F, glow("cinder_stalker"))); + sink.register(ModEntities.FROST_STRIDER.get(), context -> new GreenxertzCreatureRenderer(context, + new FrostStriderModel(FrostStriderModel.createBodyLayer().bakeRoot()), + tex("frost_strider"), 1.0F, 1.0F, 1.0F, 0.5F, glow("frost_strider"))); + sink.register(ModEntities.MEADOW_LOPER.get(), context -> new GreenxertzCreatureRenderer(context, + new MeadowLoperModel(MeadowLoperModel.createBodyLayer().bakeRoot()), + tex("meadow_loper"), 1.0F, 1.0F, 1.0F, 0.6F, glow("meadow_loper"))); + sink.register(ModEntities.EMBER_STRUTTER.get(), context -> new GreenxertzCreatureRenderer(context, + new EmberStrutterModel(EmberStrutterModel.createBodyLayer().bakeRoot()), + tex("ember_strutter"), 1.0F, 1.0F, 1.0F, 0.3F, glow("ember_strutter"))); + sink.register(ModEntities.WOOLLY_DRIFT.get(), context -> new GreenxertzCreatureRenderer(context, + new WoollyDriftModel(WoollyDriftModel.createBodyLayer().bakeRoot()), + tex("woolly_drift"), 1.0F, 1.0F, 1.0F, 0.5F, glow("woolly_drift"))); } private static Identifier tex(String name) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/EmberStrutterModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/EmberStrutterModel.java new file mode 100644 index 0000000..188a132 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/EmberStrutterModel.java @@ -0,0 +1,83 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Ember Strutter (DEEPER_TERRAFORM_DESIGN.md §5) — the skittish chicken-analogue of terraformed + * Cindara: a plump little body on two quick legs, an upright neck with a small combed head and + * beak, stubby wing slabs and a raked tail fan. Idle: rapid bird breathing, sharp pecky head bobs + * and nervous wing flicks. + */ +public class EmberStrutterModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "ember_strutter"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public EmberStrutterModel(ModelPart root) { + super(root); + // Quick alternating two-leg strut. + swingLimb("leg_l", 0F, 0.7F); + swingLimb("leg_r", Mth.PI, 0.7F); + // Rapid little bird breaths. + breathing(0.18F, 0.25F); + // Sharp pecky head bobs (neck and head together) and nervous wing flicks. + ambient("head", Direction.Axis.X, 0.16F, 0F, 0.1F); + ambient("neck", Direction.Axis.X, 0.16F, 0F, 0.07F); + ambient("wing_l", Direction.Axis.Z, 0.14F, 0.5F, 0.06F); + ambient("wing_r", Direction.Axis.Z, 0.14F, 2.1F, 0.06F); + ambient("tail_fan", Direction.Axis.X, 0.1F, 1.0F, 0.05F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-3F, 14F, -4F, 6F, 6F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("neck", + CubeListBuilder.create().texOffs(0, 28).addBox(-1.5F, 9F, -5F, 3F, 5F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-2F, 5F, -6F, 4F, 4F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("beak", + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, 7F, -8F, 2F, 1F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("comb", + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, 3F, -5F, 1F, 2F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("wing_l", + CubeListBuilder.create().texOffs(44, 0).addBox(-4F, 14F, -3F, 1F, 4F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("wing_r", + CubeListBuilder.create().texOffs(44, 0).addBox(3F, 14F, -3F, 1F, 4F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("tail_fan", + CubeListBuilder.create().texOffs(44, 0).addBox(-2F, 12F, 3.5F, 4F, 3F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_l", + CubeListBuilder.create().texOffs(44, 0).addBox(-2F, 20F, -0.5F, 1F, 4F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_r", + CubeListBuilder.create().texOffs(44, 0).addBox(1F, 20F, -0.5F, 1F, 4F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FrostStriderModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FrostStriderModel.java new file mode 100644 index 0000000..67e962e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FrostStriderModel.java @@ -0,0 +1,98 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Frost Strider (NEW_DESTINATION_DESIGN.md §4) — a tall, gangly ice predator stalking Glacira on + * four stilt legs: a slim raised body, a long low-slung neck and angular browed head, a row of + * ice-shard back spines, and hip-pivoted stilt legs that trot diagonally. Silhouette is deliberately + * distinct from the four existing creatures (upright biped / low six-leg dome / chubby toddler / + * heavy quadruped): this one is all legs. Idle: a quick, shallow, bird-like breath under a wary + * side-to-side head scan, with a faint shimmer-tremble in the back shards. + */ +public class FrostStriderModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "frost_strider"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public FrostStriderModel(ModelPart root) { + super(root); + // Diagonal trot on long stilts; modest amplitude keeps the feet planted. + swingLimb("leg_fl", 0F, 0.4F); + swingLimb("leg_br", 0F, 0.4F); + swingLimb("leg_fr", Mth.PI, 0.4F); + swingLimb("leg_bl", Mth.PI, 0.4F); + // Signature idle: quick, shallow, bird-like breathing. + breathing(0.12F, 0.3F); + // Wary head scan (neck + jaw track the head)… + ambient("head", Direction.Axis.Y, 0.07F, 0F, 0.07F); + ambient("neck", Direction.Axis.Y, 0.07F, 0F, 0.05F); + ambient("jaw", Direction.Axis.Y, 0.07F, 0F, 0.07F); + // …and a faint shimmer-tremble through the ice shards (staggered phases ripple back-to-front). + ambient("shard_0", Direction.Axis.Z, 0.11F, 0.0F, 0.03F); + ambient("shard_1", Direction.Axis.Z, 0.11F, 1.6F, 0.03F); + ambient("shard_2", Direction.Axis.Z, 0.11F, 3.2F, 0.03F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-4F, 2F, -7F, 8F, 5F, 14F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("haunch", + CubeListBuilder.create().texOffs(0, 0).addBox(-3.5F, 0F, 3F, 7F, 3F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("neck", + CubeListBuilder.create().texOffs(0, 0).addBox(-1.5F, -3F, -11F, 3F, 6F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-2.5F, -6F, -17F, 5F, 4F, 7F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("brow", + CubeListBuilder.create().texOffs(0, 28).addBox(-3F, -7F, -15F, 6F, 1F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("jaw", + CubeListBuilder.create().texOffs(0, 28).addBox(-2F, -2F, -16F, 4F, 1F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (raked shards are rotated; the stilt legs are two-cube — Java-authoritative) + // A row of ice-shard spines along the spine, raked back like wind-blown icicles. + float[] shardZ = {-4F, 0F, 4F}; + float[] shardH = {6F, 7F, 5F}; + for (int i = 0; i < shardZ.length; i++) { + root.addOrReplaceChild("shard_" + i, + CubeListBuilder.create().texOffs(44, 0).addBox(-1F, -shardH[i], -1F, 2F, shardH[i], 2F), + PartPose.offsetAndRotation(0F, 2.5F, shardZ[i], -0.3F, 0F, 0F)); + } + + // Four stilt legs: hip pivot at the body line, thin shafts dropping to a small splayed foot. + leg(root, "leg_fl", -3F, -5F); + leg(root, "leg_fr", 3F, -5F); + leg(root, "leg_bl", -3F, 5F); + leg(root, "leg_br", 3F, 5F); + + return LayerDefinition.create(mesh, 64, 64); + } + + private static void leg(PartDefinition root, String name, float x, float z) { + root.addOrReplaceChild(name, CubeListBuilder.create() + .texOffs(44, 0).addBox(-1F, 0F, -1F, 2F, 15F, 2F) + .texOffs(44, 0).addBox(-1.5F, 15F, -2F, 3F, 2F, 4F), + PartPose.offset(x, 7F, z)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeadowLoperModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeadowLoperModel.java new file mode 100644 index 0000000..4db1572 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeadowLoperModel.java @@ -0,0 +1,84 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Meadow Loper (DEEPER_TERRAFORM_DESIGN.md §5) — the placid cow-analogue grazer: a deep barrel body + * on four sturdy legs, a broad low-held head with a wide muzzle and small horn nubs, and a lazy + * swishing tail. Silhouette: heavy and horizontal, nothing like the existing predators. Idle: slow, + * deep grazing breaths with the head dipping toward the grass and the tail swatting. + */ +public class MeadowLoperModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "meadow_loper"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public MeadowLoperModel(ModelPart root) { + super(root); + // A steady quadruped amble. + swingLimb("leg_fl", 0F, 0.55F); + swingLimb("leg_br", 0F, 0.55F); + swingLimb("leg_fr", Mth.PI, 0.55F); + swingLimb("leg_bl", Mth.PI, 0.55F); + // Slow, deep grazer breathing. + breathing(0.05F, 0.7F); + // Head dips toward the grass; the muzzle follows. + ambient("head", Direction.Axis.X, 0.045F, 0F, 0.08F); + ambient("muzzle", Direction.Axis.X, 0.045F, 0F, 0.08F); + // Lazy tail swat. + ambient("tail", Direction.Axis.Y, 0.09F, 0.8F, 0.18F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-5F, 8F, -8F, 10F, 9F, 16F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-3F, 6F, -13F, 6F, 6F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("muzzle", + CubeListBuilder.create().texOffs(0, 28).addBox(-2F, 9F, -16F, 4F, 3F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("horn_l", + CubeListBuilder.create().texOffs(44, 0).addBox(-4F, 4F, -11F, 1F, 2F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("horn_r", + CubeListBuilder.create().texOffs(44, 0).addBox(3F, 4F, -11F, 1F, 2F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("tail", + CubeListBuilder.create().texOffs(44, 0).addBox(-0.5F, 9F, 8F, 1F, 7F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_fl", + CubeListBuilder.create().texOffs(44, 0).addBox(-5F, 17F, -7F, 3F, 7F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_fr", + CubeListBuilder.create().texOffs(44, 0).addBox(2F, 17F, -7F, 3F, 7F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_bl", + CubeListBuilder.create().texOffs(44, 0).addBox(-5F, 17F, 4F, 3F, 7F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_br", + CubeListBuilder.create().texOffs(44, 0).addBox(2F, 17F, 4F, 3F, 7F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RuinWardenModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RuinWardenModel.java new file mode 100644 index 0000000..53b1a10 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RuinWardenModel.java @@ -0,0 +1,73 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Ruin Warden model — a hulking crystalline construct: a heavy head, a broad torso, jagged crystal + * shoulder spires, and thick hip/shoulder-pivoted limbs that stride heavily. Idle: a slow heave and + * a faint flicker of the shoulder spires. + */ +public class RuinWardenModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "ruin_warden"), "main"); + + @SuppressWarnings("this-escape") + public RuinWardenModel(ModelPart root) { + super(root); + breathing(0.05F, 0.8F); + swingLimb("leg_left", 0F, 0.5F); + swingLimb("leg_right", Mth.PI, 0.5F); + swingLimb("arm_left", Mth.PI, 0.35F); + swingLimb("arm_right", 0F, 0.35F); + ambient("crystal_left", Direction.Axis.Z, 0.10F, 0F, 0.05F); + ambient("crystal_right", Direction.Axis.Z, 0.10F, 1.6F, 0.05F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-5F, -10F, -5F, 10F, 10F, 10F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, 0F, -3F, 12F, 14F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("crystal_left", + CubeListBuilder.create().texOffs(44, 0).addBox(-9F, -2F, -2F, 3F, 8F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("crystal_right", + CubeListBuilder.create().texOffs(44, 0).addBox(6F, -2F, -2F, 3F, 8F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (pivoted limbs below are Java-authoritative) + + root.addOrReplaceChild("arm_left", + CubeListBuilder.create().texOffs(44, 12).addBox(-2F, 0F, -2F, 4F, 14F, 4F), + PartPose.offset(-8F, 1F, 0F)); + root.addOrReplaceChild("arm_right", + CubeListBuilder.create().texOffs(44, 12).addBox(-2F, 0F, -2F, 4F, 14F, 4F), + PartPose.offset(8F, 1F, 0F)); + root.addOrReplaceChild("leg_left", + CubeListBuilder.create().texOffs(44, 32).addBox(-2.5F, 0F, -2.5F, 5F, 10F, 5F), + PartPose.offset(-3F, 14F, 0F)); + root.addOrReplaceChild("leg_right", + CubeListBuilder.create().texOffs(44, 32).addBox(-2.5F, 0F, -2.5F, 5F, 10F, 5F), + PartPose.offset(3F, 14F, 0F)); + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/WoollyDriftModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/WoollyDriftModel.java new file mode 100644 index 0000000..ded674e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/WoollyDriftModel.java @@ -0,0 +1,88 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Woolly Drift (DEEPER_TERRAFORM_DESIGN.md §5) — the shaggy sheep-analogue of terraformed Glacira: + * a big rounded fleece block on stubby legs, ridged with wind-packed snow tufts, a small bare face + * and drooped ears. Idle: slow huddled breathing, ear twitches and a gentle ripple through the + * fleece tufts like wind over a snowdrift. + */ +public class WoollyDriftModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "woolly_drift"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public WoollyDriftModel(ModelPart root) { + super(root); + // A short-strided trundle on stubby legs. + swingLimb("leg_fl", 0F, 0.5F); + swingLimb("leg_br", 0F, 0.5F); + swingLimb("leg_fr", Mth.PI, 0.5F); + swingLimb("leg_bl", Mth.PI, 0.5F); + // Slow, huddled-in-the-cold breathing. + breathing(0.06F, 0.55F); + // Ear twitches and the wind-ripple through the fleece tufts (staggered phases). + ambient("ear_l", Direction.Axis.Z, 0.13F, 0F, 0.08F); + ambient("ear_r", Direction.Axis.Z, 0.13F, 1.4F, 0.08F); + ambient("tuft_0", Direction.Axis.Z, 0.08F, 0.0F, 0.04F); + ambient("tuft_1", Direction.Axis.Z, 0.08F, 1.5F, 0.04F); + ambient("tuft_2", Direction.Axis.Z, 0.08F, 3.0F, 0.04F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-5F, 8F, -7F, 10F, 9F, 14F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("tuft_0", + CubeListBuilder.create().texOffs(44, 0).addBox(-3F, 8F, -5.5F, 6F, 2F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("tuft_1", + CubeListBuilder.create().texOffs(44, 0).addBox(-3F, 8F, -1.5F, 6F, 2F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("tuft_2", + CubeListBuilder.create().texOffs(44, 0).addBox(-3F, 8F, 2.5F, 6F, 2F, 3F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-2.5F, 7F, -11F, 5F, 5F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("ear_l", + CubeListBuilder.create().texOffs(44, 0).addBox(-4F, 8F, -9F, 1.5F, 3F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("ear_r", + CubeListBuilder.create().texOffs(44, 0).addBox(2.5F, 8F, -9F, 1.5F, 3F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_fl", + CubeListBuilder.create().texOffs(44, 0).addBox(-4F, 17F, -5.5F, 2F, 7F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_fr", + CubeListBuilder.create().texOffs(44, 0).addBox(2F, 17F, -5.5F, 2F, 7F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_bl", + CubeListBuilder.create().texOffs(44, 0).addBox(-4F, 17F, 3.5F, 2F, 7F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("leg_br", + CubeListBuilder.create().texOffs(44, 0).addBox(2F, 17F, 3.5F, 2F, 7F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/CinderStalker.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/CinderStalker.java new file mode 100644 index 0000000..2a9787c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/CinderStalker.java @@ -0,0 +1,66 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Cinder Stalker (Phase 7) — the hostile predator of the volcanic moon Cindara. Tougher and faster + * than the Greenxertz {@link XertzStalker}, and fire-immune (set via the EntityType builder), so the + * planet's lava and heat don't bother it. Server-authoritative AI. + */ +public class CinderStalker extends Monster { + + public CinderStalker(EntityType type, Level level) { + super(type, level); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.CINDER_STALKER_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.CINDER_STALKER_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.CINDER_STALKER_DEATH.get(); + } + + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes() + .add(Attributes.MAX_HEALTH, 30.0D) + .add(Attributes.MOVEMENT_SPEED, 0.33D) + .add(Attributes.ATTACK_DAMAGE, 7.0D) + .add(Attributes.FOLLOW_RANGE, 28.0D); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, false)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + + this.targetSelector.addGoal(1, new HurtByTargetGoal(this)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/EmberStrutter.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/EmberStrutter.java new file mode 100644 index 0000000..a0de805 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/EmberStrutter.java @@ -0,0 +1,59 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Ember Strutter (DEEPER_TERRAFORM_DESIGN.md §5) — the skittish ground bird of mature terraformed + * Cindara: the chicken-analogue, ember-feathered like its scorched homeworld (and fire-proof like + * everything that survives there). Breeds with seeds; drops Strutter Drumstick. + */ +public class EmberStrutter extends TerraformLivestock { + + public EmberStrutter(EntityType type, Level level) { + super(type, level); + } + + @Override + public boolean isFood(ItemStack stack) { + return stack.is(Items.WHEAT_SEEDS); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob partner) { + return ModEntities.EMBER_STRUTTER.get().create(level, EntitySpawnReason.BREEDING); + } + + public static AttributeSupplier.Builder createAttributes() { + return createLivestockAttributes(6.0D, 0.3D); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.EMBER_STRUTTER_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.EMBER_STRUTTER_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.EMBER_STRUTTER_DEATH.get(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/FrostStrider.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/FrostStrider.java new file mode 100644 index 0000000..b104aa8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/FrostStrider.java @@ -0,0 +1,74 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Frost Strider (NEW_DESTINATION_DESIGN.md §4) — the hostile predator of the frozen moon Glacira, + * the cold mirror of the {@link CinderStalker}: where the Magma Hulk is heavy and trotting, the + * Frost Strider is tall and gangly, stalking the ice on stilt legs. Freeze-immune (powder snow and + * cold don't bother it — {@link #canFreeze()}), slightly faster but more fragile than the Cinder + * Stalker. Server-authoritative AI. + */ +public class FrostStrider extends Monster { + + public FrostStrider(EntityType type, Level level) { + super(type, level); + } + + /** The native of an ice moon does not take freezing damage (the cold analogue of fireImmune). */ + @Override + public boolean canFreeze() { + return false; + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.FROST_STRIDER_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.FROST_STRIDER_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.FROST_STRIDER_DEATH.get(); + } + + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes() + .add(Attributes.MAX_HEALTH, 24.0D) + .add(Attributes.MOVEMENT_SPEED, 0.36D) + .add(Attributes.ATTACK_DAMAGE, 6.0D) + .add(Attributes.FOLLOW_RANGE, 28.0D); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.1D, false)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + + this.targetSelector.addGoal(1, new HurtByTargetGoal(this)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/MeadowLoper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/MeadowLoper.java new file mode 100644 index 0000000..436d8aa --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/MeadowLoper.java @@ -0,0 +1,58 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Meadow Loper (DEEPER_TERRAFORM_DESIGN.md §5) — the placid bulk grazer of mature terraformed + * Greenxertz: the cow-analogue of the seeded ecosystem. Breeds with wheat; drops Loper Haunch. + */ +public class MeadowLoper extends TerraformLivestock { + + public MeadowLoper(EntityType type, Level level) { + super(type, level); + } + + @Override + public boolean isFood(ItemStack stack) { + return stack.is(Items.WHEAT); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob partner) { + return ModEntities.MEADOW_LOPER.get().create(level, EntitySpawnReason.BREEDING); + } + + public static AttributeSupplier.Builder createAttributes() { + return createLivestockAttributes(10.0D, 0.22D); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.MEADOW_LOPER_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.MEADOW_LOPER_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.MEADOW_LOPER_DEATH.get(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/RuinWarden.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/RuinWarden.java new file mode 100644 index 0000000..49e7e2b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/RuinWarden.java @@ -0,0 +1,69 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.MeleeAttackGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal; +import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal; +import net.minecraft.world.entity.monster.Monster; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; + +/** + * Ruin Warden (ALIEN_VILLAGERS_DESIGN.md §5.2 / §7) — the boss guardian of the ancient ruins and the + * mega-city keep. A towering crystalline construct: heavily armoured, hard to knock back, and a + * dangerous melee threat. Spawned by structure generation; the reward for clearing it is the + * structure's deep loot. + */ +public class RuinWarden extends Monster { + + public RuinWarden(EntityType type, Level level) { + super(type, level); + } + + public static AttributeSupplier.Builder createAttributes() { + return Monster.createMonsterAttributes() + .add(Attributes.MAX_HEALTH, 120.0D) + .add(Attributes.MOVEMENT_SPEED, 0.24D) + .add(Attributes.ATTACK_DAMAGE, 9.0D) + .add(Attributes.ATTACK_KNOCKBACK, 1.0D) + .add(Attributes.KNOCKBACK_RESISTANCE, 0.75D) + .add(Attributes.ARMOR, 6.0D) + .add(Attributes.FOLLOW_RANGE, 32.0D); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(2, new MeleeAttackGoal(this, 1.0D, true)); + this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 0.8D)); + this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 12.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + + this.targetSelector.addGoal(1, new HurtByTargetGoal(this)); + this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true)); + } + + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.RAVAGER_AMBIENT; + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return SoundEvents.RAVAGER_HURT; + } + + @Override + protected SoundEvent getDeathSound() { + return SoundEvents.RAVAGER_DEATH; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/TerraformLivestock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/TerraformLivestock.java new file mode 100644 index 0000000..66fd4e3 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/TerraformLivestock.java @@ -0,0 +1,49 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.BreedGoal; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.FollowParentGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.PanicGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.TemptGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; + +/** + * Base for the terraform livestock species (DEEPER_TERRAFORM_DESIGN.md §5) — the mod's first + * breedable {@code Animal}s, the "Earth life took hold" payoff of Living terraformed ground. One + * shared food-driven goal set (panic/breed/tempt/follow-parent/wander); species supply their food, + * sounds, attributes and offspring. Breed foods are vanilla crops on purpose: terraformed grass + * drops seeds, so the ranching loop closes on the planet without overworld imports. + */ +public abstract class TerraformLivestock extends Animal { + + protected TerraformLivestock(EntityType type, Level level) { + super(type, level); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(1, new PanicGoal(this, 1.8D)); + this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); + this.goalSelector.addGoal(3, new TemptGoal(this, 1.2D, this::isFood, false)); + this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.2D)); + this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D)); + this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F)); + this.goalSelector.addGoal(7, new RandomLookAroundGoal(this)); + } + + /** Shared attribute base; species pass their health/speed. */ + public static AttributeSupplier.Builder createLivestockAttributes(double health, double speed) { + return Animal.createAnimalAttributes() + .add(Attributes.MAX_HEALTH, health) + .add(Attributes.MOVEMENT_SPEED, speed); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/WoollyDrift.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/WoollyDrift.java new file mode 100644 index 0000000..2df82b2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/WoollyDrift.java @@ -0,0 +1,65 @@ +package za.co.neroland.nerospace.entity; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.registry.ModSounds; + +/** + * Woolly Drift (DEEPER_TERRAFORM_DESIGN.md §5) — the shaggy cold-coat grazer of mature terraformed + * Glacira: the sheep-analogue, unbothered by the cold like the Frost Strider that hunts it. Breeds + * with wheat; drops Drift Fleece (→ string). + */ +public class WoollyDrift extends TerraformLivestock { + + public WoollyDrift(EntityType type, Level level) { + super(type, level); + } + + /** Bred for the ice moon: the cold coat means no freeze build-up (mirrors the Frost Strider). */ + @Override + public boolean canFreeze() { + return false; + } + + @Override + public boolean isFood(ItemStack stack) { + return stack.is(Items.WHEAT); + } + + @Nullable + @Override + public AgeableMob getBreedOffspring(ServerLevel level, AgeableMob partner) { + return ModEntities.WOOLLY_DRIFT.get().create(level, EntitySpawnReason.BREEDING); + } + + public static AttributeSupplier.Builder createAttributes() { + return createLivestockAttributes(8.0D, 0.23D); + } + + @Override + protected SoundEvent getAmbientSound() { + return ModSounds.WOOLLY_DRIFT_AMBIENT.get(); + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return ModSounds.WOOLLY_DRIFT_HURT.get(); + } + + @Override + protected SoundEvent getDeathSound() { + return ModSounds.WOOLLY_DRIFT_DEATH.get(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java index 2619ebb..a58adbf 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java @@ -5,8 +5,14 @@ import net.minecraft.world.entity.MobCategory; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.entity.CinderStalker; +import za.co.neroland.nerospace.entity.EmberStrutter; +import za.co.neroland.nerospace.entity.FrostStrider; import za.co.neroland.nerospace.entity.Greenling; +import za.co.neroland.nerospace.entity.MeadowLoper; import za.co.neroland.nerospace.entity.QuartzCrawler; +import za.co.neroland.nerospace.entity.RuinWarden; +import za.co.neroland.nerospace.entity.WoollyDrift; import za.co.neroland.nerospace.entity.XertzStalker; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -38,6 +44,36 @@ public final class ModEntities { key -> EntityType.Builder.of(Greenling::new, MobCategory.AMBIENT) .sized(0.5F, 0.6F).eyeHeight(0.45F).clientTrackingRange(8).build(key)); + public static final RegistryEntry> RUIN_WARDEN = ENTITY_TYPES.register( + "ruin_warden", + key -> EntityType.Builder.of(RuinWarden::new, MobCategory.MONSTER) + .sized(1.4F, 3.0F).eyeHeight(2.6F).clientTrackingRange(10).build(key)); + + public static final RegistryEntry> CINDER_STALKER = ENTITY_TYPES.register( + "cinder_stalker", + key -> EntityType.Builder.of(CinderStalker::new, MobCategory.MONSTER) + .sized(0.8F, 2.0F).eyeHeight(1.7F).fireImmune().clientTrackingRange(8).build(key)); + + public static final RegistryEntry> FROST_STRIDER = ENTITY_TYPES.register( + "frost_strider", + key -> EntityType.Builder.of(FrostStrider::new, MobCategory.MONSTER) + .sized(0.8F, 2.4F).eyeHeight(2.1F).clientTrackingRange(8).build(key)); + + public static final RegistryEntry> MEADOW_LOPER = ENTITY_TYPES.register( + "meadow_loper", + key -> EntityType.Builder.of(MeadowLoper::new, MobCategory.CREATURE) + .sized(1.1F, 1.3F).eyeHeight(1.1F).clientTrackingRange(8).build(key)); + + public static final RegistryEntry> EMBER_STRUTTER = ENTITY_TYPES.register( + "ember_strutter", + key -> EntityType.Builder.of(EmberStrutter::new, MobCategory.CREATURE) + .sized(0.5F, 0.9F).eyeHeight(0.7F).fireImmune().clientTrackingRange(8).build(key)); + + public static final RegistryEntry> WOOLLY_DRIFT = ENTITY_TYPES.register( + "woolly_drift", + key -> EntityType.Builder.of(WoollyDrift::new, MobCategory.CREATURE) + .sized(0.9F, 1.2F).eyeHeight(1.0F).clientTrackingRange(8).build(key)); + private ModEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java index c2768c7..ea627c0 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java @@ -4,8 +4,14 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import za.co.neroland.nerospace.entity.CinderStalker; +import za.co.neroland.nerospace.entity.EmberStrutter; +import za.co.neroland.nerospace.entity.FrostStrider; import za.co.neroland.nerospace.entity.Greenling; +import za.co.neroland.nerospace.entity.MeadowLoper; import za.co.neroland.nerospace.entity.QuartzCrawler; +import za.co.neroland.nerospace.entity.RuinWarden; +import za.co.neroland.nerospace.entity.WoollyDrift; import za.co.neroland.nerospace.entity.XertzStalker; /** @@ -25,6 +31,12 @@ public static void forEach(Sink sink) { sink.accept(ModEntities.XERTZ_STALKER.get(), XertzStalker.createAttributes()); sink.accept(ModEntities.QUARTZ_CRAWLER.get(), QuartzCrawler.createAttributes()); sink.accept(ModEntities.GREENLING.get(), Greenling.createAttributes()); + sink.accept(ModEntities.RUIN_WARDEN.get(), RuinWarden.createAttributes()); + sink.accept(ModEntities.CINDER_STALKER.get(), CinderStalker.createAttributes()); + sink.accept(ModEntities.FROST_STRIDER.get(), FrostStrider.createAttributes()); + sink.accept(ModEntities.MEADOW_LOPER.get(), MeadowLoper.createAttributes()); + sink.accept(ModEntities.EMBER_STRUTTER.get(), EmberStrutter.createAttributes()); + sink.accept(ModEntities.WOOLLY_DRIFT.get(), WoollyDrift.createAttributes()); } private ModEntityAttributes() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java index 52ad052..d7f1fc5 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java @@ -29,6 +29,26 @@ public final class ModSounds { public static final RegistryEntry GREENLING_HURT = register("entity.greenling.hurt"); public static final RegistryEntry GREENLING_DEATH = register("entity.greenling.death"); + public static final RegistryEntry CINDER_STALKER_AMBIENT = register("entity.cinder_stalker.ambient"); + public static final RegistryEntry CINDER_STALKER_HURT = register("entity.cinder_stalker.hurt"); + public static final RegistryEntry CINDER_STALKER_DEATH = register("entity.cinder_stalker.death"); + + public static final RegistryEntry FROST_STRIDER_AMBIENT = register("entity.frost_strider.ambient"); + public static final RegistryEntry FROST_STRIDER_HURT = register("entity.frost_strider.hurt"); + public static final RegistryEntry FROST_STRIDER_DEATH = register("entity.frost_strider.death"); + + public static final RegistryEntry MEADOW_LOPER_AMBIENT = register("entity.meadow_loper.ambient"); + public static final RegistryEntry MEADOW_LOPER_HURT = register("entity.meadow_loper.hurt"); + public static final RegistryEntry MEADOW_LOPER_DEATH = register("entity.meadow_loper.death"); + + public static final RegistryEntry EMBER_STRUTTER_AMBIENT = register("entity.ember_strutter.ambient"); + public static final RegistryEntry EMBER_STRUTTER_HURT = register("entity.ember_strutter.hurt"); + public static final RegistryEntry EMBER_STRUTTER_DEATH = register("entity.ember_strutter.death"); + + public static final RegistryEntry WOOLLY_DRIFT_AMBIENT = register("entity.woolly_drift.ambient"); + public static final RegistryEntry WOOLLY_DRIFT_HURT = register("entity.woolly_drift.hurt"); + public static final RegistryEntry WOOLLY_DRIFT_DEATH = register("entity.woolly_drift.death"); + private static RegistryEntry register(String path) { Identifier id = Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, path); return SOUND_EVENTS.register(path, key -> SoundEvent.createVariableRangeEvent(id)); diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index e19c24c..c0f134d 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -57,6 +57,12 @@ "entity.nerospace.xertz_stalker": "Xertz Stalker", "entity.nerospace.quartz_crawler": "Quartz Crawler", "entity.nerospace.greenling": "Greenling", + "entity.nerospace.ruin_warden": "Ruin Warden", + "entity.nerospace.cinder_stalker": "Cinder Stalker", + "entity.nerospace.frost_strider": "Frost Strider", + "entity.nerospace.meadow_loper": "Meadow Loper", + "entity.nerospace.ember_strutter": "Ember Strutter", + "entity.nerospace.woolly_drift": "Woolly Drift", "gas.nerospace.empty": "Empty", "gas.nerospace.oxygen": "Oxygen", "item.nerospace.frame_casing": "Frame Casing", diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5f794be015679b479db93b7377193529ae4d76 GIT binary patch literal 2438 zcmV;133>L3P)+J3}z83R0Mp5ZdUWY{bqK3a06y{-;a6o&CHvb-6!{66fd5NK`u&*EWPZIA5s}kHDU+fSOQ(xc?!SGPVvyH7l#hM3ys_Pmw^tp@?o~deqraLw z8GpUcYy*O~0?x0Oa=Iu*L}XG_a=IvGbU3K{RUta(;X~N~p=9~$IP9}Z@DylCWzjx% zWya{K2Y6PyI>=>oIFPrOeQNbdQPutT-@Z2lV?5g^@&cs$UtZLnDUy0XG++BNCYbev z(?ywpS1%CRSbcuIl&|l9h{)Qa2nKl;gS;`$_Az<9?V{kVH`;mVMY@Jk*5+Q5qLN8b z$@t5;j1C7iXkAd-Yb%_5YzTphSS(kv^=Zuq713t#tAkwjcDAJ$ix+rCD zXFK_`pmDnwgIs#AUhX`3+7)RA85RunIzGx}>r*8U_n&2J?~L-aR*A@S+hMr|`&d;n zEO{{2J{^aY1i=Wr1+8TFn=Y>IuU6gKJF6{g-JOAdxc@8<_n#ZpLd>gpmnX;@k-uI^9FO9_cie8I30^^c0VfL$EuRM zll7SWi;Y0c?X4%E0+$GQ;q=06}t9o(e zy??Q)`RcgEa#bs%807W6eIAzUit(|k=)Sh85W(B4<-1s}HXeX3qtDiQwg#}N@ay{@ z^jYWMo#eGbr@N_}+AX#}N^1xu+$z?ZSlg>Y?Xx9}2N`OP#Q_#{#z%R*C0J@yx;x41 zxN$c>Z|qU!y+8WcSZA?ZHNYbf1~vkYau-N z1#7`Z^+XZ_M5Gp|4ZK<(_nU~w?bWJLn5fV?e^OKp@G2A*FzN-YPeFL^8?4B)ZHwh< zt$@d>qWP17$LH};vru|_*{|^MULTB} zy!3~{>U#n1W30wR)t_C9)bWiKzbUHgzX6D0a7 z<;jI>dvl`ovgj2Q0eIj%pq?-+ax)<pl-4B9NufT63hY7K!i*%&ZSc}z~k=Y9}j3wT0#`?swmZL8*@^cKx; zONq(r@T-?QPbio-(G%=Bz}VQfecDAHHrE?x$7E5eK;gZJ>si}^WN8V7@_=_yh^-Bn z+#aUB6_Q!mONC4*Pslb8@c1BkftrnDa}POwdHjsUz5_BVKUP(8JZ;xO^4u8LU}bq%peUQBt7Re(`@GGp+eH~(S#E*(WAtK4~7tTTQwt)u<$=mzhSj&@nlT}p4nBak;{S}7(W(a?Xpo+3!QF+mO z0t%1m4Wm3#6bRY2QA&a!{gFr}YVgA?Vvnhr0@^6DZsaA=OIYUpo<R~>v_uS{@eHDNShr~dsUxTy|Mb9c?si%s&oF|(nUsstjT*yl|(c&!iM3ou?6_vxRitEZB0pr;E zb+~I#V&K?)&N-HwkNcm*CobafVbBq z2CzYqn?>VL=(cg5hq3kAl;ar?#VLh-B4=$A_1&=ClaXH<(Y@3E9ItFqk50O&n)PZmrgud>7dwOQ9AZ}010qYm)EdRB|ZA-}G| zN8_S=A9;}{Qym1mMqt(#1wn5cSi%VI$-~H-=hX~I^}o(~xszGlOKoj!OfWBabqTm^ zpEIRkMzPln8L~LQfOXx+ERHY*d0rOESvQYYg12cU3Z`G3~Lf&c&j07*qoM6N<$ Ef^uiXS^xk5 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker_glow.png new file mode 100644 index 0000000000000000000000000000000000000000..2f0ae502af0e7c24c83efa2049b5148a9b37d4b6 GIT binary patch literal 396 zcmV;70dxL|P)PYX*+xDA5i>9p+6R`C;E&7lnr9!N>yH!F;-yao$JvlRA zo7qs7Y0*?FSl8^=GFE#AD3vF~-S>qnE05`QJ8Hl@Zyt&>eJZqQva^=j^DVO7oRHJI zsqa`9?0IW8uoTDTl-*xzMTD}S5}c``z|dqTk}XE;eUcDYnh&3843@61Mc4;fqtwp& zDcqHq^K1?%t?&&j-&r9i+ox4Xjl+rSWSINhE?Z^Hz|Xd14YLvlS?YUCa-S<)NI#tu{=?wcHc2(IkGqdSxYiXpp+p07_PKKBE zy=sr&SCzd3nas?_cZ;kHY`!z>f7nqi_L~{A<M5MGk9bSFVFJn#g^;W`q>-68w+W8+Gb`&dZ@}?{f7c3;>)KOGcy}qpVtIb6!`fh zjHl0p!Sks6bhWKzNaJL9Y0IYaOPKKAZxF0td zMuMXfZNgfuBP}*Au4XowZ)`H(*z)P+%}DFl1G+2{r>m`XW*^UXEEN5w#r@dEcZ>SU z6i|xcGo!C)`MqiF;ig|l;Nx^V;j?w zF+id!g>p)vg5H3NUV0u4P8-Unk(Bo%VFF9D&g|pa4hed~-s@)XeSj}RiYY3{44g9_ z8bw7x@_hZgmnZG>_~7Xk=|DnKyuY}bnVHpjcX2hVE5T@RdJu~KEGmMEs(g*`At{!W zP2N8lUcMP-|HtTh-s#%;_Gz$Nu3z7jfvabbMpE9R?rl{b7{bhII+OWEHbzmy)TVl{ zz5bb{kiQlwKPK~y-BzVd=9~K2>@Dv-;QjdiT-V;w;MAt8t&Q&%wK04Y`D-$08^KUv zl^SRe)ol55Xr#{U0g796_v+wZLVi@tk+RlrEsL6qTyw# z`v6sMRbvz>$P~*7tHNA0Oc9T^;Xhq%t&2kBMk4MF(N@SdMvG)n;f;KwX!X3SviH^p zFa-8Wdd4|{J#m(wNXw&kl<$5h`LGHwJduqMD({~?L!8eEu5w#>gei-qQGp;v2#WyZ zTVNQn&}1Qn3_WAURAEV!3ddj%8qhn88LBRWN@2_~g|I5b)V^GkqKqAtP*gffS(K%# zYA6#>g(3>Y1Xm@H3C0v)5rFcjNMl%(hAe{-K~yByC-@PaQDxB2p#rKmP*H-kx@?ju z!4Ml6se2TV3C)b%ww^QPqbT$1_4@!NEE<6ctM;~u231KS#Tg$wmy6JJEh6AxUSv$$ zmOxgj82bD5Tqxr+md#v5b^XGzUO>ie6jafr>NKXEO2Hi9V!EXHf>{{Lky0kgU%#%( zlIk}U-gwzm)lsR*WU0#DI|fWw+r~ThG@@^AIk3rjKSu-i9Fq=sKJ7unb-gm|UBEGn zJTo@Wyc2<#pJQIl@nsQ(;9Ou?XrmYsLnRq4SGxDSYyht#$pOrl1qf!S4 z?&~hsueMyjEBWL5vkg9fwe{>*8+`t1e++-&-{J5mxQzF(U#ZPt%7cRuxnrUD^5EVChwry`}-Yl^9AZBCJID`v?rN zNx^x>FWn~o;T7&98j zR0rn$NU^rST=)YRF8Sf^*RtyU+Cp3^!1IW$3Exz5jE#ZKyJ<<5`6P2wwgy zrVkoKRVYtaVG=u+_BT?15mwAqEmp$eJ@UX2rAHLrN(P)yb6urOPEb{Tq8gve*(g2O zIxB~{o&whnekYi#`H{8JfXcX$*I7x7au_QClZ(+v&s}A?Tv=kd8}$*n@Nb(cs^YK_ z(M)IRR`_S~HdmHZ_(vm3jbl@k$%glh-v=;4UIA35jbKPO%1tkPoBAK|R``TGx;d}p z@{cKbOdhM~h(K2HR5C^-^8f=P;)vk6{6eACzFbr&tWZ@&RRV@zg(t`aSHd#{#t6tU zXNvlD)zo zpp6t{JqObBbp2Z41x7&_nBjcevW0sh0V8Dhd)Yl23VGpv`ELP4%Lr^dVP*uj2Mxwb z9V!xmu;Lrhp04)46I29|D2`n7fxfhFF#j37ww0+-2Gayu5|J3!6=>$lCL0Q* zlmd7LX-BmD`8ookMgr&jT9g(n{G*U>Gmpl|Ri)f${6HcCEf&@)4Dl?xZ-Wsr1uo)7wx()%g!oa7&f$jw#$1okrZjf^ z{~kxi-VhO}vwLZJ?w^f!dCOFRTLE^Z{8VKC0000000000064rq1_=SPCPSQ} P00000NkvXXu0mjfkbS<% literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider.png new file mode 100644 index 0000000000000000000000000000000000000000..7cf554c99109fae2741a5bf9de1aae52d2006f3c GIT binary patch literal 2045 zcmV;Zj&g9#N0WdbdTWpE@M=nM1(-o*&vXre_JVP_8Y1rCHlAqG6GGBeSH z`Uc_s0&O7bs_wbxRJB}S?xVZw)m5iz`pT0x^TVf|`JO#KY|O8VzyC3(+b^HY%xq`A zXZyz|{jNvd2650s}2LL=}!UNd=k zADz}k2F#<(!<+4oat;z2pp<3f%qek<{qr%jFWx^sY^-BmIXc|6qr+XP@ZRnpBjq!r z*_^n8xJ%5?Xe{zk(V$W#^6&g6GFh#+wpwrP;`D$T(@L3R&=?gVQLl`jiez*&j!jO! z{rlv*{rTh4&Mud>T5rwF?BevmZXf>Z8TfX$F zAE1n8#;OR&K(j>#&hPwYwcgs<<+9(k7zJj}sSugtPWb$b(}Vt=fgBpldV==8#6~-- z__O&t^F8Z)w_WMG>#t^JhNXYUlThN_D%s4B2y5 zrJ^G`rbJ79J-@o|o2A8kHs}{#%ge6{)L7RTjm*kq#b_(6yj%1+9u*!Dk>6^)9W+)5 zz^remcDE{zRg`oFh*Pl1v2gbYXSaD(`Mcl}K zJ1AgeRfV>~PM8|Fo9=}3tNZ@yI~%WY9a1%hMzu6YPFYovouJvc=3@%@PJ8|{LQG)1 zSb8|QQ4u4UaaG^1Qox@lZ{~+G0+3XHfTZTA+>N}Ifyx1yLzn|FH9A&ft2(QUX3F4u zRnwJ2Rm+S#7212=t3INOFInhh#-L&@j42)8u_lPscN}LrwswK2i_7RcK~zfpn20^P zG6I#Z8XP4v^28&Wvx3glq%M>=^_z4^=H@ z20ED{zpHcwyvhc(sT~V3WsDNS(gSl@?XUt|R62%uYu+p1i~Oc7<*c>*Ma5gjRW(q$ zD9XwuzQd|B_%f8$*@_{qZ@~xVSQd40eX9Jg&BXavs9dJSeAlf$UeW%O&1o$RpBWYrF4msBR0?DmfleMYLXk=QV+VIYMVT>KBcuS2J-;SOn3Hf$9|7$5 zyB}|PG~nUIVm`A!-~UxQh6!iL+=BYA1v^}avN|(;H1xTb25uy;^Rkcx!pxT?>Y@mYvh z^`51Rnb|ZN$&6XdXM;08cS5|zvp-Z!p>@^@y;NjIhbucORZIG*x|e_RJJgAf(#XmR+t8zH*S6 zI1phI8M3l)Qwmm2%1R{ag8X8LqkMK9ltFeXoHZC#^TLc^PHX7{lrGs$E8tWmcv=Z! zdBX&?DArTJ?zzl5#J?6x!?5Uk&sh}Ky&ryfbk(g4dWtUslDJ5GSS@Eju z!0`?lDIu(EF?6mJj?c_^RS{azALpnP>B}Gkmx@|}D55r&rYh3ZE}~3kV8n>ZhK}tf zo2;sV8pB;>WsE75A-PJIsv4j(;{E97Fe+X-CK~-a`55OgqcZ?S2dGraYb;wUkEb0XD zT47jGyT%cvLgiejoU`2O(v)Ozjo%ls(ObPl{}&Ot{dFJ$Arw!8X-V7=_|mJ$?@6$i zC*VO?2~=)UJzqN4e;=J|Ca4!MjB|u_w-yCgNFQG64(V8ZY@!YM#2I-*HU{)<*qxvWI^6o|YKX43a@H%j<%*R6 z+Z?W7$?5WTNq9?nRDY)>t}dWIeB{3WS?)jxA%qY@2qAOp5FRi32|~D7gbEcwh}5nQqe6voan%(mO{yzW9an`bQlvOSs*qB3aFs&15aDpC zA0Skz5JrVD{tr%}oAtAgo|$(&VD@X?`Tlx$<+Gbl^OxQB(G-j)=VoTM+dkTOa^4&r zcH2k$`1yTnO+6Ue;pxG)o1IOjV>2^bE!T!Tm_~`WU;FvT&+l!uT-*Hm!s@}$R^Pta z{oSS|Bg)FWk!IzHj?70C)E6Ux?Pk~Dtd?s#JUv)F7}{hyw(VwT^x&ZsBE=O? zJiok}T0IzAJ?dLM7@CHMo0-|+>7fzK4dP7M zqikpvlnv+f5Bq0Zefw6j;R$Wm%W3r(NrfB>j0SUM>%q`$dc<8V*LHLBp%HemxDAcT z{MdVpZ)V5HB4b}(O`E7QGrPar*yYvK4*Tbm^$2J0u6}$;j3(;YBU4X^n_ULU8XnuWRtK~YRsfrcfXMP^~Xaodx zPZ`*Gp1r&J@g=JGEU<#84YE+!rrrXo?Yffl+3Y0M>QUbg`{yuanQrBTIXt*Xu`qb; zd9RdrL77&Grc&P9?qUCY`~&69+R>5m!F1>;1P;M0B+Xvaz>Cpg|J;QC{Q5!~1$n9K zsE@ms5j^iP{4cMjP1GqJ9ePofR}oMGpZ;7RE`AxP>8&K&{oUq-hCfHSvnZ|*0hvaf z0Ex44CeyJjddM8V1PzmI-fKPxJ{#}Qxh({KUoTky< zg=tnwk!&PZVTN8ur4LUJwpy;6B3TmN>2py=-YSAhBRlgzx+jxh<_*R%GyDCIUu2Q< zU0SpwbUG+pR0A@_B04N58_S|MJs3*mRXtGxb0pw%MJH>e3_Wit zK0x@ajqN7uuL|&8fY~t>oUWO@Zk0#<)&|D;GS6n?LwgD~0&l345o8ze!2;WspSvP_ zWFb)rp2yiBUNh9(j>@X%s6;*L+iv@4^{AhA7u6D*&5(P#9`%neG-xN~gN-Yw+8ioa z4~8f6_2EMpZ zlN>&qOvm#1{oTgy?>4fPc@(h1{!AIgp>m}>A02S!M#UXG5?0G~Yg2--Sll*~A=2@4 zKYHZ3*O4zO<$qaCQr?e-sNMGQXaJt2ojH0LM_dG+^*s_)__7hu=$QNS>x<^v`Jz(F zDLxjf8lZ+ehYA&UhE5oj$VOZTe;?p4LYTJ7XVn4l-gdLI)p8vcHZOqDK1QVsUbnhH zqr~@I#4z+C>G&LmSj@14I1PWsp^e?A9`$X0eR0xpJViOQ@9#Dzvhe)Et&xYNvzt%z z7YsKvLaF~B_Rn^6^Pzbkn1Sj7s66w;mm3V(_B`7!s425~Bh+P#RLr|DR03ujAI4EL zf3!ZMuW&1+oC4KGvhA~4lkL%Yq*b1cHb9Z#6dg{FRs2I)*FS$XGqe5U#zyB;yZi6o7O}~6Y=3`k--{F~g>kflLwc-8WCV4NYckxP zVm>;bngajC#l#~)GX+((j_4^IwE#^QutL|OG5km8)8@L%7Z+x( z?kS^j_4jM^e3p6&U&*J6efnu^|F{XGfSDpg&Rd1U9*Nd64GeR9&IZcT6puaJ0M(;i zYm*Xv=`m}6rNuA6QSr+u;TNxFMs_M{yw1f`b&I=NAH)qz+3w|uj?|HWdl#dtH&=T9pVahOO z!Iz;Dp-iZ(0!J+~qseWn3xAeB8;0Z@z~QL8r;d!H+7)w$TZabEUmaut5zDN>u&q_+6E(mB2X5N45@^Ogj2Lja%INEbH%@kJeU<6g;RVYbdJ-(p?%0jwp-H2#J9_!LYqWThbE~Q_EHY zc@6VWk->CX-jiR7vJm)Og(52+c);~;i39ZOr%seY#CFP6U`4%Ym9_ZkrWA?wZ4+KIWio-nFE-(@`x&wZ&A2QPOG%a zQdC7N^2ufZGqYY~IS%c&q(@Zfx)jt%2F`WAr=qn4$fwl?kx`UanfH`JrQBqLb*UYt zR$8hth*L#YQ#pj;r9z55q?C(<&04Ucsd7K7OX}L}`>({Jd`H6Bbp|SRBB8p>5s@NY zWJWxH-r5U9xmzL{c{(bP%77?Tm(){TB%-a>`|*FxUw9bqe`Bct0000BgVy)faP~spiS%V3%H|Cs%uR+PmqgmLZ*t zPiGyz;`)`Th)u(CNfl#~t%tDx@@ZdBUFTUB_vuq@PLhH~@z#HjuKzMO)@aa*`LCMS zY8K0)Ue9)_LHQk@<3qCwj=rX63wSIJpJFcSDV9IJv;R|M^*pI_(ecX;McaHZ_bo2Mcq7OP^{CoHB&^r=aQQkQ5fEv(S5TJce WFkw0K%qORHKzvVEKbLh*2~7YeZg1`Y literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden.png new file mode 100644 index 0000000000000000000000000000000000000000..81bbf484e0a1614bcb6989b3248ab65a20a65803 GIT binary patch literal 2764 zcmV;-3N!VIP)5{1i#7a14@42d)-8Z;rJ{R}fCAUete7)C(>_W^>Uppo%WKtPD!VY9HSz!Pd@ z;5YCEb5{G?YgcWjCCm19|5R01uUcK*p8fdt^AJ{c`|aTSa&eYEzwKV)slSehh}ZkK zxO%vZ<>D-M`)&IzYpc5IG|B1a>c2@>;2oH zLn(jTy~Jw$YoMrvaDy!uXU$N{#aTS{*Rk7go07ZzHf}B+-+v5T zMim`Q+|4S*DU3=6uI|DW*ew@lZM@g}w^*%zO@cLzEInv+^*Q_T?dKsw(})-#ISoT0 zCD`-(-3Vo;jF2IBX5`Fp-mk{@aVk$47~t!d?+2d=@5o);4V8sH%~lmcU70KdRsdHi zSILzv0kxY`c<(@sp5O0MV03|1Z@1qzgK?JC2S2;Qd{vx4(SW}T0IB@V#p6IHfg+#H zDZE>rs4B!(2l~y8MkHrZypM?Jy7PfRR95eqLR4O3DT+M5-?cEVJ{(K9oHhRJ>zD6` z%*8c;qjCm(r%*YY85yPG8I0o<7O~uRFQX_@jZfVMAUGAyyhBC~##?z(MOZSyd>yM0 zM@|`)?N8x>%5J~?Lxt%wbV0e4)-21_!(}tpQ-2-L?{@=Zh_GlN3E};a$gs$*UGUfY zw^4r@Q}TL$8Mdj+x%LP?PyKaMkZXy9Yn|S9FKsQ7+mlPE5WJR)Kl=g)GzTw@tNK)w z@)5^4d6e04XHF{0Nbq|9Hk!fL`?vO7YgEgI-otmF6 zQn5WMrSRt-N?EJ#Eec2gSKMbsFM(R$e>A4(mSvlWKdwtK6liG~W#KmiL4uve&Xgx< zTdy_)SxDCA`cTDx-%}Hbi2=*SSz~x~LDk>wx1$}L*rhTw?twHISsp#XjYUOO-1GYW zqt%-!C+Lkdm`dfwYMZ%sM5MT)I@u0RiqlwAY|$C4o@Gl)&op`K)#jKG@qM@isd51s zfH`2gmt;m}adzln=s?Xr%6%T$|q36~Br>gwS#p5O2O7(g8}#u69w+(VnUkujy*)Mkg{kb%0=Z0iQ{ zXQaz2&+m5wd3@Zq9VQ!eh2$BWc~W5+(DwnHRrVo3q%aab13N0GcSbIH3z?-}R}YtE zf^<&BAfXI>E2F% zsj$yZ23k~5gE@c^u$Z?ToK4i!{l@H7)w11`|a>a}ilNq`J zegEMhvSO=Ka=qTa9rprLvs#6~EUvq8suZ`1FAoo$rxdCbbWY&1H4yauF?U`W2>BXP zdFrnlZZPD~+B6&iieoT$$o8z05igCKL~*`2uznXnx$rz@Q)D1V zuZ-$$%I6svYFy1Y0hRi(h_=su|Mky7yD8rlW=u~V3dAzk&s_3UzL8`sJDQ^+)l&7m zMb^%r&7b<~#`rv!ug}@+E4C^Rn8j;`F+X(m&l)S+Ysf&E95jfSAeE%IxYzCW+c5~T zWT=_}P{fcIsBp%!eFgi-P+f|DHdD>QthMB$Oamt97a#Ol7ZoZ&`JEYJCRAk*$DL@id0WE){Od?2Kci#Gu*Uodh>A2*Q4YzpMF$+gNWG7;lb9JO;m zBscL8%&{sG6>$u`x82K_2>C#a$=N_zyGrrQC~iDIP;p?EYx5MSB4?7P0>_Ad&jlP* z1d1#~d!f23;1?dKRJOLge-Fccm?@xp>aPdq15S69u9tzby9DZYa5N^trkEJrh@Drr zvNFp{aLuCcJJgoTBs&|-mPe-K)DoKUnp{PDIV0I2Y9@r!Pi{`Clvfso{GNeN2oFW8 z@Hq88mrHx$bME_z-hcmhGidvt`285Ya{p`$pHu=P`N1eFoVhhGP-UfZ z&rEqwhb`(Br9ZtzFjhJ|m(XtJ5eMp>kvSiFU+sJI*%342>lm z;TS0qia5Hy6s#hHz7rEX+)#Kb_t^`U`d&pTr_-6dzBD9biUHaBshC@}X{m6*=`g`J zD^oZyMTJ$I1?synBMpPxu=x66xM5v|NN1MnpU)Tyxm>FBJNqJol#6%W`A9}$1UGeB zeu_Gyo1z?SDx73tFsrYAt0g3*J{x6Dd8!N(F;2Xyjv~iy|8Gu^4c#Mn(j%U&4yini zrowAB&h1_3YKf_Tz5IBNvrXQ>Z8UY+?T7n~IoBeQFBOEF9q zhDmLt^x~;rwYW-^v z|Jjy2X1OS?%R6Xzm?39(4#3@6wne4up33#H4af0QYL~A9V-;@s2T9S07!1I{Goe^A za^&^gcpT4@mK2glbuLrprP+jL*ZLxi3GyaJ#5hkk6)siy=Lt`SOoc9IX!^_?f|iEw z7_XJ74qWC$Yq$i!sp{;^w$q>SZ7X@_ri7c{C?MN zS5=1;S!g2g$awl)j8cj!#Hs#zwHf6CoN4;qWut6B43kPsb+`2 z)AH^y{=0h%`_KM$=HxB=Pg5pCyCLFda#Wm+81j5ikj;?lxgjd$H71vWyKeRyWylU` zoomQ20}Pl#b{E2vm0ZVt|8X+=Q-!fiDGKjba|n`bFa+72`b!R=yxs$UQ%H33=-Q^WJS-(51ehXE}u(T%12tw zdN&u3M+}gXN?rR=GmB^_*Ibp5sQzt2SLCLJ6Tv@aFgiG$Reim(nWkK%-~R{x&8>z0 S%{&(X0000@ zcAO`AXJN;U@K|~to|OkZlK>Gw!YNAG-yfg%)8b}p7cWLp$LIZgkZoVgYPtg*OR#I{ zeMD|NA;=Ve#>;k`vN4&BsqSgG_0+pCyahUUt)~`=TvH$Ru%}iL2Nm1&?)GXrS zDx2H2zcy5@DG>bEII?d@5kwVQRaE4aFIQMpyYFE0arAztoAG633)iBvh6J?%j1hLa zn2PF?NSSus?Kn>tD8sp+zNrL zHxX#ZnkY2|dTWps*6&w#dy(5qa0Q#w|jR^BAdlu5O=sPfEcw}M`@ArH6HCVMm)T((|d z=|Z>m7;>sv~GsHriWq5;6&Eyq|OSA7G`Rp zf`pjdB$YlH*Hjs<@6!_^maq5S%F%9wtN`iYA$g9#^N@^or0l1fW{;2)4G{u1mb~KP z6f&=_yQ;2N-cPS9GeV8ftFO(xI|E27s`RGvKNBO=6sUTh9p$@3l;)vhot*|tCmY1D zvZe7$VdSmrKvowlD>9Sj@+69BG8FG%rpm~Q>anPn(IHU}ynjGI!gxG(D^|u~EH4k| zGUY@R)1&o1;%_%=>9rM+<;|?mS8s1DU%SDHG;Dcf zMtA~wK_b93*mJE`o<9^uC`B=O`0LNFcPsP+{Qi1>kA5Tg`1twwfT`=POHNdYl_rR7 zf4xSJ=Euz1yfypZt18I~&DNy2!x({@=c~NY#wnhxHW@)WbkW-7;>;zB+AmN7Ou5nY zQOJ%iyc0xPsx}quz_Su(SUPghj-sUMqE$SlY)0~~qk$SG$Uo|^y!X?Un&lTkw806P zAVIY{L}fMVC_6(L>k%UciwX=+cqV2{Op~g#Lj0>+ORu9_&ynS;O_t?LBXzp4N@=?T z3V?fMieggc9rB*MqC+BJAX`7--X6V_hT1~1fhh|_$dZZ|A)M^kxbLE2#8v$~f=qZN zz*4d!_lvBqBjE6vzKI9!&A%K?DyWXfl~Y2h*uT$Bv~#hSE<^GIa~=(ljB2-#!~>m z{Xq|dXUXz@__;ePx4;Mi9&TiaRQ4#ZKiId|) z%%f$hZMXmiNdI01eZQC6$JThDgah~ttD?GhGr&S<5oqR#Bi{!AoPYibAQ=fZl!^f0 zaYj^!(76PBLh@3uS;c^pV3u-7UB*)l&}ky$O=(lw0er^mf*VaK!1buJZi<3ZH2|o8 z#(}iaWhfj0qwzF0dJ{`sT@-de-|xlY#tUH`WaWJk8bvok!%kNA1h|RaIOYw-3>39Z|fE@As>?7vMVbR*|Y!m^cMgV{HZ>ji+&;n@3A@ z+AZzQ&_<{_33>G;FFI|>jey|-iH9AgaOJ=2B7pZ%udBNrY1-soqDh0O0~02Z)Su71 z8WCBjPR$yhJIyva)*+4nQ_)h37ZpFid=o@VpssM`MNwTl$V3F_2IWHe@jO~;L!9%f z+e-bPJ}Q%7hWi7v@mX5R5HEvVh?;l0!PJMISG@2~gII zs@r4Tw=U;xjIS&L%4~)Afy!=%b`|FUX#vtac?W2>YJY~;@H`J;7yD$e!tAWf2Ami2JedEcefYt$?=NZrU39AU$=M-qXJG)%btEQVENW6BUnGeG-)kgg} zN4}32gXbHTTrEv*6diNJ)i&V2;kp-;Rlc#Qi#P?iQKl7uBB9C`l#;~z0B+EaZ(kCf z-hcQA-jAmPo$o5zjnK>2Z;XFZ(_xo!x5Lr`q;U?A79fptfV2Q+Im zYf5@8Px4~-S4@66W#^ah@^d0qZ2odihI75z1#DO@Ej+PzPHN<|se6`OV^<9+TT|}8 zeDV{Q{P#y2Wh&;`U&`96sI+MP? zzWBb4OkNy*Aa#J7hd=Hr+x;DR zcWy~+;hMo@#!LvtA77Hak?kNdDg9K_U32JtbYZhH>}J!`DkgE{KTIEOQt!*K74g* zQSn^;J|l#Z)Db45G=X+W}>7P+xsowLs>x_STt$F)* z0k6^RklU6sPMrH;GkI&{{p0r;w)U>LBlp^_wD_J{MZV;N-zBBF71lhl+6Ni7FqkYh zci0_TU3qFvUE)uE){-xh0z3}f4#ri?RsZMQIl5K!*u``Q0|s4&voCl#jwb>&fOU~c a*e7(&Jba9|_6;z#GI+ZBxvX Date: Sat, 20 Jun 2026 16:37:40 +0200 Subject: [PATCH 32/82] Add Alien Villager entity, renderer & trades Introduce a new Alien Villager NPC and supporting assets: adds the AlienVillager entity (merchant behavior, per-player Reputation, persistence, variant/planet/biome/color seed), createAttributes(), and registration in ModEntities and ModEntityAttributes. Implements Phase-2 trading flow and gift interactions with tiered offers via new AlienTrades and Reputation utility; offers include the Xertz Resonator (added as a plain item, model and texture included; custom gear behaviour deferred). Adds client-side rendering: AlienVillagerModel, AlienVillagerRenderState, AlienVillagerRenderer, textures (base/meadow/cindara/glacira/glow) and language entries; registers the renderer in ClientEntityRenderers. Updates docs (MULTILOADER_MIGRATION.md) noting port progress and temporary Greenxertz-only planet selection; natural spawning and spawn eggs are deferred pending dimensions/biomes. Persistence: per-villager reputation map is serialized/deserialized with a Codec; trading state and display tier logic included. --- docs/MULTILOADER_MIGRATION.md | 18 +- .../nerospace/client/AlienVillagerModel.java | 82 ++++ .../client/AlienVillagerRenderState.java | 19 + .../client/AlienVillagerRenderer.java | 73 ++++ .../client/ClientEntityRenderers.java | 1 + .../nerospace/entity/AlienVillager.java | 406 ++++++++++++++++++ .../nerospace/registry/ModEntities.java | 6 + .../registry/ModEntityAttributes.java | 2 + .../neroland/nerospace/registry/ModItems.java | 4 +- .../nerospace/village/AlienTrades.java | 72 ++++ .../nerospace/village/Reputation.java | 37 ++ .../nerospace/items/xertz_resonator.json | 6 + .../assets/nerospace/lang/en_us.json | 2 + .../models/item/xertz_resonator.json | 6 + .../textures/entity/alien_villager.png | Bin 0 -> 2205 bytes .../entity/alien_villager_cindara.png | Bin 0 -> 2209 bytes .../entity/alien_villager_glacira.png | Bin 0 -> 2161 bytes .../textures/entity/alien_villager_glow.png | Bin 0 -> 454 bytes .../textures/entity/alien_villager_meadow.png | Bin 0 -> 2200 bytes .../textures/item/xertz_resonator.png | Bin 0 -> 145 bytes 20 files changed, 729 insertions(+), 5 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderState.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderer.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/entity/AlienVillager.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/village/AlienTrades.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/village/Reputation.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/xertz_resonator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_resonator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_cindara.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glacira.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_meadow.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_resonator.png diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index d471b36..5814371 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -39,6 +39,15 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > end-to-end: generator → pipe → oxygen generator → pipe → gas tank. (The world oxygen-field effect + > HUD + the generator GUI are a deferred atmosphere subsystem.) > +> **2026-06-20 (later still): ALL 10 mobs ported** — all 4 cells green. On the entity seam below, added +> `cinder_stalker`, `frost_strider`, `ruin_warden`, the three terraform livestock (`meadow_loper`, +> `ember_strutter`, `woolly_drift` via a shared `TerraformLivestock` base), and the **alien villager** +> (full `Merchant` trading + per-player `Reputation` + gift loop + per-individual render tint/skin, with +> its own renderer). Ported the `village` trade package (`Reputation`, `AlienTrades`) and a plain +> `xertz_resonator` item (its gear behaviour deferred). The villager's per-dimension planet pick is +> temporarily fixed to Greenxertz until `ModDimensions` lands. Natural spawning + spawn eggs remain +> deferred (mobs are summonable; spawning waits on the planet dimensions/biomes). +> > **2026-06-20 (later still): ENTITY seam + Greenxertz creatures ported** — all 4 cells green. New > cross-loader seam: entity types via `RegistrationProvider` over `ENTITY_TYPE` (`EntityType.Builder… > build(key)`); **attributes** via `ModEntityAttributes` applied per loader (NeoForge @@ -64,12 +73,13 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > a deferred enhancement. > > Remaining, by subsystem (rough size): **dimensions** (Greenxertz/Cindara/Glacira biomes+dims+travel; -> unblocks the planet ores' worldgen); **entities** (alien villager, xertz stalker + attributes + -> renderers); **rockets** (items, tiers, launch logic); **quarry** (area mining); **structures** +> unblocks the planet ores' worldgen, mob natural-spawning, and the villager's per-planet variant); +> **rockets** (items, tiers, launch logic); **quarry** (area mining); **structures** > (station/village/meteor cores + events); **atmosphere/terraforming** (oxygen field, terraformer, > monitor, hydration); **solar panel tiers/array/BER** (single-tier base is done); **star guide** -> (progression UI); **creative item/fluid/gas stores** (infinite-resource config — marginal). -> Recommended order: entities → dimensions → rockets → quarry → structures → atmosphere → the rest. +> (progression UI); **creative item/fluid/gas stores** (infinite-resource config — marginal); plus mob +> **spawn eggs + natural-spawn rules** (deferred with dimensions). **All 10 mobs are otherwise ported.** +> Recommended order: dimensions → rockets → quarry → structures → atmosphere → the rest. --- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerModel.java new file mode 100644 index 0000000..e5a3d2b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerModel.java @@ -0,0 +1,82 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Alien Villager (Phase 0) — an upright humanoid: a domed head, a robed torso, two crystalline + * shoulder growths (the Greenxertz silhouette cue), and hip/shoulder-pivoted arms and legs that + * stride as it walks. Idle: a slow head sway and a faint wobble of the shoulder crystals. + * + *

Geometry split (model_sync rules): the static torso/head/crystals live in the marker block + * (one cube per bone, no rotation), so they round-trip to {@code alien_villager.bbmodel}. The + * pivoted limbs sit OUTSIDE the block (Java-authoritative) because their pivots are at the joint, + * which the marker form can't express. + */ +public class AlienVillagerModel extends GreenxertzMobModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "alien_villager"), "main"); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public AlienVillagerModel(ModelPart root) { + super(root); + // Opposed stride: legs lead, arms counter-swing. + swingLimb("leg_left", 0F, 0.6F); + swingLimb("leg_right", Mth.PI, 0.6F); + swingLimb("arm_left", Mth.PI, 0.4F); + swingLimb("arm_right", 0F, 0.4F); + // Idle: a slow, calm head sway and a faint out-of-phase wobble of the shoulder crystals. + ambient("head", Direction.Axis.Y, 0.05F, 0F, 0.06F); + ambient("crystal_left", Direction.Axis.Z, 0.08F, 0F, 0.04F); + ambient("crystal_right", Direction.Axis.Z, 0.08F, 1.4F, 0.04F); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + // model_sync:begin + root.addOrReplaceChild("head", + CubeListBuilder.create().texOffs(0, 28).addBox(-4F, -6F, -4F, 8F, 8F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-4F, 2F, -2F, 8F, 12F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("crystal_left", + CubeListBuilder.create().texOffs(44, 0).addBox(-6F, 0F, -1F, 2F, 5F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("crystal_right", + CubeListBuilder.create().texOffs(44, 0).addBox(4F, 0F, -1F, 2F, 5F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + // model_sync:end (pivoted limbs below are Java-authoritative — joints, not origin-anchored) + + // Shoulder-pivoted arms (pivot at the shoulder, cubes hang below). + root.addOrReplaceChild("arm_left", + CubeListBuilder.create().texOffs(44, 8).addBox(-1F, 0F, -2F, 2F, 11F, 4F), + PartPose.offset(-5F, 3F, 0F)); + root.addOrReplaceChild("arm_right", + CubeListBuilder.create().texOffs(44, 8).addBox(-1F, 0F, -2F, 2F, 11F, 4F), + PartPose.offset(5F, 3F, 0F)); + + // Hip-pivoted legs (pivot at the hip; feet rest on the ground at java y=24). + root.addOrReplaceChild("leg_left", + CubeListBuilder.create().texOffs(44, 24).addBox(-1.5F, 0F, -2F, 3F, 10F, 4F), + PartPose.offset(-2F, 14F, 0F)); + root.addOrReplaceChild("leg_right", + CubeListBuilder.create().texOffs(44, 24).addBox(-1.5F, 0F, -2F, 3F, 10F, 4F), + PartPose.offset(2F, 14F, 0F)); + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderState.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderState.java new file mode 100644 index 0000000..917f25d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderState.java @@ -0,0 +1,19 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.entity.state.LivingEntityRenderState; + +import za.co.neroland.nerospace.entity.AlienVillager; + +/** + * Render state for the Alien Villager. Carries the per-individual variant pulled off the entity in + * AlienVillagerRenderer#extractRenderState: the colour seed drives the palette tint, the planet + + * home biome select the base/accessory texture, and the display tier (Phase 2) warms the tint as the + * villager grows to trust players. + */ +public class AlienVillagerRenderState extends LivingEntityRenderState { + + public int colorSeed; + public String biomeId = ""; + public AlienVillager.Planet planet = AlienVillager.Planet.GREENXERTZ; + public int displayTier; +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderer.java new file mode 100644 index 0000000..52762be --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/AlienVillagerRenderer.java @@ -0,0 +1,73 @@ +package za.co.neroland.nerospace.client; + +import java.util.Random; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.entity.MobRenderer; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.entity.AlienVillager; + +/** + * Renderer for the Alien Villager. Per-individual palette tint (getModelTint), per-planet + per-biome + * skin (getTextureLocation: Greenxertz green/steel with a lighter meadow set, Cindara ember/red, + * Glacira frost/pale), a trust-warmed mood tint, and the emissive eye/crystal glow. + */ +public class AlienVillagerRenderer + extends MobRenderer> { + + private static Identifier tex(String n) { + return Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/" + n + ".png"); + } + + private static final Identifier BASE = tex("alien_villager"); + private static final Identifier MEADOW = tex("alien_villager_meadow"); + private static final Identifier CINDARA = tex("alien_villager_cindara"); + private static final Identifier GLACIRA = tex("alien_villager_glacira"); + private static final Identifier GLOW = tex("alien_villager_glow"); + + @SuppressWarnings("this-escape") + public AlienVillagerRenderer(EntityRendererProvider.Context context) { + super(context, new AlienVillagerModel(AlienVillagerModel.createBodyLayer().bakeRoot()), 0.4F); + this.addLayer(new GlowEyesLayer(this, GLOW)); + } + + @Override + public AlienVillagerRenderState createRenderState() { + return new AlienVillagerRenderState(); + } + + @Override + public void extractRenderState(AlienVillager entity, AlienVillagerRenderState state, float partialTick) { + super.extractRenderState(entity, state, partialTick); + state.colorSeed = entity.getColorSeed(); + state.biomeId = entity.getBiomeId(); + state.planet = entity.getPlanet(); + state.displayTier = entity.getDisplayTier(); + } + + @Override + public Identifier getTextureLocation(AlienVillagerRenderState state) { + return switch (state.planet) { + case CINDARA -> CINDARA; + case GLACIRA -> GLACIRA; + case GREENXERTZ -> (state.biomeId != null && state.biomeId.contains("meadow")) ? MEADOW : BASE; + }; + } + + @Override + protected int getModelTint(AlienVillagerRenderState state) { + Random rnd = new Random(state.colorSeed); + int warmth = Math.max(0, Math.min(5, state.displayTier)) * 4; + int r = clamp(200 + rnd.nextInt(56) + warmth); + int g = clamp(216 + rnd.nextInt(40) + warmth / 2); + int b = clamp(190 + rnd.nextInt(56)); + return 0xFF000000 | (r << 16) | (g << 8) | b; + } + + private static int clamp(int v) { + return Math.max(0, Math.min(255, v)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java index af2639a..5f0a6db 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java @@ -50,6 +50,7 @@ public static void registerAll(Sink sink) { sink.register(ModEntities.WOOLLY_DRIFT.get(), context -> new GreenxertzCreatureRenderer(context, new WoollyDriftModel(WoollyDriftModel.createBodyLayer().bakeRoot()), tex("woolly_drift"), 1.0F, 1.0F, 1.0F, 0.5F, glow("woolly_drift"))); + sink.register(ModEntities.ALIEN_VILLAGER.get(), AlienVillagerRenderer::new); } private static Identifier tex(String name) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/AlienVillager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/AlienVillager.java new file mode 100644 index 0000000..4424583 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/entity/AlienVillager.java @@ -0,0 +1,406 @@ +package za.co.neroland.nerospace.entity; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.UUID; + +import com.mojang.serialization.Codec; + +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.PathfinderMob; +import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.goal.AvoidEntityGoal; +import net.minecraft.world.entity.ai.goal.FloatGoal; +import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal; +import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal; +import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.MerchantMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.trading.Merchant; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.village.AlienTrades; +import za.co.neroland.nerospace.village.Reputation; + +/** + * Alien Villager (ALIEN_VILLAGERS_DESIGN.md). A social alien NPC of the nerospace planets: a + * wary-neutral wanderer that the player wins over to unlock trades. + * + *

Phase 0/1: wanders its home biome, carries a per-individual variant (planet, biome, colour seed) + * that drives a unique render tint + per-biome skin. + * + *

Phase 2: it is now a {@link Merchant}. Each villager tracks a per-player reputation score + * (0..{@link Reputation#MAX}) -> 6 tiers. Gifting palette-appropriate goods raises reputation; at T1+ + * the villager opens the vanilla trading screen with a tier-gated offer list ({@link AlienTrades}). + * Completing trades nudges reputation up. Reputation is stored on the villager for now; the Village + * Core block (Phase 3/4) will aggregate it per-village. + */ +public class AlienVillager extends PathfinderMob implements Merchant { + + /** Which planet's species this villager belongs to (drives palette + silhouette later). */ + public enum Planet { + GREENXERTZ, CINDARA, GLACIRA; + + public static Planet byOrdinal(int i) { + Planet[] values = values(); + return values[Math.floorMod(i, values.length)]; + } + } + + private static final EntityDataAccessor DATA_PLANET = + SynchedEntityData.defineId(AlienVillager.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BIOME = + SynchedEntityData.defineId(AlienVillager.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor DATA_COLOR_SEED = + SynchedEntityData.defineId(AlienVillager.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_DISPLAY_TIER = + SynchedEntityData.defineId(AlienVillager.class, EntityDataSerializers.INT); + + private static final Codec> REP_CODEC = Codec.unboundedMap(Codec.STRING, Codec.INT); + + private boolean variantAssigned; + + private final Map reputation = new HashMap<>(); + + private Player tradingPlayer; + private MerchantOffers offers; + private int villagerXp; + + public AlienVillager(EntityType type, Level level) { + super(type, level); + } + + public static AttributeSupplier.Builder createAttributes() { + return PathfinderMob.createMobAttributes() + .add(Attributes.MAX_HEALTH, 20.0D) + .add(Attributes.MOVEMENT_SPEED, 0.3D) + .add(Attributes.FOLLOW_RANGE, 16.0D); + } + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + super.defineSynchedData(builder); + builder.define(DATA_PLANET, Planet.GREENXERTZ.ordinal()); + builder.define(DATA_BIOME, ""); + builder.define(DATA_COLOR_SEED, 0); + builder.define(DATA_DISPLAY_TIER, 0); + } + + @Override + protected void registerGoals() { + this.goalSelector.addGoal(0, new FloatGoal(this)); + this.goalSelector.addGoal(2, new AvoidEntityGoal<>(this, Player.class, 4.0F, 1.0D, 1.2D)); + this.goalSelector.addGoal(6, new WaterAvoidingRandomStrollGoal(this, 0.9D)); + this.goalSelector.addGoal(7, new LookAtPlayerGoal(this, Player.class, 8.0F)); + this.goalSelector.addGoal(8, new RandomLookAroundGoal(this)); + } + + @Override + public void tick() { + super.tick(); + if (!this.level().isClientSide()) { + if (!this.variantAssigned) { + assignVariant(); + this.variantAssigned = true; + } + if (this.tradingPlayer != null + && (this.tradingPlayer.isRemoved() || this.distanceToSqr(this.tradingPlayer) > 100.0D)) { + this.setTradingPlayer(null); + } + } + } + + private void assignVariant() { + setColorSeed(this.random.nextInt() | 1); + setPlanet(planetForDimension()); + Holder biome = this.level().getBiome(this.blockPosition()); + biome.unwrapKey().ifPresent(key -> setBiomeId(key.identifier().toString())); + } + + private Planet planetForDimension() { + // Multiloader port: the planet dimensions aren't ported yet, so every villager is the + // Greenxertz species for now. Restore per-dimension selection once ModDimensions lands. + return Planet.GREENXERTZ; + } + + // --- Interaction: gifts + trading ----------------------------------------- + + @Override + protected InteractionResult mobInteract(Player player, InteractionHand hand) { + ItemStack held = player.getItemInHand(hand); + boolean gift = isGift(held); + boolean canTrade = getTier(player) >= 1 && !this.isBaby(); + + if (!gift && !canTrade) { + if (!this.level().isClientSide()) { + this.playSound(SoundEvents.VILLAGER_NO, 1.0F, 1.0F); + } + return InteractionResult.SUCCESS; + } + if (this.level().isClientSide()) { + return InteractionResult.SUCCESS; + } + if (gift) { + receiveGift(player, held); + return InteractionResult.SUCCESS; + } + if (this.getTradingPlayer() == null && this.isAlive()) { + startTrading(player); + } + return InteractionResult.SUCCESS; + } + + private void startTrading(Player player) { + rebuildOffers(player); + this.setTradingPlayer(player); + OptionalInt opt = player.openMenu(new SimpleMenuProvider( + (id, inv, p) -> new MerchantMenu(id, inv, this), this.getDisplayName())); + if (opt.isPresent()) { + player.sendMerchantOffers(opt.getAsInt(), this.getOffers(), 1, this.getVillagerXp(), + this.showProgressBar(), false); + } + } + + private void receiveGift(Player player, ItemStack held) { + int gain = giftValue(held); + if (gain <= 0) { + return; + } + held.consume(1, player); + addReputation(player, gain); + this.playSound(SoundEvents.VILLAGER_YES, 1.0F, 1.0F); + if (this.level() instanceof ServerLevel server) { + server.sendParticles(ParticleTypes.HAPPY_VILLAGER, + this.getX(), this.getY() + 1.6D, this.getZ(), 5, 0.3D, 0.3D, 0.3D, 0.0D); + } + } + + private static boolean isGift(ItemStack stack) { + return !stack.isEmpty() && giftValue(stack) > 0; + } + + private static int giftValue(ItemStack stack) { + if (stack.is(ModItems.XERTZ_QUARTZ.get())) { + return 3; + } + if (stack.is(ModItems.NEROSIUM_INGOT.get())) { + return 5; + } + if (stack.is(ModItems.ALIEN_FRAGMENT.get())) { + return 6; + } + if (stack.is(Items.EMERALD)) { + return 4; + } + return 0; + } + + // --- Reputation ----------------------------------------------------------- + + public int getReputation(Player player) { + return this.reputation.getOrDefault(player.getUUID(), 0); + } + + public int getTier(Player player) { + return Reputation.tier(getReputation(player)); + } + + public void addReputation(Player player, int amount) { + int value = Reputation.clamp(getReputation(player) + amount); + this.reputation.put(player.getUUID(), value); + refreshDisplayTier(); + if (this.tradingPlayer == player) { + this.offers = null; + } + } + + private void refreshDisplayTier() { + int best = 0; + for (int score : this.reputation.values()) { + best = Math.max(best, Reputation.tier(score)); + } + this.entityData.set(DATA_DISPLAY_TIER, best); + } + + public int getDisplayTier() { + return this.entityData.get(DATA_DISPLAY_TIER); + } + + private void rebuildOffers(Player player) { + int tier = player != null ? getTier(player) : 1; + this.offers = AlienTrades.forTier(Math.max(1, tier)); + } + + // --- Merchant ------------------------------------------------------------- + + @Override + public void setTradingPlayer(Player player) { + this.tradingPlayer = player; + if (player == null) { + this.offers = null; + } + } + + @Override + public Player getTradingPlayer() { + return this.tradingPlayer; + } + + @Override + public MerchantOffers getOffers() { + if (this.offers == null) { + rebuildOffers(this.tradingPlayer); + } + return this.offers; + } + + @Override + public void overrideOffers(MerchantOffers newOffers) { + this.offers = newOffers; + } + + @Override + public void notifyTrade(MerchantOffer offer) { + offer.increaseUses(); + this.villagerXp += Math.max(1, offer.getXp()); + if (this.tradingPlayer != null) { + addReputation(this.tradingPlayer, 1); + } + } + + @Override + public void notifyTradeUpdated(ItemStack stack) { + // No price-demand simulation in Phase 2. + } + + @Override + public int getVillagerXp() { + return this.villagerXp; + } + + @Override + public void overrideXp(int xp) { + this.villagerXp = xp; + } + + @Override + public boolean showProgressBar() { + return false; + } + + @Override + public SoundEvent getNotifyTradeSound() { + return SoundEvents.VILLAGER_YES; + } + + @Override + public boolean isClientSide() { + return this.level().isClientSide(); + } + + @Override + public boolean stillValid(Player player) { + return this.tradingPlayer == player && this.isAlive() && this.distanceToSqr(player) <= 100.0D; + } + + // --- Variant accessors ----------------------------------------------------- + + public Planet getPlanet() { + return Planet.byOrdinal(this.entityData.get(DATA_PLANET)); + } + + public void setPlanet(Planet planet) { + this.entityData.set(DATA_PLANET, planet.ordinal()); + } + + public String getBiomeId() { + return this.entityData.get(DATA_BIOME); + } + + public void setBiomeId(String id) { + this.entityData.set(DATA_BIOME, id); + } + + public int getColorSeed() { + return this.entityData.get(DATA_COLOR_SEED); + } + + public void setColorSeed(int seed) { + this.entityData.set(DATA_COLOR_SEED, seed); + } + + // --- Persistence ----------------------------------------------------------- + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + super.addAdditionalSaveData(output); + output.putInt("Planet", this.entityData.get(DATA_PLANET)); + output.putString("HomeBiome", getBiomeId()); + output.putInt("ColorSeed", getColorSeed()); + output.putBoolean("VariantAssigned", this.variantAssigned); + output.putInt("VillagerXp", this.villagerXp); + Map serial = new HashMap<>(); + this.reputation.forEach((uuid, score) -> serial.put(uuid.toString(), score)); + output.store("Reputation", REP_CODEC, serial); + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + super.readAdditionalSaveData(input); + this.entityData.set(DATA_PLANET, input.getIntOr("Planet", Planet.GREENXERTZ.ordinal())); + setBiomeId(input.getStringOr("HomeBiome", "")); + setColorSeed(input.getIntOr("ColorSeed", 0)); + this.variantAssigned = input.getBooleanOr("VariantAssigned", false); + this.villagerXp = input.getIntOr("VillagerXp", 0); + this.reputation.clear(); + Optional> stored = input.read("Reputation", REP_CODEC); + stored.ifPresent(map -> map.forEach((key, score) -> { + try { + this.reputation.put(UUID.fromString(key), score); + } catch (IllegalArgumentException ignored) { + // skip malformed UUID keys + } + })); + refreshDisplayTier(); + } + + // --- Sounds --------------------------------------------------------------- + + @Override + protected SoundEvent getAmbientSound() { + return SoundEvents.VILLAGER_AMBIENT; + } + + @Override + protected SoundEvent getHurtSound(DamageSource damageSource) { + return SoundEvents.VILLAGER_HURT; + } + + @Override + protected SoundEvent getDeathSound() { + return SoundEvents.VILLAGER_DEATH; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java index a58adbf..b8c0e84 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java @@ -5,6 +5,7 @@ import net.minecraft.world.entity.MobCategory; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.entity.AlienVillager; import za.co.neroland.nerospace.entity.CinderStalker; import za.co.neroland.nerospace.entity.EmberStrutter; import za.co.neroland.nerospace.entity.FrostStrider; @@ -74,6 +75,11 @@ public final class ModEntities { key -> EntityType.Builder.of(WoollyDrift::new, MobCategory.CREATURE) .sized(0.9F, 1.2F).eyeHeight(1.0F).clientTrackingRange(8).build(key)); + public static final RegistryEntry> ALIEN_VILLAGER = ENTITY_TYPES.register( + "alien_villager", + key -> EntityType.Builder.of(AlienVillager::new, MobCategory.CREATURE) + .sized(0.6F, 1.95F).eyeHeight(1.7F).clientTrackingRange(10).build(key)); + private ModEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java index ea627c0..fae21eb 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java @@ -4,6 +4,7 @@ import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.ai.attributes.AttributeSupplier; +import za.co.neroland.nerospace.entity.AlienVillager; import za.co.neroland.nerospace.entity.CinderStalker; import za.co.neroland.nerospace.entity.EmberStrutter; import za.co.neroland.nerospace.entity.FrostStrider; @@ -37,6 +38,7 @@ public static void forEach(Sink sink) { sink.accept(ModEntities.MEADOW_LOPER.get(), MeadowLoper.createAttributes()); sink.accept(ModEntities.EMBER_STRUTTER.get(), EmberStrutter.createAttributes()); sink.accept(ModEntities.WOOLLY_DRIFT.get(), WoollyDrift.createAttributes()); + sink.accept(ModEntities.ALIEN_VILLAGER.get(), AlienVillager.createAttributes()); } private ModEntityAttributes() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 2f60762..4baf269 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -92,6 +92,8 @@ public final class ModItems { public static final RegistryEntry FRAME_CASING = item("frame_casing"); public static final RegistryEntry GRAV_STRIDERS = item("grav_striders"); public static final RegistryEntry DRIFT_FLEECE = item("drift_fleece"); + /** Trade-only Artificer gear; ported as a plain item (its custom gear behaviour is deferred). */ + public static final RegistryEntry XERTZ_RESONATOR = item("xertz_resonator"); // --- Tool + armor materials -------------------------------------------- public static final ToolMaterial NEROSIUM_TOOL_MATERIAL = new ToolMaterial( @@ -182,7 +184,7 @@ public static Map, List> creativeTabItems NEROSIUM_DUST.get(), ALIEN_FRAGMENT.get(), ALIEN_TECH_SCRAP.get(), ALIEN_CORE.get(), ROCKET_FUEL_CANISTER.get(), FRAME_CASING.get(), GRAV_STRIDERS.get(), DRIFT_FLEECE.get()), CreativeModeTabs.TOOLS_AND_UTILITIES, - List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get()), + List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get()), CreativeModeTabs.COMBAT, List.of( OXYGEN_SUIT_HELMET.get(), OXYGEN_SUIT_CHESTPLATE.get(), OXYGEN_SUIT_LEGGINGS.get(), OXYGEN_SUIT_BOOTS.get(), diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/AlienTrades.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/AlienTrades.java new file mode 100644 index 0000000..b9da3fd --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/AlienTrades.java @@ -0,0 +1,72 @@ +package za.co.neroland.nerospace.village; + +import java.util.Optional; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.trading.ItemCost; +import net.minecraft.world.item.trading.MerchantOffer; +import net.minecraft.world.item.trading.MerchantOffers; +import net.minecraft.world.level.ItemLike; + +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Tier-gated trade tables for the Greenxertz alien villagers (ALIEN_VILLAGERS_DESIGN.md §6). Offers + * are cumulative: a tier-N villager offers everything unlocked at tiers 1..N. Spans universal + * materials, nerospace progression, rare alien goods, and — at the top tiers — the exclusive Artificer + * gear. Emeralds are the currency so the trades are useful in any modpack. + */ +public final class AlienTrades { + + private static final float PRICE_MULT = 0.05F; + + private AlienTrades() { + } + + public static MerchantOffers forTier(int tier) { + MerchantOffers offers = new MerchantOffers(); + + if (tier >= 1) { + offers.add(sell(ModItems.XERTZ_QUARTZ.get(), 12, Items.EMERALD, 1, 16, 1)); + offers.add(buy(Items.EMERALD, 1, Items.IRON_INGOT, 3, 16, 1)); + offers.add(buy(Items.EMERALD, 2, Items.BREAD, 6, 16, 1)); + } + if (tier >= 2) { + offers.add(buy(Items.EMERALD, 4, ModItems.NEROSIUM_INGOT.get(), 1, 12, 5)); + offers.add(buy2(Items.EMERALD, 1, ModItems.RAW_NEROSTEEL.get(), 8, + ModItems.NEROSTEEL_INGOT.get(), 4, 12, 5)); + offers.add(sell(ModItems.ALIEN_FRAGMENT.get(), 4, Items.EMERALD, 1, 12, 2)); + } + if (tier >= 3) { + offers.add(buy(Items.EMERALD, 8, Items.DIAMOND, 1, 6, 10)); + offers.add(buy(Items.EMERALD, 5, ModItems.ROCKET_FUEL_CANISTER.get(), 1, 8, 8)); + } + if (tier >= 4) { + offers.add(buy2(Items.EMERALD, 12, ModItems.ALIEN_TECH_SCRAP.get(), 2, + ModItems.ALIEN_CORE.get(), 1, 4, 15)); + // Exclusive Artificer gear (§6.1). + offers.add(buy(Items.EMERALD, 16, ModItems.XERTZ_RESONATOR.get(), 1, 2, 15)); + } + if (tier >= 5) { + offers.add(buy(Items.EMERALD, 18, Items.DIAMOND, 3, 4, 20)); + offers.add(buy2(ModItems.ALIEN_CORE.get(), 1, Items.EMERALD, 24, + ModItems.GRAV_STRIDERS.get(), 1, 2, 20)); + } + return offers; + } + + private static MerchantOffer buy(ItemLike cost, int n, ItemLike result, int rc, int maxUses, int xp) { + return new MerchantOffer(new ItemCost(cost, n), new ItemStack(result, rc), maxUses, xp, PRICE_MULT); + } + + private static MerchantOffer buy2(ItemLike costA, int a, ItemLike costB, int b, + ItemLike result, int rc, int maxUses, int xp) { + return new MerchantOffer(new ItemCost(costA, a), Optional.of(new ItemCost(costB, b)), + new ItemStack(result, rc), maxUses, xp, PRICE_MULT); + } + + private static MerchantOffer sell(ItemLike cost, int n, ItemLike result, int rc, int maxUses, int xp) { + return buy(cost, n, result, rc, maxUses, xp); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/Reputation.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/Reputation.java new file mode 100644 index 0000000..140c8a7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/Reputation.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.village; + +/** + * Reputation tier math for the Alien Villagers (ALIEN_VILLAGERS_DESIGN.md §3). A per-player score in + * {@code [0, MAX]} maps to 6 tiers (T0 Stranger → T5 Kin). Tiers gate trades now, and will gate + * teaching/structure access in later phases. + * + *

Interim note (Phase 2): reputation is stored per-villager (on the entity). When the Village Core + * block arrives (Phase 3/4) it will aggregate reputation per-village, as the design specifies. + */ +public final class Reputation { + + public static final int MAX = 100; + + /** Minimum score for each tier T0..T5. */ + private static final int[] TIER_MIN = {0, 10, 25, 45, 70, 95}; + + public static final int MAX_TIER = TIER_MIN.length - 1; + + private Reputation() { + } + + /** The tier (0..5) for a raw reputation score. */ + public static int tier(int score) { + int t = 0; + for (int i = 0; i < TIER_MIN.length; i++) { + if (score >= TIER_MIN[i]) { + t = i; + } + } + return t; + } + + public static int clamp(int score) { + return Math.max(0, Math.min(MAX, score)); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/xertz_resonator.json b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_resonator.json new file mode 100644 index 0000000..dee4024 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_resonator.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/xertz_resonator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index c0f134d..f3f1bc7 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -63,6 +63,8 @@ "entity.nerospace.meadow_loper": "Meadow Loper", "entity.nerospace.ember_strutter": "Ember Strutter", "entity.nerospace.woolly_drift": "Woolly Drift", + "entity.nerospace.alien_villager": "Alien Villager", + "item.nerospace.xertz_resonator": "Xertz Resonator", "gas.nerospace.empty": "Empty", "gas.nerospace.oxygen": "Oxygen", "item.nerospace.frame_casing": "Frame Casing", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_resonator.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_resonator.json new file mode 100644 index 0000000..040d1e1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_resonator.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/xertz_resonator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager.png new file mode 100644 index 0000000000000000000000000000000000000000..67160fd31288242aa57e190575ae0699dd4692a1 GIT binary patch literal 2205 zcmV;O2x9k%P)HF^YTzU}n-tW)defM8-I;$J&CuU~HhjTMC zd-Cv+nVDU`wrp?S9vP#poo(w4`t=>BCl4Rl*3NcgtvBe`&&P*zde6_WeSgO@FJ3-3 zGqWFm`q{pFe9w08Z#B+k@}kTt9*p$#&lmOc-eiB_GGpq7abE14zsGZN-F5MQ5d`u& z_;|FSgY!Us>P)A9V{{U&>k*(RdKeYN&v*`j#&u@QNRh95-U*Gcb7zxA4a!@H08}*6 z-ekXyf@mN&m^0j5A1?@q6xZW^TvIyu{ezE3((}kGi@rJnp8fIr$=+n&#-laKG}Z$* z#PR&SZLW{)*G_JX)*0?Vm`?Uyme6<{1_ltR6Ty_*-9ceVHuglq!N7kJcK+&nb2TH&(pI z0C|L{+7JtsG7d9_d*6ZMJEj)Cquj39c{pP#cI$l3)XAwO39+z{2?WWNE3k$e5k+av4sm&}c3)+~y>L4RQmP`Qt1*g5u& z9>n|12)G~bRthxM0zWq%t=aMXeQk&~U{N_`5X{#IT-AtDpI5Vn^XlAseD8z-H9JoM z)dhO}rN(EiFubEsWgOqn+VX#yFVuWu1`(j5!Hp29*Izn~hO>g_0yo#k4e-iK%-a|x zF?{hH+h^T^)r7cLMey!j2OF!uKYI$8okjx|nAk48MY5c17?X_82uc}Gr35Q@Xq>tT zM)G184G||nGqc}*cv2e!L1O4f1=M85K%w%b_Q(kDu^QA_-B>?~10U0X`#E-AiY&)n%#W=NB-X zt#wv6)=%10OJvOUv-z8yQKarNP^C^$(z}a{)$oy#YHe*4C=Ec&>v+G7o{XoNSvONi zc~2=(Iv4j;0Yn9rZjo0Od085mnN_7$;W@^PQkv*g`@%}hCOy^T+fddv@x9Z(F{&rQ zEVuEZsu!gk%_1Y7QQ_Vux|AMluVR9k(kwMp-6&0d475@f_e#YNBZ%o6@l=cnDv3rx zfTdxCom1cyaHa$5iR-aNM){3FDE=sl0!!gjYC&!41@dYeJsEJ^%lOs?DCKwQjH~sx`*pkshDI%L={LyS)?FEnG6&I9(9S8Ue$@-pno0#O7W+^Jdl9Q%qH`hO{X)v za${g-R(~?SDtFt|pf2$WRHyp0myTKt<)cI#MpiQe| z&#Ex9I~7`lJ(q}q_jODk9ID!SIx3H8=%1_rmeQs|M(VQDWIm(M!{G{5ScN%zj^LOx zSY1FsxSyp?Z*VpVW~~E(j`}P0`>4NCgU2XvgRsdgJ|o~v@ry*I>MRlC@!|aJFGI)u6xn=L`FA`IDKQr$96W8ss7qUa2-HzHDA#JR>d40W41)boBm*%byyN!9d3% zm;qLVQzTMW=vX^PzI7XHlc!E@41ZPy(CpkGS-z?st)^4O2j8*KMp^M}_uKEkkpLoC zvt|l7vEt{EGU~6`WVje5F;(8ZJ}jnSmI`Xz%_1jK#en)dhBz}wcF%_-MZ@6=?e)sz zs`}#|Msa4a&f|k;jlT^dA?OmJQOufNj}bGS&g|Xm!&=ccBQ>iFDgsn`MLvxmL=@O7 zS6A)I)m8c)z0JUzou>(=i$?%@PASZ)eSAm4V$KR>odU7KN1DuMcK7BTyLa;<)b|#aCTg@cGpksbzWO||SDz;f>o@}A z9kpkszc9Iyt{4JFFRHfZ`xxd3JVsA%&~N@-fKfJ$DTPX5%48swc(OBbtt7wh0_&Aq0l$6M3V@@eJyb-O0WfE&L{k zMqon-Bj(Ypcp_MTZUeq;ab+@5p9;OhS_uOYl~H$<@svTB(xqHSW2s?^)ra1I{3?tg z9KFKwIupPYZ)@Cmp4~y&7!_@cH|+u{k6CDDg%Izd@HPrF`3NKf%fLr|3x6C{q0+YP zup-7lG2o@M+5M;oGl+6dH)Ga@Q3m89hAp*4l_snuW|4q;b#apkWVxFeu5=&yMA@aY zC1;fMMp+g1Z#7gP6Gp07p$r6&wY*Zj!S2IS5%0vi6*6Y6tgWX}QC2XS5{#G1ebiI6 zKPEGtbK_Rb`%wYrJ*G&eL{vnDJv$fWDUUKEs7}M8y34%Hyi{sA%F@V(-HklhT9%S= zjis1s+rHlTQS_oc)rF{B#&n8zP|3I+-?LOuwSze)MsmDU;X*vj3KsxnI*7D;VV zf`zw|8_zM&k$0EnxN1qIFwZ&$YgyTQX;0NCTrcrzbOH2!scJK&l(nM*rOM5CMsRKF fOT>WSBhP;UKEuSw2#L+*00000NkvXXu0mjf0a`X@ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_cindara.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_cindara.png new file mode 100644 index 0000000000000000000000000000000000000000..27a03fb553288ab238cd6aedaf6436047468aed5 GIT binary patch literal 2209 zcmV;S2wwMzP)Dn!hpV>;M#-?`uWzVo}59tiK=-=A~NcmBU56X#EzJTx;K z?jM+$*~KeY&CKlB@e}s!=@VmgbK|=8W+v-9P8Y9SwVNB)8*9Cp$@+P?e?af~8Mg25 zc;?RLrkR<2bNNfVys~T;Zf`ZtW%8oTDjtmV%k{78=bi1XvCE998^(FDbN(LB#dX)k ze%wr;=O{=RY6*Hk_-P+6X0#N653v{yU#?dae%HN?`!t;XHRgGFj( zW;R$^t-XBg_z7x+SO*3xtF^cN6X~P(-;*GjXINygdf?>Xj}A^#`AimbN)^EkR#qFu z&nb2TH&(pI0C|L{+7JtsG7d9_d*6ZMJEj)Cquh?!D4a1BJoJ72*zpr~D$~X4Wi< zy_w0eIY8w;o?++MJ9-fBGb7-Byjv;ISPT5zU}e?zUc9Ug(FQCkrwoGm8iA`CQR?$* z)^J`ES3mynkO4ItrGV-J-RY^urxU)TP-Pt7&)V{TnJ?6QVg?bQqQQ+2sXIMogO$~~ z;JLu%we<#gc#iF}Zoz6o+^ZsZ_pXDD)!&~z1K#&;vQ2{lXF;J*{sXa2nd#nacoIiE) zFcvgV1JxDKU}hY&*_$-^{m;MB*<4lUcn&GMm#xS9zE&--try2&1vsWdG~#gopaDK3 zR^3ZyvDIa%<>!%nZQ}f?lZS1pB{F9F+5FAUC{lMBs8XjW>D@ubYWT=VwYD}2lm;N? zb-dq3PsY>Ctdl9Eyr+~Xor`;_0HT6Qx5z7tyetjO%&JnW@El`CDNS^$ePJbLlb-7F zZ76G-_&%}N>(-NCmfLty)r(S&W|0xksBmu+T}ltOS24j%X_gwQZj>fJ23jeLd!^!s z5ybS3cq+yOl|-W;z|t_n&MEK;IMV_3#P!%Bqx{Ao6n`m-0!!gjYC&!41@dYeJsEJ^ z%lOs?DCKwQjH~sx`*pkshDI%L={LyS)?FEnG6&I9(9S8Ue$@-%;YEnl;YoB zyCwmdne9D#XuG?Q?StOCW@hy#Z7Y(RQSh0i7>iajNC7M$ZE1J+v0TUW!J(?Hrz1#AL;qw2u#`3x^H5ft_8vW? z&ngO3n6qc}R^)-_vQTdm#V(^#;1%fZ{6hWSonNTIV-&bS*kl%;5pbsX+eD@6ED_^y z|6ugj;rJ5|MT&;lpM0l&cJMI@&>+fVDA(6nWGWsk4ODwXSz~hm%TxDs^#07j-wlH> z(Ea|L^@h(=02$#FiG)QI??#y_#XGoAYAJ3Ee^v$1Y-EruUo(%f=~VH-cPz9~Ry^BT z`}7kDAc8e}lLENLil0YHcYeY8BuZkdiUIX^3~^?V?4A!v ziu(OI+3T~K!AMnpyo0B|@W^HNB37^1;6~qSUh)0G2*DUnpMAr|}CB z1$K6J)@EmC>3j4x18+7;6HFJ60Q8(v7&}8L%(0lWLRqIktniWc9zC=(XV2N0v*+qH zd}sHZXB#T@Y&IjXXi*WN3jA>YVAKYc_qy{7_VcZ#alZTfGYQ5G>F2ZW3JO3}9^bPs zQIQwkS1DDQje|e3xMW8bm&RINBz!-zxMYl13A`(s6+Q+)eQ#lDqDE^ovxw9o@}A9kpkszc9Iyt{4JFFRHfZ`)Z=}v-l0M@pl16*)*mUDupSNfmGtj&cwBn z{JIOUuay}?6#O3+xZ*c^WSvyny|JctWt)vg)P;&fi6)lCK z&Z0og=b2}-BE{}TnHa`wvcn>pb(M&z9)vLDa5zB$_>6@m-irz$FifAw8$FI^P><|R z_Qj~9Jc$ZsQ(C--Ja7-!(ZFrMw=J$rChAk6cUUW7Afhtrt}>o72vfS0>u4-BOtJdV zo0-fb0KLNUIupPYZ)@Cmp4~y&7!_@cH|+u{k6CDDg%Izd@HPrF`3NKf%fLr|V}Be~ zq0+YPup-7lG2o@M+5M;oGl+6dCu7!zQ3m8~3|nf8Dot2R%pw8x>fk05$Z|I`T3aCe@tdP=f)95R+U-m zut;i?5-hxx+<1KI61~^M+l&wbfXgU0AH2j}8y$Ip<;f{*FAG z8zVC_`}W8C_Tfh#+r7JY8hqJ(@y;qTM!Nm|{rY)lcdvDr33bP~EXL>WkuR>hF8&jZ zKv~BxTPoBtP@dxHUA}U4620pgpeRNd6~xcThla*==FCV@u1DSpov^hzrdfmcwK4z| zjkL47S7$+VkUPu;j@H*&7etEdaX+pp9sK<9OC5QXm1SR@0l)w8=VWJh&sJBKrO=26 z?uchk9@}Vr&F*~rLxb1XR6R1IvO2|#+1wb}@k_J8Iqrz9&2i&wl)*B!GBaCUS+1je zVKAUhh&ZsivRp^oKat+M^no;zMTTVt!viNbesGRnPw!$usiJYKE6WY=bBf)-ofXe9 zgFHhNHbkIO&SB2*=)2+gj-`d~cyHhAG@Y>&JoSD3!eC%ud~?TU=H}})NB*chyOdEW zJ(<()-Mu64!c2-8g!}G@YG-$^VGtva`qSfMo0*$8cbZuf6lWI}TXTS_edJ+$>>VSB z=b005Kc20W(TD}kx4N=y&z?N49nr;DR8Bbr>opp#Frw7w)vV#NdS8F{(}Wpnc3J|e z3(U;TH$I*49WPbR@%^kV|Cjkf%_nA%0V*5Z36W;z=52LlxduGHaJ0VGFkVH8MH{mu zrZ4ibeby})Cd9oe8t>6{V6+`CqXl_uRa^q&OjqE^`io6GGj)e@}>632+uJLs$w+VD0IZ?%5r1fX9aW&lJBWe zH;ZhEFv1F4alO?AM(bj*o0*%pUmyHjY=I2jOT5_XveI&XBwy=Y z+rB;Nf|e*ao81+vBab3=mKjy*6eYd;C|C_28L8IRMS;=)#G;PpyXdJ(ub(NTqNkK7 z@x?s_fT*C-Ey~I=FDnBxv#Qi8yvCSQN)w#|IR=K9fvU%Mp%S>p_ul0zSL;bIt8F}} z@S;?sS!P6@(nzgj;Jw*|d31PingQ_wrZg)Jg&U>Gj~T6$#k~^vVFodMqdgT9f=aSp zdX`~?@hRh#;i{iUJ#js@$asI0)A*$*URX+>QVZ&W7bvT1_GE_RUM9D8K&ie;eUZObL8ec#;_(>sr+}Ym>~0PJLWZ^&LO6I%UcsWyYiO zxYxDCsdKnqmB1te5e1NnvP?mWcQT`x;Zc`}^a>|t7Zy)5Kq>y|ctqEoUc5GYw7+lf zzJJNgto~$jRqb}cpf0^%@nxml&je)Abb7SEZ%f0WJ=))I?JW(5_K;vpl%HWhmsZD~ z6)>|i6?%j{mxvkf>sUTGRCSGXR36LFKN$d)%BDg=>af#`*Zg^DIHU@zG-uDzI2H_s z3uqATXQeZ{@MaRsS_c|BJ$3p%J#}h4W`R3|O=j^K4bBulOIE7R;-BuMes&pigd6?N z+h)&Sojkko(Glp7vrKp;Y*2F9yuf5eYRv(xPTlAj{pYWmnZb;XAeb4f2&YJ-4Cq)p zN4a$y>{6#r?hJfp?ye$8$a;DOws1X|GoiSLly>n8nOtP8BnRMU~O? zRP%=s0;6{aUuQjC8HhZJU$sy+RyRt3rR?;S*;~Dn=YQUC#&?zKUFplu$1UZ%v_OR+ z3{x1!IbU4@%7_=Q&BhyJ8*iK{ith>sN+79>i)(5|V3^R=c;~4>FpFTUYpijVF>x)X zJ)@WbI36?V@+FGNQ$#V~;`x)GtmWbR#q%ehC@V8QDw+Xb{ETO^XLP3G?>+f@GATvh zsY@GF*`zYTGeD)U6Z@tYi8-j$3eC*=pIjTa7?>bbC<@AIF7CJg|Ez5&8)tDK%AA#D z5nFG48)PYjjzBOe4Jpc4#lzB;QPu_-V4|M03yUXz9mdO}kj%5$i28r_vrH@LgLbHU z>NXfX#ZoQ`!=XjhEAD3?#m>e%v5eSchajp-7F+7k;X(7?+Axicvt5}es%`$#;oWH-n0v-I%cVv0U@5l%eyGd?ngs1W0~<$ zUh9veDpk6+9R^~|C}wzxH#;BoU=C5i>1V>a2+9mOi(^Y|QI!d6iCHF~UVYqT7qZ&T z99KGza^l@3-cm41dgEOc_HQ**pb$n1tndyrAZvLgyut3nN)gY*vlR+vt*mRLQBek% zObI4S)jsN}+8?_!^0{*>7X7FIiyl)XQz9y&(w_0f`&2}k6I7>RS>0#RW>G4&9OY@0 z!_Gz-Y%MFvxW-CMwQXN-{Fm5Sd#YceY8lHZoda2U z*IQWOoE;FWx>d7Oo{dUCOzXktYten4BM~yRl#e3 zm)#9gx??se1ngV{QwV+#Cn4Enn1LsigrtJ?aU}u?N(rhB000000MBUf32Lb-?~kE< zu@m=X*(C1G(AsdjskD8ZZ_LCiPpGdwDkzAi0d&5(do*Ep1ObME)P@BD7W1LfWO`*Y6u&i|L!&V}n2kIc*t z$NOex_SLNoGc!9kf7YHnduohs+_`B}bJO)5r>|~p*o`|k8*5W@)AjS=c%R<$Gi=}A z@yz|rduC>~{>@#xeC=ak>h@bHs0*&j;n2{o1_q-DtVPm*XqXy+o zL;xxpX=``8j)G_)H<&YATUwnE5Gk(5{kW!d@cSbhOV1;(Ec)sQ_;vTUqpjU-TUlI| zOk+K8Lma$3u(hRCyZg&#LCV`||$%I%sRhcl*vhrX|$n?Gw;)<3o0Y_DGP${&?y zLK&69lNs&d>H{eYBPm7@?zC(oW*Z?9v!OMf%5N*Jsa>^i>uMxPa5v4w_W)0`n`RMDbM+~Uh zaSEs|(3|ZwKArF#g(~Cte%6-%%Y32c6Elba6%B5LNWIyftt>9r17mwOhOJvOUv-z8yQKarNP^C^$(z}a{)$oy#YHe*4C=Ec&>v+G7o{XoNSvONi zc~2=(Iv4j;0Yn9rZjo0Od085mnN_7$;W@^PQkv*g`@%}hCOy^T+fddv@x3#D>3ls2 zX1R?QRlO+XXcigqj0*QQ(WUfYdleJRlxC@+>PBhuW1y9?xK}EE7(qdfKq;!&Zw%J z@nF0ZXa%NJhe|Rlmd`94nE+J?nG*P{>PZGZ=C#Ui)+U(|ow~T7@;iQJdCHVS%D|)Y zxYxGDse8Cym5NDLL{x!Pltl_sl*vFb;8B-Y=~bPWnwvh30Hyf7&D#=?nc3c}kv-lU z*?S+nYi3q|GQKKz+ti>oyFzCzBbN&%5Yg>`jjDpWB z#aOhOK?-02X-kjyMsgj~2ZyS*o{k_f4gHf9z*5>&%tKjm+Iuyk&ngO3n6qc}R^)-_ zvQTdm#V(^#;1%e>Ouv2~%=ByU7zJ(+Hkrj|1e_^;lBiUjC1N}r?;rm<9RI~bk)k2q z{^y0Uq0~~`82+pZpxLoOvV6@v#->xn2j8*KMp^M}_meL!O8^n9*_#x= zHCFsQQU)`9+Z#+qNvs*qAH9(I&cH-KcrJp>A}3PCfciUzI5S9g&xa&M3xmGw^;yke zq$)q&LEfEj|N6f1+aMBxE)g2Vtm$A{dxM$2{kZd8WB>C9pGh!oNI#!_S5N?=^7x*8iHf}NzDlXe zY!dwN^w5Tl4~_9Efp}Qcy(g$%UIexZ^mcmeHQK07Y%(GdM zVt1oV3}ZIgVG+%`O2kwTLKt#5oS*=F#=;WsMTHO;rcdOJ9>+7NM|LOsV$@ZhLW<*?E)%~S!ia35bvSzHVQNO2qXi`z(;-)KaQ$UY1?*K5o4ej z@Y31re$;~*L^-FMF>Avp19B3>mfE696V?*5NI<>1xXA>v+|3MEx{rLK?9$niGfH}+ ztP1Z#fvlNrys zaVzHir~vaGQzTO&Dx$)kos066N0|{+r(sduW!`39DzzMCY2?H1MjmV}OUby#QcSgN zUvKK#DL%< a&wl~7hq$$ij(t%80000oc{0qpL1p6 zAyvWB^jq~Qvs9QDz4Q4kzchzAQTw)l3CnWsZGZNEnXhw}2ME&4^!_gfO6R9NcG@NW qV8LTH$L Date: Sat, 20 Jun 2026 16:43:12 +0200 Subject: [PATCH 33/82] Add planet dimensions, biomes, and ore data Port planetary content to datapacks and expose dimension keys in code. Adds dimension JSONs (greenxertz, cindara, glacira, station), a custom dimension_type/space.json, biome definitions (with mob spawners and ore feature references), configured and placed worldgen features for four ores, and a ModDimensions Java class that provides ResourceKey constants (Level/LevelStem) for use by code (rocket travel, villager variant). Also updates MULTILOADER_MIGRATION.md to document the port and next steps. --- docs/MULTILOADER_MIGRATION.md | 29 ++++++--- .../nerospace/registry/ModDimensions.java | 43 ++++++++++++ .../data/nerospace/dimension/cindara.json | 11 ++++ .../data/nerospace/dimension/glacira.json | 11 ++++ .../data/nerospace/dimension/greenxertz.json | 11 ++++ .../data/nerospace/dimension/station.json | 12 ++++ .../data/nerospace/dimension_type/space.json | 19 ++++++ .../nerospace/worldgen/biome/cindara.json | 50 ++++++++++++++ .../nerospace/worldgen/biome/glacira.json | 50 ++++++++++++++ .../nerospace/worldgen/biome/greenxertz.json | 65 +++++++++++++++++++ .../configured_feature/cindrite_ore.json | 27 ++++++++ .../configured_feature/glacite_ore.json | 27 ++++++++ .../configured_feature/nerosteel_ore.json | 27 ++++++++ .../configured_feature/xertz_quartz_ore.json | 27 ++++++++ .../placed_feature/cindrite_ore_placed.json | 27 ++++++++ .../placed_feature/glacite_ore_placed.json | 27 ++++++++ .../placed_feature/nerosteel_ore_placed.json | 27 ++++++++ .../xertz_quartz_ore_placed.json | 27 ++++++++ 18 files changed, 509 insertions(+), 8 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDimensions.java create mode 100644 multiloader/common/src/main/resources/data/nerospace/dimension/cindara.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/dimension/glacira.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/dimension/greenxertz.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/dimension/station.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/dimension_type/space.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/cindara.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/glacira.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/greenxertz.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/cindrite_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/glacite_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosteel_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/xertz_quartz_ore.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/cindrite_ore_placed.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/glacite_ore_placed.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosteel_ore_placed.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/xertz_quartz_ore_placed.json diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index 5814371..b0b869c 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -39,6 +39,19 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > end-to-end: generator → pipe → oxygen generator → pipe → gas tank. (The world oxygen-field effect + > HUD + the generator GUI are a deferred atmosphere subsystem.) > +> **2026-06-20 (later still): PLANET DIMENSIONS ported** — all 4 cells green. In 26.x dimensions are +> pure datapack data, which loads identically on both loaders with no Java registration: copied +> `dimension/{greenxertz,cindara,glacira,station}.json`, the custom `dimension_type/space.json` (END +> starfield, no sun — used by cindara/glacira/station; greenxertz keeps the vanilla overworld type), +> the three planet `worldgen/biome/*.json`, and the ore configured/placed features they reference. The +> planet biomes already list the **ported mobs as natural spawners** and the **ported ores as features**, +> so the worlds populate themselves. `ModDimensions` (common) holds the `ResourceKey`s for code (rocket +> travel, villager variant). The Greenxertz biome's structure features (hamlet/ruin/mega_city) were +> stripped — they belong to the deferred **structures** subsystem. **Caveat (runtime, can't verify +> headlessly):** the `space` dimension_type JSON was authored for 26.1.2; if 26.2 changed that schema the +> space dimensions may need a 26.2 variant (greenxertz, on the vanilla type, is unaffected). Mob +> spawn-placement rules (ground/light) remain deferred — spawning uses the biome lists + vanilla defaults. +> > **2026-06-20 (later still): ALL 10 mobs ported** — all 4 cells green. On the entity seam below, added > `cinder_stalker`, `frost_strider`, `ruin_warden`, the three terraform livestock (`meadow_loper`, > `ember_strutter`, `woolly_drift` via a shared `TerraformLivestock` base), and the **alien villager** @@ -72,14 +85,14 @@ and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after e > capability (extract-only) so the pipe network drains it. Root's tiered sun-tracking array + BER are > a deferred enhancement. > -> Remaining, by subsystem (rough size): **dimensions** (Greenxertz/Cindara/Glacira biomes+dims+travel; -> unblocks the planet ores' worldgen, mob natural-spawning, and the villager's per-planet variant); -> **rockets** (items, tiers, launch logic); **quarry** (area mining); **structures** -> (station/village/meteor cores + events); **atmosphere/terraforming** (oxygen field, terraformer, -> monitor, hydration); **solar panel tiers/array/BER** (single-tier base is done); **star guide** -> (progression UI); **creative item/fluid/gas stores** (infinite-resource config — marginal); plus mob -> **spawn eggs + natural-spawn rules** (deferred with dimensions). **All 10 mobs are otherwise ported.** -> Recommended order: dimensions → rockets → quarry → structures → atmosphere → the rest. +> Remaining, by subsystem (rough size): **rockets** (items, tiers, launch + the dimension-travel +> mechanic into the now-ported planets); **quarry** (area mining + fake-player); **structures** +> (station/village/meteor cores + the stripped hamlet/ruin/mega_city features); **atmosphere/ +> terraforming** (oxygen field, terraformer, monitor, hydration); **meteor events**; **solar panel +> tiers/array/BER** (single-tier base is done); **star guide** (progression UI); **creative +> item/fluid/gas stores** (marginal); plus polish — mob **spawn-placement/eggs** and the **villager's +> per-planet variant** (re-enable now that `ModDimensions` exists). The planets, all 10 mobs, and the +> full machine/logistics stack are ported. Recommended order: rockets → quarry → structures → atmosphere → the rest. --- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDimensions.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDimensions.java new file mode 100644 index 0000000..54eabe8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDimensions.java @@ -0,0 +1,43 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.dimension.LevelStem; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * The nerospace planet dimensions, ported cross-loader. In 26.x the dimensions themselves are pure + * datapack data — {@code data/nerospace/dimension/*.json} (+ the custom {@code dimension_type/space.json} + * and {@code worldgen/biome/*.json}), which load identically on NeoForge and Fabric with no Java + * registration. This class only holds the {@link ResourceKey}s code uses to address/teleport into them + * (the rocket travel mechanic, the alien villager's per-planet variant, …). The datagen bootstrap that + * authored the JSON is not needed at runtime, so it is intentionally omitted here. + */ +public final class ModDimensions { + + public static final ResourceKey GREENXERTZ_STEM = stem("greenxertz"); + public static final ResourceKey GREENXERTZ_LEVEL = level("greenxertz"); + + public static final ResourceKey CINDARA_STEM = stem("cindara"); + public static final ResourceKey CINDARA_LEVEL = level("cindara"); + + public static final ResourceKey GLACIRA_STEM = stem("glacira"); + public static final ResourceKey GLACIRA_LEVEL = level("glacira"); + + public static final ResourceKey STATION_STEM = stem("station"); + public static final ResourceKey STATION_LEVEL = level("station"); + + private static ResourceKey stem(String name) { + return ResourceKey.create(Registries.LEVEL_STEM, Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, name)); + } + + private static ResourceKey level(String name) { + return ResourceKey.create(Registries.DIMENSION, Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, name)); + } + + private ModDimensions() { + } +} diff --git a/multiloader/common/src/main/resources/data/nerospace/dimension/cindara.json b/multiloader/common/src/main/resources/data/nerospace/dimension/cindara.json new file mode 100644 index 0000000..0db8f75 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/dimension/cindara.json @@ -0,0 +1,11 @@ +{ + "type": "nerospace:space", + "generator": { + "type": "minecraft:noise", + "biome_source": { + "type": "minecraft:fixed", + "biome": "nerospace:cindara" + }, + "settings": "minecraft:overworld" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/dimension/glacira.json b/multiloader/common/src/main/resources/data/nerospace/dimension/glacira.json new file mode 100644 index 0000000..ab99487 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/dimension/glacira.json @@ -0,0 +1,11 @@ +{ + "type": "nerospace:space", + "generator": { + "type": "minecraft:noise", + "biome_source": { + "type": "minecraft:fixed", + "biome": "nerospace:glacira" + }, + "settings": "minecraft:overworld" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/dimension/greenxertz.json b/multiloader/common/src/main/resources/data/nerospace/dimension/greenxertz.json new file mode 100644 index 0000000..ae835a4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/dimension/greenxertz.json @@ -0,0 +1,11 @@ +{ + "type": "minecraft:overworld", + "generator": { + "type": "minecraft:noise", + "biome_source": { + "type": "minecraft:fixed", + "biome": "nerospace:greenxertz" + }, + "settings": "minecraft:overworld" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/dimension/station.json b/multiloader/common/src/main/resources/data/nerospace/dimension/station.json new file mode 100644 index 0000000..1e9e79e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/dimension/station.json @@ -0,0 +1,12 @@ +{ + "type": "nerospace:space", + "generator": { + "type": "minecraft:flat", + "settings": { + "biome": "minecraft:the_void", + "features": false, + "lakes": false, + "layers": [] + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/dimension_type/space.json b/multiloader/common/src/main/resources/data/nerospace/dimension_type/space.json new file mode 100644 index 0000000..45d5be0 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/dimension_type/space.json @@ -0,0 +1,19 @@ +{ + "ambient_light": 0.0, + "coordinate_scale": 1.0, + "has_ceiling": false, + "has_ender_dragon_fight": false, + "has_fixed_time": true, + "has_skylight": true, + "height": 384, + "infiniburn": "#minecraft:infiniburn_overworld", + "logical_height": 384, + "min_y": -64, + "monster_spawn_block_light_limit": 0, + "monster_spawn_light_level": { + "type": "minecraft:uniform", + "max_inclusive": 7, + "min_inclusive": 0 + }, + "skybox": "end" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/cindara.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/cindara.json new file mode 100644 index 0000000..bc6b608 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/cindara.json @@ -0,0 +1,50 @@ +{ + "carvers": [ + "minecraft:cave", + "minecraft:cave_extra_underground" + ], + "downfall": 0.0, + "effects": { + "foliage_color": "#5a3a2a", + "grass_color": "#4a3a33", + "water_color": "#70402a" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "nerospace:cindrite_ore_placed" + ] + ], + "has_precipitation": false, + "spawn_costs": {}, + "spawners": { + "ambient": [], + "axolotls": [], + "creature": [ + { + "type": "nerospace:alien_villager", + "maxCount": 2, + "minCount": 1, + "weight": 3 + } + ], + "misc": [], + "monster": [ + { + "type": "nerospace:cinder_stalker", + "maxCount": 2, + "minCount": 1, + "weight": 14 + } + ], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": 2.0 +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/glacira.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/glacira.json new file mode 100644 index 0000000..6719e32 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/glacira.json @@ -0,0 +1,50 @@ +{ + "carvers": [ + "minecraft:cave", + "minecraft:cave_extra_underground" + ], + "downfall": 0.0, + "effects": { + "foliage_color": "#a8d8e8", + "grass_color": "#c8e8f0", + "water_color": "#77c8e8" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "nerospace:glacite_ore_placed" + ] + ], + "has_precipitation": false, + "spawn_costs": {}, + "spawners": { + "ambient": [], + "axolotls": [], + "creature": [ + { + "type": "nerospace:alien_villager", + "maxCount": 2, + "minCount": 1, + "weight": 3 + } + ], + "misc": [], + "monster": [ + { + "type": "nerospace:frost_strider", + "maxCount": 2, + "minCount": 1, + "weight": 14 + } + ], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": -0.5 +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/greenxertz.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/greenxertz.json new file mode 100644 index 0000000..727494b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/greenxertz.json @@ -0,0 +1,65 @@ +{ + "carvers": [ + "minecraft:cave", + "minecraft:cave_extra_underground", + "minecraft:canyon" + ], + "downfall": 0.0, + "effects": { + "foliage_color": "#4fb85a", + "grass_color": "#5bd46a", + "water_color": "#3a8e63" + }, + "features": [ + [], + [], + [], + [], + [], + [], + [ + "nerospace:nerosteel_ore_placed", + "nerospace:xertz_quartz_ore_placed" + ] + ], + "has_precipitation": false, + "spawn_costs": {}, + "spawners": { + "ambient": [ + { + "type": "nerospace:greenling", + "maxCount": 4, + "minCount": 2, + "weight": 8 + } + ], + "axolotls": [], + "creature": [ + { + "type": "nerospace:quartz_crawler", + "maxCount": 3, + "minCount": 1, + "weight": 10 + }, + { + "type": "nerospace:alien_villager", + "maxCount": 3, + "minCount": 1, + "weight": 6 + } + ], + "misc": [], + "monster": [ + { + "type": "nerospace:xertz_stalker", + "maxCount": 2, + "minCount": 1, + "weight": 12 + } + ], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": 0.8 +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/cindrite_ore.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/cindrite_ore.json new file mode 100644 index 0000000..08c3c39 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/cindrite_ore.json @@ -0,0 +1,27 @@ +{ + "type": "minecraft:ore", + "config": { + "discard_chance_on_air_exposure": 0.0, + "size": 8, + "targets": [ + { + "state": { + "Name": "nerospace:cindrite_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + }, + { + "state": { + "Name": "nerospace:cindrite_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + } + ] + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/glacite_ore.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/glacite_ore.json new file mode 100644 index 0000000..ba1b7c1 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/glacite_ore.json @@ -0,0 +1,27 @@ +{ + "type": "minecraft:ore", + "config": { + "discard_chance_on_air_exposure": 0.0, + "size": 8, + "targets": [ + { + "state": { + "Name": "nerospace:glacite_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + }, + { + "state": { + "Name": "nerospace:glacite_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + } + ] + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosteel_ore.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosteel_ore.json new file mode 100644 index 0000000..1d28650 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/nerosteel_ore.json @@ -0,0 +1,27 @@ +{ + "type": "minecraft:ore", + "config": { + "discard_chance_on_air_exposure": 0.0, + "size": 9, + "targets": [ + { + "state": { + "Name": "nerospace:nerosteel_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + }, + { + "state": { + "Name": "nerospace:nerosteel_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + } + ] + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/xertz_quartz_ore.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/xertz_quartz_ore.json new file mode 100644 index 0000000..b84a3e5 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/xertz_quartz_ore.json @@ -0,0 +1,27 @@ +{ + "type": "minecraft:ore", + "config": { + "discard_chance_on_air_exposure": 0.0, + "size": 14, + "targets": [ + { + "state": { + "Name": "nerospace:xertz_quartz_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:stone_ore_replaceables" + } + }, + { + "state": { + "Name": "nerospace:xertz_quartz_ore" + }, + "target": { + "predicate_type": "minecraft:tag_match", + "tag": "minecraft:deepslate_ore_replaceables" + } + } + ] + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/cindrite_ore_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/cindrite_ore_placed.json new file mode 100644 index 0000000..7c96ad2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/cindrite_ore_placed.json @@ -0,0 +1,27 @@ +{ + "feature": "nerospace:cindrite_ore", + "placement": [ + { + "type": "minecraft:count", + "count": 7 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "max_inclusive": { + "absolute": 48 + }, + "min_inclusive": { + "absolute": -48 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/glacite_ore_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/glacite_ore_placed.json new file mode 100644 index 0000000..3b7b5b4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/glacite_ore_placed.json @@ -0,0 +1,27 @@ +{ + "feature": "nerospace:glacite_ore", + "placement": [ + { + "type": "minecraft:count", + "count": 7 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "max_inclusive": { + "absolute": 48 + }, + "min_inclusive": { + "absolute": -48 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosteel_ore_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosteel_ore_placed.json new file mode 100644 index 0000000..f457482 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/nerosteel_ore_placed.json @@ -0,0 +1,27 @@ +{ + "feature": "nerospace:nerosteel_ore", + "placement": [ + { + "type": "minecraft:count", + "count": 10 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:trapezoid", + "max_inclusive": { + "absolute": 72 + }, + "min_inclusive": { + "absolute": -32 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/xertz_quartz_ore_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/xertz_quartz_ore_placed.json new file mode 100644 index 0000000..e15fef4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/xertz_quartz_ore_placed.json @@ -0,0 +1,27 @@ +{ + "feature": "nerospace:xertz_quartz_ore", + "placement": [ + { + "type": "minecraft:count", + "count": 12 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:height_range", + "height": { + "type": "minecraft:uniform", + "max_inclusive": { + "absolute": 110 + }, + "min_inclusive": { + "absolute": 0 + } + } + }, + { + "type": "minecraft:biome" + } + ] +} \ No newline at end of file From 3ef734cc3c7b1e9b956e324079d101cdb9f02d3f Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:10:24 +0200 Subject: [PATCH 34/82] Create MULTILOADER_PORT_CHECKLIST.md --- docs/MULTILOADER_PORT_CHECKLIST.md | 156 +++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 docs/MULTILOADER_PORT_CHECKLIST.md diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md new file mode 100644 index 0000000..dc79d81 --- /dev/null +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -0,0 +1,156 @@ +# Nerospace multiloader — port checklist + +Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the +cross-loader `multiloader/` project. As of this audit: **85 classes ported, 179 remaining**, all four +build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. + +Legend: `[x]` done · `[~]` partial / simplified · `[ ]` not started. +Risk = how much is loader-coupled or **runtime-only-verifiable** (rendering / world / behaviour can't be +checked by a headless build). + +--- + +## ✅ Done (cross-loader, all 4 cells green) + +- [x] **Platform seams** — `Services`/`IPlatformHelper`, `RegistrationProvider` (+ per-loader factories), + capability seams for item / energy / fluid / gas (expose + query), `FluidFactory` seam. +- [x] **Registries** — blocks, items, block-entities, menu types, entities, sounds, dimension keys, + entity attributes (subset that's ported). +- [x] **Logistics** — energy / fluid / gas / **item** transport; the universal pipe relays all four. +- [x] **Machines / storage** — combustion + passive + solar generators, oxygen generator, nerosium + grinder (+ 3 GUIs), item store, battery, creative battery, fluid tank, gas tank, trash can. +- [x] **Rocket-fuel fluid** — `BaseFlowingFluid`/`FluidType` (NeoForge) vs hand-written `FlowingFluid` + (Fabric), liquid block + bucket; NeoForge in-world render. (Fabric in-world render = follow-up.) +- [x] **All 10 mobs** — xertz stalker, quartz crawler, greenling, ruin warden, cinder/frost striders, + 3 terraform livestock, alien villager (full Merchant trading + reputation). Models, renderers, + glow layers, sounds, `village` trade tables. +- [x] **Planet dimensions** — Greenxertz / Cindara / Glacira / Station (datapack data + `space` + dimension_type + planet biomes that spawn the mobs and generate the ores). +- [x] **Overworld nerosium ore** worldgen (NeoForge biome modifier + Fabric biome API). + +--- + +## 🚧 Remaining subsystems + +### Rockets & travel (`rocket/` 11 + client + items) — **scoped, next** +- [ ] `RocketTier`, `Destinations` (port; inline `Tuning` values). +- [ ] `RocketEntity` — **rewrite, not copy**: root is NeoForge-`transfer`-coupled (`RocketFuelTank`, + `ItemStacksResourceHandler`) and pulls in `Tuning` + `ModCriteria`. Rebuild on the cross-loader + `FluidTank` + vanilla `ServerPlayer.teleportTo`. Risk: **high (travel/teleport unverifiable headlessly)**. +- [ ] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen` (destination selector + fuel gauge). +- [ ] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer`, `RocketRenderState`; entity + item textures. +- [ ] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). +- [ ] `StationCoreBlock`(+BE), `StationRegistry` (multi-station slots) — depends on **structures** + data attachments. + +### Quarry (`machine/quarry/` 11 + client) +- [ ] Area miner: controller block/BE + menu/screen, frame + landmark blocks/BE, `QuarryRegion`, + `MinerTier`, `OutputFilter`, `PlanetMiningProfile`, `QuarryChunkLoader`. Risk: **high** (chunk-loading, + fake-player-style mining, multiblock). Chunk-loading needs a cross-loader seam (NeoForge ticket API vs Fabric). + +### Fuel machines (`machine/Fuel*` — depends on the ported rocket-fuel fluid) +- [ ] `FuelTankBlock`(+BE +menu), `FuelRefineryBlock`(+BE +menu) + their screens — refine inputs → rocket fuel, + pump into a docked rocket. Rebuild on cross-loader `FluidTank` (root uses NeoForge transfer). + +### Atmosphere / terraforming (`world/Oxygen*`, `world/Terraform*`, `machine/Terraform*`, `HydrationModule`) +- [ ] Oxygen field (airless-dimension survival): `OxygenField`, `OxygenFieldManager`, `OxygenFieldEvents`, + oxygen HUD + air-bubble suppression, suit checks. Needs the **networking seam** (sync to client). +- [ ] Terraformer + Terraform Monitor + Hydration Module (blocks/BE/menus/screens), `TerraformManager`, + `TerraformConversion`, `TerraformDrift`, `TerraformFauna`, `TerraformChunkLoader`, `TerraformResources`, + `GreenxertzAtmosphere`, terraformed biomes. Risk: **high** (world mutation, chunk-loading, events). + +### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) +- [ ] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + their + configured/placed-feature JSON (the 3 features I **stripped** from the Greenxertz biome) + structure data. +- [ ] `VillageCoreBlock`(+BE) — per-village reputation aggregation. + +### Meteor events (`meteor/` 8 + client) +- [ ] `FallingMeteorEntity` (+ model/renderer/state), `MeteorCallerItem`, `MeteorCoreBlock`(+BE), + `MeteorEventManager`, `MeteorEvents`, `MeteorSite`, `MeteorLoot`. Needs networking seam (impact sync). + +### Star Guide / progression (`progression/` 5 + client + item) +- [ ] `StarGuide`, `StarGuideProgress`, `StarGuideBlock`(+BE), `StarGuideMenu` + screen, hologram BER, + `StarGuideBookItem`. Progression-tracking UI. + +### Pipes — advanced (`pipe/` 4 + items + payload + renderer; basic pipe already ported) +- [ ] `PipeNetwork`, `TravellingItem`, `PipeIoMode`, `PipeResourceType`, `PipeFilterItem`, + `PipeUpgradeItem`, `SetPipeModePayload`, `UniversalPipeRenderer` (streams + travelling-item visuals, + per-side I/O modes, filters). Needs networking seam. + +### Machine modules / upgrades (`module/` 3) +- [ ] `MachineModules`, `ModuleType`, `UpgradeModuleItem` — speed/efficiency upgrade items for machines. + +### Solar — tiers/array/BER (`solar/` 4; single-tier base **done**) +- [~] `SolarTier`, `SolarArray` (multi-panel pooling), the root tiered block/BE + sun-tracking BER. + +### Creative storage variants (`storage/Creative*`) +- [ ] `CreativeItemStore`, `CreativeFluidTank`, `CreativeGasTank` (+ `AbstractStorageBlock`) — infinite + configurable sources. Marginal (creative-only). + +### Utility items (`item/`) +- [ ] `ConfiguratorItem`, `DestinationCompassItem`, `GreenxertzNavigatorItem`, `PipeFilterItem`, + `PipeUpgradeItem`, `StarGuideBookItem`, `NerospaceSpawnEggItem` (+ **spawn eggs** for all mobs). +- [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. + +### Cross-cutting registries (`registry/`) +- [ ] `ModDataComponents`, `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge + attachments vs Fabric component/attachment API), `ModCriteria` (advancement triggers), `ModTags`, + `ModFeatures`, `ModConfiguredFeatures`/`ModPlacedFeatures`/`ModBiomes`/`ModBiomeModifiers` (datagen + bootstraps — mostly superseded by the copied JSON), `ModCreativeModeTabs` (a dedicated mod tab; we + currently inject into vanilla tabs), `ModDimensionTypes` (space type — JSON already copied). + +### Networking (`network/` 5) — **needed by oxygen HUD, meteors, pipe modes** +- [ ] Cross-loader packet seam: NeoForge `PayloadRegistrar` vs Fabric networking API. `ModNetwork`, + `ModPayloads`, `OxygenFieldSyncPayload`, `MeteorSyncPayload`, `SetPipeModePayload`. + +### Commands & compat +- [ ] `command/NerospaceCommands` — `/nerospace` debug/admin commands (vanilla Brigadier; loader event differs). +- [ ] `compat/jei/*` — recipe-viewer integration. NeoForge = JEI; Fabric would use REI/EMI. Cross-mod, low priority. + +### Config / tuning +- [ ] `Config` + `Tuning` — NeoForge `ModConfigSpec`-based; needs a cross-loader config seam (or a simple + shared config). Many ported machines currently use inlined constants where the root reads `Tuning`. + +### Spawn rules +- [ ] `entity/ModEntityEvents` — natural-spawn placement rules (ground/light) + a cross-loader spawn-placement + seam (NeoForge `RegisterSpawnPlacementsEvent` vs Fabric). Mobs currently spawn via biome lists + vanilla defaults. + +--- + +## 📡 Sentry / telemetry (`telemetry/` 3 + `sentry_test` block) — **POPIA/GDPR-sensitive** +- [ ] `NerospaceTelemetry` — the Sentry client: captures Nerospace exceptions/crashes, with **PII + scrubbing, de-dup, rate-limiting** and an active/opt-in gate. +- [ ] `SentryLogAppender` — Log4j2 appender that selects ERROR/FATAL events touching Nerospace code. +- [ ] `SentryTestBlock` — a debug block that forces a captured error. + +**Compliance gate (per project preference — POPIA + GDPR):** before porting, confirm telemetry is +**opt-in** (off by default), transmits **no personal data** (scrub usernames, UUIDs, IPs, file paths, +world names), documents what's collected + retention, and offers a clear off switch. Verify the Sentry +DSN/endpoint and data-processing terms meet POPIA (SA) + GDPR (EU). Re-confirm the scrubbing covers +log paths like `C:\Users\\...`. Do **not** port as-is until this is signed off. + +--- + +## 🛠️ Tools / sync engines (`tools/`) — currently target the **root** mod only +These are dev-time generators, not shipped code. They write to the root's `src/main/resources` paths, so +they must be pointed at (or duplicated for) `multiloader/common/src/main/resources` to drive the +multiloader's assets instead of the current copy-from-root approach. + +- [ ] `model_sync.py` — **entity-model sync engine** (Blockbench `.bbmodel` ⇄ Java `LayerDefinition`, + Y-flip, mtime-directional). Wire to the multiloader's `client/*Model.java` + `art/blockbench/entity`. +- [ ] `gen_textures.py` — procedural 16×16 texture generator (additive). Repoint output dir. +- [ ] `gen_bbmodels.py` — Blockbench source generator for block/item textures. Repoint. +- [ ] `gen_logo.py` — CurseForge logo + in-game mods-list icon. Repoint / re-emit per loader. +- [ ] `check_assets.py` — "every model resolves" validator. Repoint at the multiloader resource roots. +- [ ] `render_contact_sheets.py` / `render_entity_previews.py` — QA atlases. Repoint. +- [x] `gradle-mcp` (server.js) — the agent build server; already used to verify all 4 cells. +- [x] `fix_markdown.py` / `markdown_check` — docs linting; loader-agnostic. + +> Note: so far the multiloader reuses the root's already-generated JSON/textures by copying them. The +> tools only need porting if the multiloader becomes the source of truth (i.e. when the root mod is retired). + +--- + +## Recommended order +rockets → fuel machines → quarry → atmosphere/terraforming → structures → meteor events → star guide → +advanced pipes → modules → networking seam (unblocks oxygen HUD / meteors / pipe modes) → config seam → +spawn rules → telemetry (after compliance sign-off) → creative variants / utility items / JEI → tools repoint. From e24e8e31e911bf92c71eef5a689cf2b5d73e0bd9 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 17:49:04 +0200 Subject: [PATCH 35/82] Add rocket entity, renderer, UI & assets Port core rockets into the multiloader: add RocketEntity, RocketItem (4 tiers), RocketLaunchPadBlock, LaunchGantryBlock, LaunchPadMultiblock, Destinations, RocketMenu and the client-side pieces (RocketModel, RocketT2/T3/T4Model, RocketRenderer, RocketRenderState, RocketScreen, SpaceButton, TexturedContainerScreen). Register the rocket renderer, update registries (blocks/items/entities/menus), and include all related assets and data (textures, item/block models, blockstates, loot tables, recipes, lang entries). Update migration/checklist docs to note the port and deferred work (station founding and automation proxy). Also trim .vscode launch configs and apply small build/client setup tweaks for Fabric/NeoForge. --- docs/MULTILOADER_MIGRATION.md | 14 + docs/MULTILOADER_PORT_CHECKLIST.md | 40 +- multiloader/.vscode/launch.json | 44 -- .../client/ClientEntityRenderers.java | 2 + .../nerospace/client/RocketModel.java | 64 ++ .../nerospace/client/RocketRenderState.java | 18 + .../nerospace/client/RocketRenderer.java | 80 +++ .../nerospace/client/RocketScreen.java | 128 ++++ .../nerospace/client/RocketT2Model.java | 56 ++ .../nerospace/client/RocketT3Model.java | 71 ++ .../nerospace/client/RocketT4Model.java | 56 ++ .../nerospace/client/SpaceButton.java | 47 ++ .../client/TexturedContainerScreen.java | 110 ++++ .../nerospace/registry/ModBlocks.java | 13 + .../nerospace/registry/ModEntities.java | 6 + .../neroland/nerospace/registry/ModItems.java | 19 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../nerospace/rocket/Destinations.java | 29 + .../nerospace/rocket/LaunchGantryBlock.java | 41 ++ .../nerospace/rocket/LaunchPadMultiblock.java | 233 +++++++ .../nerospace/rocket/RocketEntity.java | 610 ++++++++++++++++++ .../neroland/nerospace/rocket/RocketItem.java | 99 +++ .../rocket/RocketLaunchPadBlock.java | 75 +++ .../neroland/nerospace/rocket/RocketMenu.java | 186 ++++++ .../neroland/nerospace/rocket/RocketTier.java | 92 +++ .../nerospace/blockstates/launch_gantry.json | 7 + .../blockstates/rocket_launch_pad.json | 7 + .../assets/nerospace/items/launch_gantry.json | 6 + .../nerospace/items/rocket_launch_pad.json | 6 + .../assets/nerospace/items/rocket_tier_1.json | 6 + .../assets/nerospace/items/rocket_tier_2.json | 6 + .../assets/nerospace/items/rocket_tier_3.json | 6 + .../assets/nerospace/items/rocket_tier_4.json | 6 + .../assets/nerospace/lang/en_us.json | 169 ++--- .../nerospace/models/block/launch_gantry.json | 201 ++++++ .../models/block/rocket_launch_pad.json | 40 ++ .../nerospace/models/item/rocket_tier_1.json | 6 + .../nerospace/models/item/rocket_tier_2.json | 6 + .../nerospace/models/item/rocket_tier_3.json | 6 + .../nerospace/models/item/rocket_tier_4.json | 6 + .../textures/block/launch_gantry.png | Bin 0 -> 391 bytes .../textures/block/launch_gantry_top.png | Bin 0 -> 436 bytes .../textures/block/rocket_launch_pad.png | Bin 0 -> 465 bytes .../nerospace/textures/entity/rocket_t1.png | Bin 0 -> 2521 bytes .../nerospace/textures/entity/rocket_t2.png | Bin 0 -> 2789 bytes .../nerospace/textures/entity/rocket_t3.png | Bin 0 -> 3065 bytes .../nerospace/textures/entity/rocket_t4.png | Bin 0 -> 3327 bytes .../assets/nerospace/textures/gui/rocket.png | Bin 0 -> 1604 bytes .../nerospace/textures/item/rocket_tier_1.png | Bin 0 -> 218 bytes .../nerospace/textures/item/rocket_tier_2.png | Bin 0 -> 251 bytes .../nerospace/textures/item/rocket_tier_3.png | Bin 0 -> 272 bytes .../nerospace/textures/item/rocket_tier_4.png | Bin 0 -> 250 bytes .../loot_table/blocks/launch_gantry.json | 21 + .../loot_table/blocks/rocket_launch_pad.json | 21 + .../data/nerospace/recipe/launch_gantry.json | 17 + .../nerospace/recipe/rocket_launch_pad.json | 16 + .../data/nerospace/recipe/rocket_tier_1.json | 17 + .../data/nerospace/recipe/rocket_tier_2.json | 18 + .../data/nerospace/recipe/rocket_tier_3.json | 19 + .../data/nerospace/recipe/rocket_tier_4.json | 19 + multiloader/fabric/build.gradle | 15 + .../fabric/NerospaceFabricClient.java | 2 + multiloader/neoforge/build.gradle | 9 + .../neoforge/NeoForgeClientSetup.java | 2 + wiki/Block-of-Glacite.md | 1 - 65 files changed, 2670 insertions(+), 129 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderState.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderer.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT2Model.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT3Model.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT4Model.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/SpaceButton.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/Destinations.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchGantryBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchPadMultiblock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketLaunchPadBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/launch_gantry.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_launch_pad.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/launch_gantry.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_launch_pad.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_1.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_2.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_3.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_4.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/launch_gantry.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_launch_pad.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_1.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_2.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_3.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_4.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry_top.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_launch_pad.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t1.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t2.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t3.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t4.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/rocket.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_1.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_2.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_3.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_4.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/launch_gantry.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/rocket_launch_pad.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/launch_gantry.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/rocket_launch_pad.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_1.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_2.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_3.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_4.json diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md index b0b869c..9dae1a0 100644 --- a/docs/MULTILOADER_MIGRATION.md +++ b/docs/MULTILOADER_MIGRATION.md @@ -8,6 +8,20 @@ API divergences found during the port. Last updated: 2026-06-20. Verified build targets: all four cells — **NeoForge @ 26.1.2 / 26.2** and **Fabric @ 26.1.2 / 26.2** — `BUILD SUCCESSFUL` via the gradle MCP after every batch. +> **2026-06-20 (later still): ROCKETS (core) ported** — all 4 cells green. The rideable +> `RocketEntity` (tiers, fuelling, destination selection, simulated ascent → vanilla teleport), the +> 4 tier `RocketItem`s, the `RocketLaunchPadBlock`/`LaunchGantryBlock` multiblock (`LaunchPadMultiblock` +> gating), the non-extended `RocketMenu`/`RocketScreen`, and the per-tier `RocketModel`/`RocketRenderer` +> (baked directly — no model-layer registry). Cross-loader rewrites off the NeoForge transfer API: fuel +> on the shared `FluidTank`, intake a plain `SimpleContainer(1)`, menu opened via vanilla +> `openMenu(MenuProvider)` with the entity ref held server-side only. Dropped the NeoForge-only +> `shouldRiderSit()` override (no vanilla/common equivalent). Assets copied (textures, item/block models, +> blockstates, loot, recipes) + 25 lang keys. **Deferred to own batches:** the multi-station founding +> system (StationCore/StationRegistry/charter/`founded_station` criterion → data-attachment + criteria +> seams + structures) and the pipe/hopper auto-fuel **proxy** into a docked rocket (entity item-cap seam). +> Recommended next: **fuel machines** (Fuel Tank + Fuel Refinery → rebuild on `FluidTank` + `EnergyBuffer` +> + plain `SimpleContainer` slots; no `MachineItemHandler` exists in the multiloader). + > **2026-06-20 progress update.** Every cross-loader **platform mechanism is now built and > verified**: registration; the item / energy / fluid capability seams (both *expose* and > *query*); block-entity tickers; and menus + screens. A working **energy network** exists diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index dc79d81..74308d7 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,24 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **85 classes ported, 179 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~102 classes ported, ~162 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-20 update — rockets (core) ported.** All 4 cells green. Added 17 classes: +> `rocket/{RocketTier, Destinations, LaunchPadMultiblock, RocketLaunchPadBlock, LaunchGantryBlock, +> RocketItem, RocketEntity, RocketMenu}` + `client/{RocketModel, RocketT2/T3/T4Model, RocketRenderState, +> RocketRenderer, TexturedContainerScreen, SpaceButton, RocketScreen}`, registered the entity / menu / +> blocks / 4 tier items, and copied the rocket assets (entity+item+block textures, GUI, item/block models, +> blockstates, loot, recipes, 25 lang keys). **Cross-loader rewrites:** fuel store on the shared +> `FluidTank` (not NeoForge transfer); intake is a plain `SimpleContainer(1)`; the menu is **non-extended** +> (rocket ref server-side only, client reads synced `ContainerData`, opened via vanilla +> `openMenu(MenuProvider)`); the renderer **bakes each tier layer directly** (no model-layer registry); +> dropped the NeoForge-only `shouldRiderSit()` override. **Deferred** (own batches): the multi-station +> founding system (`StationCoreBlock`+BE, `StationRegistry`, Station Charter, `founded_station` criterion — +> needs data-attachment + criteria seams + structures) and the pipe/hopper **automation proxy** that feeds +> fuel into a docked rocket (needs the entity item-capability seam). Runtime behaviour (travel/teleport, +> rendering) is unverifiable headlessly — compile-verified on all 4 cells only. + Legend: `[x]` done · `[~]` partial / simplified · `[ ]` not started. Risk = how much is loader-coupled or **runtime-only-verifiable** (rendering / world / behaviour can't be checked by a headless build). @@ -32,15 +47,20 @@ checked by a headless build). ## 🚧 Remaining subsystems -### Rockets & travel (`rocket/` 11 + client + items) — **scoped, next** -- [ ] `RocketTier`, `Destinations` (port; inline `Tuning` values). -- [ ] `RocketEntity` — **rewrite, not copy**: root is NeoForge-`transfer`-coupled (`RocketFuelTank`, - `ItemStacksResourceHandler`) and pulls in `Tuning` + `ModCriteria`. Rebuild on the cross-loader - `FluidTank` + vanilla `ServerPlayer.teleportTo`. Risk: **high (travel/teleport unverifiable headlessly)**. -- [ ] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen` (destination selector + fuel gauge). -- [ ] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer`, `RocketRenderState`; entity + item textures. -- [ ] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). -- [ ] `StationCoreBlock`(+BE), `StationRegistry` (multi-station slots) — depends on **structures** + data attachments. +### Rockets & travel (`rocket/` 11 + client + items) — **core DONE (4 cells green); station-founding deferred** +- [x] `RocketTier`, `Destinations` (ported; `Tuning` values inlined as identity-multiplier base values). +- [~] `RocketEntity` — rebuilt on the cross-loader `FluidTank` + a plain `SimpleContainer(1)` intake + + vanilla `ServerPlayer.teleportTo`. **Deferred:** the NeoForge-transfer entity item-capability + **automation proxy** (pipe/hopper → docked rocket) and the multi-station selection. Risk: travel/teleport + unverifiable headlessly — compile-verified only. +- [x] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen` (destination selector + fuel gauge). Menu is + **non-extended** (no loader-divergent extended-menu API); the station/FOUND rows are deferred. +- [x] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer` (bakes each tier layer directly — no + model-layer registry), `RocketRenderState`; entity + item textures copied. +- [x] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). +- [ ] `StationCoreBlock`(+BE), `StationRegistry` (multi-station slots), Station Charter, `founded_station` + criterion — **deferred**: needs the data-attachment + criteria seams (+ structures). The Orbital Station + destination currently docks the rider at the shared origin platform. ### Quarry (`machine/quarry/` 11 + client) - [ ] Area miner: controller block/BE + menu/screen, frame + landmark blocks/BE, `QuarryRegion`, diff --git a/multiloader/.vscode/launch.json b/multiloader/.vscode/launch.json index cea1058..de2e32f 100644 --- a/multiloader/.vscode/launch.json +++ b/multiloader/.vscode/launch.json @@ -121,50 +121,6 @@ "projectName": "neoforge", "preLaunchTask": "ml-prepare-neoforge-server-26.2", "shortenCommandLine": "none" - }, - { - "type": "java", - "request": "launch", - "name": "neoforge - Client", - "presentation": { - "group": "Mod Development - neoforge", - "order": 0 - }, - "projectName": "neoforge", - "mainClass": "net.neoforged.devlaunch.Main", - "args": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunProgramArgs.txt" - ], - "vmArgs": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\clientRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" - ], - "cwd": "${workspaceFolder}\\neoforge\\runs\\client", - "env": {}, - "console": "internalConsole", - "shortenCommandLine": "none" - }, - { - "type": "java", - "request": "launch", - "name": "neoforge - Server", - "presentation": { - "group": "Mod Development - neoforge", - "order": 1 - }, - "projectName": "neoforge", - "mainClass": "net.neoforged.devlaunch.Main", - "args": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunProgramArgs.txt" - ], - "vmArgs": [ - "@C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\build\\moddev\\serverRunVmArgs.txt", - "-Dfml.modFolders\u003dnerospace%%C:\\Users\\dario\\Documents\\Github\\nerospace\\multiloader\\neoforge\\bin\\main" - ], - "cwd": "${workspaceFolder}\\neoforge\\runs\\server", - "env": {}, - "console": "internalConsole", - "shortenCommandLine": "none" } ] } \ No newline at end of file diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java index 5f0a6db..8fe5e74 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java @@ -51,6 +51,8 @@ public static void registerAll(Sink sink) { new WoollyDriftModel(WoollyDriftModel.createBodyLayer().bakeRoot()), tex("woolly_drift"), 1.0F, 1.0F, 1.0F, 0.5F, glow("woolly_drift"))); sink.register(ModEntities.ALIEN_VILLAGER.get(), AlienVillagerRenderer::new); + + sink.register(ModEntities.ROCKET.get(), RocketRenderer::new); } private static Identifier tex(String name) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketModel.java new file mode 100644 index 0000000..6e90fc0 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketModel.java @@ -0,0 +1,64 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * A simple rocket model: a tall cylindrical body, a nose cone, and four tail fins. Built with the + * 26.1 {@code LayerDefinition} mesh API; parts use the standard entity-model convention (feet at the + * part origin, body drawn upward via negative Y) so the shared render transform stands it upright. + * + *

Cross-loader port note: the renderer bakes each tier's {@code createBodyLayer()} directly (the + * Greenxertz-mob pattern), so no model-layer registry is needed on either loader.

+ */ +public class RocketModel extends EntityModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "rocket"), "main"); + + public RocketModel(ModelPart root) { + super(root); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, -16F, -6F, 12F, 36F, 12F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("nose", + CubeListBuilder.create().texOffs(0, 56).addBox(-4F, -24F, -4F, 8F, 8F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("console", + CubeListBuilder.create().texOffs(44, 68).addBox(-4F, -1.5F, -5.5F, 8F, 5F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("bell", + CubeListBuilder.create().texOffs(0, 80).addBox(-4F, 20F, -4F, 8F, 3F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_north", + CubeListBuilder.create().texOffs(64, 0).addBox(-1F, 14F, -10F, 2F, 10F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_south", + CubeListBuilder.create().texOffs(64, 0).addBox(-1F, 14F, 6F, 2F, 10F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_west", + CubeListBuilder.create().texOffs(64, 0).addBox(-10F, 14F, -1F, 4F, 10F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_east", + CubeListBuilder.create().texOffs(64, 0).addBox(6F, 14F, -1F, 4F, 10F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + + return LayerDefinition.create(mesh, 128, 128); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderState.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderState.java new file mode 100644 index 0000000..61265bb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderState.java @@ -0,0 +1,18 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** Render state for the rocket: per-tier visual scale + texture (cockpit rework). */ +public class RocketRenderState extends EntityRenderState { + + /** Per-tier hull scale (see {@code RocketEntity.visualScale}). */ + public float scale = 1.6F; + /** Per-tier hull texture. */ + public Identifier texture = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/rocket_t1.png"); + /** Tier ordinal — picks the per-tier geometry. */ + public int tier; +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderer.java new file mode 100644 index 0000000..444d81a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketRenderer.java @@ -0,0 +1,80 @@ +package za.co.neroland.nerospace.client; + +import com.mojang.blaze3d.vertex.PoseStack; + +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.rocket.RocketEntity; + +/** + * Entity renderer for the rocket. Renders {@link RocketModel} via the 26.1 submit pipeline with a + * PER-TIER scale and texture: bigger tiers genuinely look bigger, and each tier carries its accent + * livery. The window band is punched out of the texture (alpha cutout), so the standing rider can see + * out of the hull. + * + *

Cross-loader port note: each tier's geometry is baked directly from its {@code createBodyLayer()} + * (the same approach the Greenxertz mob renderers use), so no model-layer registry is required.

+ */ +public class RocketRenderer extends EntityRenderer { + + private static final Identifier[] TEXTURES = { + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/rocket_t1.png"), + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/rocket_t2.png"), + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/rocket_t3.png"), + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/rocket_t4.png"), + }; + private static final int FULL_BRIGHT = 0x00F000F0; + + /** Per-tier geometry: T1 classic, T2 boosters, T3 ring, T4 heavy. */ + private final RocketModel[] models; + + public RocketRenderer(EntityRendererProvider.Context context) { + super(context); + this.models = new RocketModel[] { + new RocketModel(RocketModel.createBodyLayer().bakeRoot()), + new RocketModel(RocketT2Model.createBodyLayer().bakeRoot()), + new RocketModel(RocketT3Model.createBodyLayer().bakeRoot()), + new RocketModel(RocketT4Model.createBodyLayer().bakeRoot()), + }; + } + + @Override + public RocketRenderState createRenderState() { + return new RocketRenderState(); + } + + @Override + public void extractRenderState(RocketEntity rocket, RocketRenderState state, float partialTick) { + super.extractRenderState(rocket, state, partialTick); + state.scale = rocket.visualScale(); + state.tier = Math.min(TEXTURES.length - 1, rocket.getTier().ordinal()); + state.texture = TEXTURES[state.tier]; + } + + @Override + public void submit(RocketRenderState state, PoseStack poseStack, SubmitNodeCollector collector, + CameraRenderState cameraState) { + poseStack.pushPose(); + // Standard entity-model orientation: flip Y/X into model space at the tier's scale. The model + // bottom (fins) sits at model-y 24 = 1.5, so -1.5 plants the fins on the pad at any scale. + float s = state.scale; + poseStack.scale(-s, -s, s); + poseStack.translate(0.0F, -1.5F, 0.0F); + + RocketModel model = this.models[Math.min(this.models.length - 1, state.tier)]; + model.setupAnim(state); + RenderType renderType = model.renderType(state.texture); + collector.order(0).submitModel(model, state, poseStack, renderType, + FULL_BRIGHT, OverlayTexture.NO_OVERLAY, -1, null, 0, null); + + poseStack.popPose(); + super.submit(state, poseStack, collector, cameraState); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java new file mode 100644 index 0000000..e59e330 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java @@ -0,0 +1,128 @@ +package za.co.neroland.nerospace.client; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.rocket.Destinations; +import za.co.neroland.nerospace.rocket.RocketMenu; +import za.co.neroland.nerospace.rocket.RocketTier; + +/** + * The interactive in-rocket UI: a sci-fi panel with a fuel gauge (intake slot beside it), an + * interactive trajectory row (one {@link SpaceButton} per reachable planet, the chosen one lit), and + * a Launch button. Built from custom widgets + gauges drawn on the hull panel (26.1 render model). + * + *

Cross-loader port note: the multi-station founding row (a station cycler + the FOUND node) is + * deferred with that subsystem, so this screen shows the planet trajectory + launch only. Destination + * buttons are built for the full destination order and enabled per the live (synced) tier.

+ */ +public class RocketScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/rocket.png"); + private static final int ACCENT = 0xFFE0506A; // rocket red + private static final int FUEL = 0xFFF0703C; // fuel orange-red + + /** The full destination order (a prefix of which is reachable per tier). */ + private static final List> ALL_DESTINATIONS = RocketTier.TIER_4.destinations(); + + private SpaceButton launchButton; + private final List destinationButtons = new ArrayList<>(); + + public RocketScreen(RocketMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 166); + this.titleLabelX = 10; + this.inventoryLabelX = 10; + this.inventoryLabelY = 10_000; // hide the redundant inventory label + } + + @Override + protected void init() { + super.init(); + this.destinationButtons.clear(); + + // Planet row: one node per destination in the global order; enabled per the live tier. + int x = this.leftPos + 28; + for (int i = 0; i < ALL_DESTINATIONS.size(); i++) { + final int index = i; + SpaceButton node = new SpaceButton(x, this.topPos + 36, 34, 14, + Component.literal(shortName(Destinations.name(ALL_DESTINATIONS.get(i)))), ACCENT, + b -> onSelectDestination(index)); + this.addRenderableWidget(node); + this.destinationButtons.add(node); + x += 36; + } + + this.launchButton = new SpaceButton(this.leftPos + 8, this.topPos + 68, 160, 14, + Component.translatable("gui.nerospace.rocket.launch"), ACCENT, b -> onLaunch()); + this.addRenderableWidget(this.launchButton); + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int pct = this.menu.getFuelPercent(); + label(g, Component.literal("Fuel: " + pct + "% " + this.menu.getFuel() + " / " + this.menu.getCapacity() + " mB"), + 8, 17, 0xFFFFC9B0); + fluidGauge(g, 8, 27, 130, 5, pct / 100f, FUEL); + label(g, Component.literal("PAD >"), 8, 39, 0xFF9FB4C8); + + int reachable = this.menu.getTier().destinations().size(); + int selected = this.menu.getDestinationIndex(); + for (int i = 0; i < this.destinationButtons.size(); i++) { + SpaceButton node = this.destinationButtons.get(i); + node.active = i < reachable && reachable > 1; + node.visible = i < reachable; + node.setSelected(i == selected); + } + if (this.launchButton != null) { + this.launchButton.active = this.menu.isLaunchable(); + } + + // A dotted trajectory arc from the pad to the selected node. + if (selected >= 0 && selected < this.destinationButtons.size()) { + SpaceButton target = this.destinationButtons.get(selected); + int x0 = this.leftPos + 26; + int y0 = this.topPos + 45; + int x1 = target.getX() + target.getWidth() / 2; + int y1 = target.getY(); + int segments = 16; + for (int i = 0; i <= segments; i++) { + float t = i / (float) segments; + int ax = Math.round(x0 + (x1 - x0) * t); + int ay = Math.round(y0 + (y1 - y0) * t - Mth.sin(t * Mth.PI) * 11.0F); + g.fill(ax, ay, ax + 2, ay + 2, ACCENT); + } + } + } + + private static String shortName(String full) { + return switch (full) { + case "Orbital Station" -> "Station"; + case "Greenxertz" -> "Xertz"; + default -> full; + }; + } + + private void onSelectDestination(int index) { + sendButton(RocketMenu.SELECT_DEST_BASE + index); + } + + private void onLaunch() { + sendButton(RocketMenu.BUTTON_LAUNCH); + } + + private void sendButton(int id) { + if (this.minecraft != null && this.minecraft.gameMode != null) { + this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, id); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT2Model.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT2Model.java new file mode 100644 index 0000000..59e9196 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT2Model.java @@ -0,0 +1,56 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Tier 2 rocket geometry: the classic hull plus twin side boosters. Geometry-only holder; the + * renderer bakes this layer and feeds it to the shared {@link RocketModel} class. + */ +public final class RocketT2Model { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "rocket_t2"), "main"); + + private RocketT2Model() { + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, -16F, -6F, 12F, 36F, 12F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("nose", + CubeListBuilder.create().texOffs(0, 56).addBox(-4F, -24F, -4F, 8F, 8F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("console", + CubeListBuilder.create().texOffs(44, 68).addBox(-4F, -1.5F, -5.5F, 8F, 5F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("bell", + CubeListBuilder.create().texOffs(0, 80).addBox(-4F, 20F, -4F, 8F, 3F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("booster_w", + CubeListBuilder.create().texOffs(80, 0).addBox(-10F, 2F, -2.5F, 4F, 18F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("booster_e", + CubeListBuilder.create().texOffs(80, 0).addBox(6F, 2F, -2.5F, 4F, 18F, 5F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_north", + CubeListBuilder.create().texOffs(64, 0).addBox(-1F, 14F, -10F, 2F, 10F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_south", + CubeListBuilder.create().texOffs(64, 0).addBox(-1F, 14F, 6F, 2F, 10F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + + return LayerDefinition.create(mesh, 128, 128); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT3Model.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT3Model.java new file mode 100644 index 0000000..348b77e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT3Model.java @@ -0,0 +1,71 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Tier 3 rocket geometry: a stretched nose over a four-slab ring skirt — the sleek long-range + * silhouette. Geometry-only holder, rendered via {@link RocketModel}. + */ +public final class RocketT3Model { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "rocket_t3"), "main"); + + private RocketT3Model() { + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, -16F, -6F, 12F, 36F, 12F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("nose", + CubeListBuilder.create().texOffs(0, 56).addBox(-4F, -30F, -4F, 8F, 14F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("tip", + CubeListBuilder.create().texOffs(44, 56).addBox(-2F, -34F, -2F, 4F, 4F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("console", + CubeListBuilder.create().texOffs(44, 68).addBox(-4F, -1.5F, -5.5F, 8F, 5F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("bell", + CubeListBuilder.create().texOffs(0, 80).addBox(-4F, 20F, -4F, 8F, 3F, 8F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("skirt_n", + CubeListBuilder.create().texOffs(64, 32).addBox(-7F, 12F, -7F, 14F, 4F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("skirt_s", + CubeListBuilder.create().texOffs(64, 32).addBox(-7F, 12F, 5F, 14F, 4F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("skirt_w", + CubeListBuilder.create().texOffs(64, 48).addBox(-7F, 12F, -5F, 2F, 4F, 10F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("skirt_e", + CubeListBuilder.create().texOffs(64, 48).addBox(5F, 12F, -5F, 2F, 4F, 10F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_north", + CubeListBuilder.create().texOffs(64, 0).addBox(-1F, 14F, -10F, 2F, 10F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_south", + CubeListBuilder.create().texOffs(64, 0).addBox(-1F, 14F, 6F, 2F, 10F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_west", + CubeListBuilder.create().texOffs(64, 0).addBox(-10F, 14F, -1F, 4F, 10F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("fin_east", + CubeListBuilder.create().texOffs(64, 0).addBox(6F, 14F, -1F, 4F, 10F, 2F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + + return LayerDefinition.create(mesh, 128, 128); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT4Model.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT4Model.java new file mode 100644 index 0000000..1ce4b4d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketT4Model.java @@ -0,0 +1,56 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Tier 4 rocket geometry: the heavy — a widened core with FOUR strap-on boosters, built for the + * Heavy Launch Complex. Geometry-only holder, rendered via {@link RocketModel}. + */ +public final class RocketT4Model { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "rocket_t4"), "main"); + + private RocketT4Model() { + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + root.addOrReplaceChild("body", + CubeListBuilder.create().texOffs(0, 0).addBox(-7F, -16F, -7F, 14F, 36F, 14F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("nose", + CubeListBuilder.create().texOffs(0, 56).addBox(-5F, -26F, -5F, 10F, 10F, 10F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("console", + CubeListBuilder.create().texOffs(44, 68).addBox(-4F, -1.5F, -6.5F, 8F, 5F, 1F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("bell", + CubeListBuilder.create().texOffs(0, 80).addBox(-5F, 20F, -5F, 10F, 3F, 10F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("booster_w", + CubeListBuilder.create().texOffs(80, 0).addBox(-11F, 2F, -3F, 4F, 18F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("booster_e", + CubeListBuilder.create().texOffs(80, 0).addBox(7F, 2F, -3F, 4F, 18F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("booster_n", + CubeListBuilder.create().texOffs(104, 0).addBox(-3F, 2F, -11F, 6F, 18F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("booster_s", + CubeListBuilder.create().texOffs(104, 0).addBox(-3F, 2F, 7F, 6F, 18F, 4F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + + return LayerDefinition.create(mesh, 128, 128); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SpaceButton.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SpaceButton.java new file mode 100644 index 0000000..e2645ad --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SpaceButton.java @@ -0,0 +1,47 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.Button; +import net.minecraft.network.chat.Component; + +/** + * A sci-fi styled button for the Nerospace machine screens: a dark recessed body with a glowing + * accent border (brighter on hover or when {@linkplain #setSelected selected}) and centred text, + * drawn entirely in {@link #extractContents} so it doesn't look like a vanilla button. + */ +public class SpaceButton extends Button { + + private final int accent; + private boolean selected; + + public SpaceButton(int x, int y, int width, int height, Component message, int accent, OnPress onPress) { + super(x, y, width, height, message, onPress, DEFAULT_NARRATION); + this.accent = accent; + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + @Override + protected void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + int x = getX(); + int y = getY(); + int w = getWidth(); + int h = getHeight(); + boolean hovered = isHoveredOrFocused() && this.active; + + int border = !this.active ? 0xFF262B33 : (this.selected || hovered ? this.accent : 0xFF2E4A5A); + int body = !this.active ? 0xFF12161F : (this.selected ? 0xFF123042 : (hovered ? 0xFF16344A : 0xFF0C1E2B)); + + extractor.fill(x, y, x + w, y + h, border); + extractor.fill(x + 1, y + 1, x + w - 1, y + h - 1, body); + extractor.fill(x + 1, y + 1, x + w - 1, y + 2, 0x22FFFFFF); // top sheen + + Font font = Minecraft.getInstance().font; + int textColor = !this.active ? 0xFF6A7280 : (hovered || this.selected ? 0xFFFFFFFF : 0xFFCFE7FF); + extractor.centeredText(font, getMessage(), x + w / 2, y + (h - 8) / 2, textColor); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java new file mode 100644 index 0000000..4cb9ef8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java @@ -0,0 +1,110 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.renderer.RenderPipelines; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.AbstractContainerMenu; + +/** + * Base class for Nerospace machine screens ("spacified"). Draws a sci-fi hull panel (256x256 PNG at + * {@code assets/nerospace/textures/gui/.png}, top-left {@code imageWidth x imageHeight} = the + * panel) and themes the title/inventory labels for a dark background. Subclasses draw their + * gauges/readouts in {@link #extractForeground} and may use the gauge/label helpers. 26.1 renders + * container screens via {@code extract*(GuiGraphicsExtractor, ...)}: the panel is blitted in + * {@link #extractContents}, custom drawing on top. + */ +public abstract class TexturedContainerScreen extends AbstractContainerScreen { + + /** Shared sci-fi palette. */ + protected static final int INK = 0xFF05080D; // near-black trough border + protected static final int TROUGH = 0xFF0B1119; // gauge backing + protected static final int TITLE = 0xFFD6ECFF; // bright label + protected static final int SUBTLE = 0xFF8DA0B4; // dim label + + private final Identifier background; + /** Machine accent colour (ARGB). */ + protected final int accent; + + protected TexturedContainerScreen(T menu, Inventory playerInventory, Component title, Identifier background, + int accent, int width, int height) { + super(menu, playerInventory, title, width, height); + this.background = background; + this.accent = accent; + } + + @Override + public void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + extractor.blit(RenderPipelines.GUI_TEXTURED, this.background, this.leftPos, this.topPos, + 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256); + super.extractContents(extractor, mouseX, mouseY, partialTick); + extractForeground(extractor); + } + + /** Subclass hook: draw gauges + readouts on top of the panel (absolute coords via leftPos/topPos). */ + protected void extractForeground(GuiGraphicsExtractor extractor) { + } + + @Override + protected void extractLabels(GuiGraphicsExtractor extractor, int mouseX, int mouseY) { + extractor.text(this.font, this.title, this.titleLabelX, this.titleLabelY, TITLE, false); + extractor.text(this.font, this.playerInventoryTitle, this.inventoryLabelX, this.inventoryLabelY, SUBTLE, false); + } + + // --- Drawing helpers (panel-relative dx/dy) ----------------------------- + + /** A horizontal gauge: dark trough with an accent fill {@code frac} (0..1) of its width. */ + protected void hGauge(GuiGraphicsExtractor g, int dx, int dy, int w, int h, float frac, int fill) { + int x = this.leftPos + dx; + int y = this.topPos + dy; + g.fill(x - 1, y - 1, x + w + 1, y + h + 1, INK); + g.fill(x, y, x + w, y + h, TROUGH); + int fw = Math.max(0, Math.min(w, Math.round(w * frac))); + if (fw > 0) { + g.fill(x, y, x + fw, y + h, fill); + g.fill(x, y, x + fw, y + 1, 0x55FFFFFF); // top sheen + } + } + + /** + * A liquid gauge: two-tone wave fill so tank contents read as FLUID, not paint — pass the content + * colour (fuel amber, O₂ cyan, water blue, meltwater frost). + */ + protected void fluidGauge(GuiGraphicsExtractor g, int dx, int dy, int w, int h, float frac, int fill) { + int x = this.leftPos + dx; + int y = this.topPos + dy; + g.fill(x - 1, y - 1, x + w + 1, y + h + 1, INK); + g.fill(x, y, x + w, y + h, TROUGH); + int fw = Math.max(0, Math.min(w, Math.round(w * frac))); + if (fw <= 0) { + return; + } + int dark = darken(fill); + long t = System.currentTimeMillis() / 200L; + for (int px = 0; px < fw; px++) { + boolean crest = ((px + t) / 3L) % 2L == 0L; + g.fill(x + px, y, x + px + 1, y + h, crest ? fill : dark); + } + g.fill(x, y, x + fw, y + 1, 0x66FFFFFF); // meniscus sheen + g.fill(x + fw - 1, y, x + fw, y + h, 0x88FFFFFF); // surface line + } + + private static int darken(int argb) { + int r = (int) (((argb >> 16) & 0xFF) * 0.72F); + int gg = (int) (((argb >> 8) & 0xFF) * 0.72F); + int b = (int) ((argb & 0xFF) * 0.72F); + return (argb & 0xFF000000) | (r << 16) | (gg << 8) | b; + } + + /** Left-aligned label text at a panel-relative position. */ + protected void label(GuiGraphicsExtractor g, Component text, int dx, int dy, int color) { + g.text(this.font, text, this.leftPos + dx, this.topPos + dy, color, false); + } + + /** Centred label text within {@code [dx, dx+width)}. */ + protected void labelCentered(GuiGraphicsExtractor g, Component text, int dx, int width, int dy, int color) { + g.centeredText(this.font, text, this.leftPos + dx + width / 2, this.topPos + dy, color); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index cdf54e9..ecdef41 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -19,6 +19,8 @@ import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; import za.co.neroland.nerospace.machine.SolarPanelBlock; import za.co.neroland.nerospace.pipe.UniversalPipeBlock; +import za.co.neroland.nerospace.rocket.LaunchGantryBlock; +import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; import za.co.neroland.nerospace.storage.CreativeBatteryBlock; import za.co.neroland.nerospace.storage.GasTankBlock; import za.co.neroland.nerospace.storage.TrashCanBlock; @@ -166,6 +168,17 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + // --- Rockets ------------------------------------------------------------ + public static final RegistryEntry ROCKET_LAUNCH_PAD = BLOCKS.register("rocket_launch_pad", + key -> new RocketLaunchPadBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.0F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + + public static final RegistryEntry LAUNCH_GANTRY = BLOCKS.register("launch_gantry", + key -> new LaunchGantryBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.0F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + // Rocket fuel world block (placed by the bucket). LiquidBlock holds the source fluid, resolved // lazily on NeoForge / after ModFluids.init() on Fabric — hence ModFluids registers first. public static final RegistryEntry ROCKET_FUEL_BLOCK = BLOCKS.register("rocket_fuel", diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java index b8c0e84..bdd9c59 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java @@ -15,6 +15,7 @@ import za.co.neroland.nerospace.entity.RuinWarden; import za.co.neroland.nerospace.entity.WoollyDrift; import za.co.neroland.nerospace.entity.XertzStalker; +import za.co.neroland.nerospace.rocket.RocketEntity; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** @@ -80,6 +81,11 @@ public final class ModEntities { key -> EntityType.Builder.of(AlienVillager::new, MobCategory.CREATURE) .sized(0.6F, 1.95F).eyeHeight(1.7F).clientTrackingRange(10).build(key)); + public static final RegistryEntry> ROCKET = ENTITY_TYPES.register( + "rocket", + key -> EntityType.Builder.of(RocketEntity::new, MobCategory.MISC) + .sized(1.0F, 3.0F).clientTrackingRange(10).build(key)); + private ModEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 4baf269..5f52853 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -26,6 +26,8 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.rocket.RocketItem; +import za.co.neroland.nerospace.rocket.RocketTier; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** @@ -72,6 +74,8 @@ public final class ModItems { public static final RegistryEntry GAS_TANK_ITEM = blockItem("gas_tank", ModBlocks.GAS_TANK); public static final RegistryEntry OXYGEN_GENERATOR_ITEM = blockItem("oxygen_generator", ModBlocks.OXYGEN_GENERATOR); public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); + public static final RegistryEntry ROCKET_LAUNCH_PAD_ITEM = blockItem("rocket_launch_pad", ModBlocks.ROCKET_LAUNCH_PAD); + public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -95,6 +99,16 @@ public final class ModItems { /** Trade-only Artificer gear; ported as a plain item (its custom gear behaviour is deferred). */ public static final RegistryEntry XERTZ_RESONATOR = item("xertz_resonator"); + // --- Rockets (one item per tier; deploys a RocketEntity onto a launch pad) ---- + public static final RegistryEntry ROCKET_TIER_1 = ITEMS.register("rocket_tier_1", + key -> new RocketItem(new Item.Properties().stacksTo(1).setId(key), RocketTier.TIER_1)); + public static final RegistryEntry ROCKET_TIER_2 = ITEMS.register("rocket_tier_2", + key -> new RocketItem(new Item.Properties().stacksTo(1).setId(key), RocketTier.TIER_2)); + public static final RegistryEntry ROCKET_TIER_3 = ITEMS.register("rocket_tier_3", + key -> new RocketItem(new Item.Properties().stacksTo(1).setId(key), RocketTier.TIER_3)); + public static final RegistryEntry ROCKET_TIER_4 = ITEMS.register("rocket_tier_4", + key -> new RocketItem(new Item.Properties().stacksTo(1).setId(key), RocketTier.TIER_4)); + // --- Tool + armor materials -------------------------------------------- public static final ToolMaterial NEROSIUM_TOOL_MATERIAL = new ToolMaterial( BlockTags.INCORRECT_FOR_IRON_TOOL, 350, 7.0F, 2.5F, 15, cTag("ingots/nerosium")); @@ -184,7 +198,8 @@ public static Map, List> creativeTabItems NEROSIUM_DUST.get(), ALIEN_FRAGMENT.get(), ALIEN_TECH_SCRAP.get(), ALIEN_CORE.get(), ROCKET_FUEL_CANISTER.get(), FRAME_CASING.get(), GRAV_STRIDERS.get(), DRIFT_FLEECE.get()), CreativeModeTabs.TOOLS_AND_UTILITIES, - List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get()), + List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get(), + ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get()), CreativeModeTabs.COMBAT, List.of( OXYGEN_SUIT_HELMET.get(), OXYGEN_SUIT_CHESTPLATE.get(), OXYGEN_SUIT_LEGGINGS.get(), OXYGEN_SUIT_BOOTS.get(), @@ -192,7 +207,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 412d4eb..751f8f4 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -8,6 +8,7 @@ import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; +import za.co.neroland.nerospace.rocket.RocketMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** Menu types, shared via {@link RegistrationProvider} over the vanilla MENU registry. */ @@ -28,6 +29,10 @@ public final class ModMenuTypes { MENUS.register("passive_generator", key -> new MenuType<>(PassiveGeneratorMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> ROCKET = + MENUS.register("rocket", + key -> new MenuType<>(RocketMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/Destinations.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/Destinations.java new file mode 100644 index 0000000..b257fd6 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/Destinations.java @@ -0,0 +1,29 @@ +package za.co.neroland.nerospace.rocket; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** Display names for rocket destinations (used by the in-rocket selector). */ +public final class Destinations { + + private Destinations() { + } + + public static String name(ResourceKey key) { + if (key.equals(ModDimensions.STATION_LEVEL)) { + return "Orbital Station"; + } + if (key.equals(ModDimensions.GREENXERTZ_LEVEL)) { + return "Greenxertz"; + } + if (key.equals(ModDimensions.CINDARA_LEVEL)) { + return "Cindara"; + } + if (key.equals(ModDimensions.GLACIRA_LEVEL)) { + return "Glacira"; + } + return "Unknown"; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchGantryBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchGantryBlock.java new file mode 100644 index 0000000..fcc5486 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchGantryBlock.java @@ -0,0 +1,41 @@ +package za.co.neroland.nerospace.rocket; + +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +/** + * The Launch Gantry module: placed on a 5x5 pad's border ring it forms the Heavy Launch Complex + * (Tier 3 without the Station-Wall ring; Tier 4 launch infrastructure). Right-click boards the rocket + * standing on the adjacent pad — the service-tower QoL, no more pixel-hunting the entity. + */ +public class LaunchGantryBlock extends Block { + + public LaunchGantryBlock(Properties properties) { + super(properties); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, net.minecraft.world.phys.BlockHitResult hit) { + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + BlockPos pad = LaunchPadMultiblock.adjacentPad(level, pos); + Set pads = pad == null ? Set.of() : LaunchPadMultiblock.connectedPads(level, pad); + RocketEntity rocket = LaunchPadMultiblock.rocketAbove(level, pads); + if (rocket == null) { + player.sendSystemMessage(Component.translatable("block.nerospace.launch_gantry.no_rocket")); + return InteractionResult.SUCCESS; + } + if (player.startRiding(rocket)) { + player.sendSystemMessage(Component.translatable("block.nerospace.launch_gantry.boarded")); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchPadMultiblock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchPadMultiblock.java new file mode 100644 index 0000000..2eace39 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/LaunchPadMultiblock.java @@ -0,0 +1,233 @@ +package za.co.neroland.nerospace.rocket; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.AABB; + +/** + * Geometry helpers for the launch-pad multiblock. A "launch pad" in survival is not a single block: + * any horizontally-connected cluster of {@link RocketLaunchPadBlock} blocks (sharing one Y level) + * forms the pad footprint a rocket is fuelled on. A complete {@code 3x3} square is the canonical pad, + * recognised so adjacent machinery can reward it; a {@code 5x5} with a Launch Gantry on its ring is + * the Heavy Launch Complex. + * + *

This class is pure geometry: it never mutates the world, so it is safe to call from a tick.

+ */ +public final class LaunchPadMultiblock { + + /** Cap on the flood fill so a pathological field of pads can't stall a tick (≥ a sloppy 5x5 field). */ + private static final int MAX_PADS = 64; + /** How far above the pad surface a rocket's feet may be and still count as "on the pad". */ + private static final double SCAN_HEIGHT = 8.0D; + + private LaunchPadMultiblock() { + } + + /** @return the first horizontally-adjacent launch-pad position to {@code origin}, or {@code null}. */ + @Nullable + public static BlockPos adjacentPad(Level level, BlockPos origin) { + for (Direction dir : Direction.Plane.HORIZONTAL) { + BlockPos candidate = origin.relative(dir); + if (isPad(level, candidate)) { + return candidate; + } + } + return null; + } + + /** + * Flood-fills the horizontally-connected pad cluster containing {@code start} (same Y level), + * capped at {@link #MAX_PADS}. Returns an empty set if {@code start} is not a pad. + */ + public static Set connectedPads(Level level, BlockPos start) { + Set found = new HashSet<>(); + if (!isPad(level, start)) { + return found; + } + Deque queue = new ArrayDeque<>(); + queue.add(start.immutable()); + found.add(start.immutable()); + while (!queue.isEmpty() && found.size() < MAX_PADS) { + BlockPos pos = queue.poll(); + for (Direction dir : Direction.Plane.HORIZONTAL) { + BlockPos next = pos.relative(dir); + if (!found.contains(next) && isPad(level, next)) { + BlockPos immutable = next.immutable(); + found.add(immutable); + queue.add(immutable); + } + } + } + return found; + } + + /** Whether {@code pads} contains a complete aligned 3x3 square (the canonical full pad). */ + public static boolean isFullThreeByThree(Set pads) { + return fullSquareCorner(pads, 3) != null; + } + + /** The min-corner of the first complete aligned {@code size x size} square in {@code pads}, or {@code null}. */ + @Nullable + public static BlockPos fullSquareCorner(Set pads, int size) { + if (pads.size() < size * size) { + return null; + } + for (BlockPos corner : pads) { + if (isSquareFrom(pads, corner, size)) { + return corner; + } + } + return null; + } + + /** + * Like {@link #fullSquareCorner(Set, int)} but the square must CONTAIN {@code pos} (XZ) — used by + * launch gating so a rocket is grounded when the square it actually stands on degrades. + */ + @Nullable + public static BlockPos fullSquareCornerContaining(Set pads, int size, BlockPos pos) { + if (pads.size() < size * size) { + return null; + } + for (BlockPos corner : pads) { + if (pos.getX() >= corner.getX() && pos.getX() < corner.getX() + size + && pos.getZ() >= corner.getZ() && pos.getZ() < corner.getZ() + size + && isSquareFrom(pads, corner, size)) { + return corner; + } + } + return null; + } + + /** Whether the {@code size x size} square with min-corner {@code corner} is contained in {@code pads}. */ + private static boolean isSquareFrom(Set pads, BlockPos corner, int size) { + for (int dx = 0; dx < size; dx++) { + for (int dz = 0; dz < size; dz++) { + if (!pads.contains(new BlockPos(corner.getX() + dx, corner.getY(), corner.getZ() + dz))) { + return false; + } + } + } + return true; + } + + private static boolean isThreeByThreeFrom(Set pads, BlockPos corner) { + return isSquareFrom(pads, corner, 3); + } + + // --- Heavy Launch Complex ------------------------------------------------ + + /** + * The Heavy Launch Complex: a complete aligned 5x5 pad with at least one Launch Gantry module on + * its border ring at pad level. A Heavy complex deploys/launches Tier 3 without the Station-Wall ring. + */ + public static boolean isHeavyComplex(Level level, Set pads) { + BlockPos corner = fullSquareCorner(pads, 5); + return corner != null && borderRingHas(level, corner, 5, + state -> state.getBlock() instanceof LaunchGantryBlock); + } + + /** {@link #isHeavyComplex} where the 5x5 must contain {@code pos} (launch gating). */ + public static boolean isHeavyComplexContaining(Level level, Set pads, BlockPos pos) { + BlockPos corner = fullSquareCornerContaining(pads, 5, pos); + return corner != null && borderRingHas(level, corner, 5, + state -> state.getBlock() instanceof LaunchGantryBlock); + } + + /** {@link #hasStationWallRing} where the ringed 3x3 must contain {@code pos} (launch gating). */ + public static boolean hasStationWallRingAround(Level level, Set pads, BlockPos pos) { + for (BlockPos corner : pads) { + if (pos.getX() >= corner.getX() && pos.getX() < corner.getX() + 3 + && pos.getZ() >= corner.getZ() && pos.getZ() < corner.getZ() + 3 + && isThreeByThreeFrom(pads, corner) && hasRingAt(level, corner)) { + return true; + } + } + return false; + } + + /** Whether any cell of the border ring around the {@code size} square matches {@code predicate}. */ + public static boolean borderRingHas(Level level, BlockPos corner, int size, + java.util.function.Predicate predicate) { + for (int dx = -1; dx <= size; dx++) { + for (int dz = -1; dz <= size; dz++) { + if (dx != -1 && dx != size && dz != -1 && dz != size) { + continue; // interior — the pad itself + } + BlockPos pos = new BlockPos(corner.getX() + dx, corner.getY(), corner.getZ() + dz); + if (predicate.test(level.getBlockState(pos))) { + return true; + } + } + } + return false; + } + + /** Tier 3 gating: whether some complete 3x3 in {@code pads} is ringed with Station Wall. */ + public static boolean hasStationWallRing(Level level, Set pads) { + for (BlockPos corner : pads) { + if (isThreeByThreeFrom(pads, corner) && hasRingAt(level, corner)) { + return true; + } + } + return false; + } + + /** Whether the 5x5 border around the 3x3 with min-corner {@code corner} is all Station Wall. */ + private static boolean hasRingAt(Level level, BlockPos corner) { + for (int dx = -1; dx <= 3; dx++) { + for (int dz = -1; dz <= 3; dz++) { + if (dx != -1 && dx != 3 && dz != -1 && dz != 3) { + continue; // interior — the pad itself + } + BlockPos pos = new BlockPos(corner.getX() + dx, corner.getY(), corner.getZ() + dz); + if (!level.getBlockState(pos).is(za.co.neroland.nerospace.registry.ModBlocks.STATION_WALL.get())) { + return false; + } + } + } + return true; + } + + /** The first {@link RocketEntity} standing on top of any pad in {@code pads}, or {@code null}. */ + @Nullable + public static RocketEntity rocketAbove(Level level, Set pads) { + if (pads.isEmpty()) { + return null; + } + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + int padY = pads.iterator().next().getY(); + for (BlockPos pad : pads) { + minX = Math.min(minX, pad.getX()); + minZ = Math.min(minZ, pad.getZ()); + maxX = Math.max(maxX, pad.getX()); + maxZ = Math.max(maxZ, pad.getZ()); + } + AABB box = new AABB(minX, padY + 0.1D, minZ, maxX + 1.0D, padY + 1.0D + SCAN_HEIGHT, maxZ + 1.0D); + List rockets = level.getEntitiesOfClass(RocketEntity.class, box); + for (RocketEntity rocket : rockets) { + if (!rocket.isLaunching()) { + return rocket; + } + } + return null; + } + + private static boolean isPad(Level level, BlockPos pos) { + BlockState state = level.getBlockState(pos); + return state.getBlock() instanceof RocketLaunchPadBlock; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java new file mode 100644 index 0000000..05b4730 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java @@ -0,0 +1,610 @@ +package za.co.neroland.nerospace.rocket; + +import java.util.Set; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.network.syncher.EntityDataAccessor; +import net.minecraft.network.syncher.EntityDataSerializers; +import net.minecraft.network.syncher.SynchedEntityData; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityDimensions; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; + +import za.co.neroland.nerospace.fluid.FluidTank; +import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The Nerospace rocket: a rideable vehicle entity placed on a {@link RocketLaunchPadBlock}. It carries + * a {@link RocketTier} and a liquid-fuel tank (millibuckets). Right-clicking with a fuel bucket/canister + * tops up the tank; right-clicking empty-handed mounts the rocket and opens its UI, whose Launch button + * starts a simulated ascent that ends by transporting the rider to the tier's planet. + * + *

All gameplay logic is server-authoritative; the client only renders synced state and ascent + * particles. The entity has no gravity and rests where placed, moving only under launch thrust.

+ * + *

Cross-loader port note. The root binds the fuel store to the NeoForge transfer API and an + * item-capability intake proxy for pipe/hopper automation, and supports the multi-station founding + * system (Station Charter → StationRegistry slots, advancement criterion). The multiloader rebuilds the + * fuel store on the cross-loader {@link FluidTank} and a plain {@link SimpleContainer} intake (manual + + * UI fuelling still work; automated pipe-into-rocket feeding waits on the entity item-capability seam). + * The multi-station founding is deferred to its own batch (it needs the data-attachment + criteria + * seams); the Orbital Station destination here docks the rider at the shared origin platform.

+ */ +public class RocketEntity extends Entity implements MenuProvider { + + private static final EntityDataAccessor DATA_FUEL = + SynchedEntityData.defineId(RocketEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_TIER = + SynchedEntityData.defineId(RocketEntity.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_LAUNCHING = + SynchedEntityData.defineId(RocketEntity.class, EntityDataSerializers.BOOLEAN); + /** Index into the current tier's destination list. */ + private static final EntityDataAccessor DATA_DEST = + SynchedEntityData.defineId(RocketEntity.class, EntityDataSerializers.INT); + + /** Ticks of ascent before the rider is transported. */ + public static final int LAUNCH_DURATION = 100; + /** One fuel canister is worth this many millibuckets. */ + public static final int CANISTER_MB = 1_000; + /** Y level of the shared Orbital Station origin platform (the founded-station system is deferred). */ + public static final int PLATFORM_Y = 64; + + private int launchTicks; + + /** + * The authoritative fuel store, synced to the client via {@link #DATA_FUEL} for the GUI. Physical + * capacity is the largest tier's; {@link #addFuel} caps top-ups to the current tier's capacity. + */ + @SuppressWarnings("this-escape") // change-callback wiring, used only after construction + private final FluidTank fuelTank = new FluidTank(maxTierFuelCapacity(), this::syncFuel); + + /** The largest tier's tank — the physical store capacity (per-tier caps live in addFuel). */ + private static int maxTierFuelCapacity() { + int max = 0; + for (RocketTier tier : RocketTier.values()) { + max = Math.max(max, tier.fuelCapacity()); + } + return max; + } + + /** + * Single-slot fuel intake: the player (or the rocket UI) drops a {@link ModItems#ROCKET_FUEL_BUCKET} + * or {@link ModItems#ROCKET_FUEL_CANISTER} here and the rocket drains it into {@link #fuelTank} on + * its server tick — returning an empty bucket. + */ + private final SimpleContainer fuelInput = new SimpleContainer(1); + + /** + * Synced to the menu: [0]=fuel, [1]=capacity, [2]=tierOrdinal, [3]=launchable, [4]=destinationIndex. + */ + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> getFuel(); + case 1 -> getTier().fuelCapacity(); + case 2 -> getTier().ordinal(); + case 3 -> canLaunch() ? 1 : 0; + case 4 -> getDestinationIndex(); + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + // Read-only from the client. + } + + @Override + public int getCount() { + return 5; + } + }; + + /** Client-side position interpolation so the ascent (and the seated rider) moves smoothly. */ + private final net.minecraft.world.entity.InterpolationHandler interpolation = + new net.minecraft.world.entity.InterpolationHandler(this); + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public RocketEntity(EntityType type, Level level) { + super(type, level); + this.setNoGravity(true); + this.blocksBuilding = true; + } + + @Override + public net.minecraft.world.entity.InterpolationHandler getInterpolation() { + return this.interpolation; + } + + // --- Per-tier presentation ---------------------------------------------- + + /** Render scale by tier — bigger boosters genuinely LOOK bigger (T1 keeps the old size). */ + public float visualScale() { + return switch (getTier()) { + case TIER_1 -> 1.6F; + case TIER_2 -> 2.0F; + case TIER_3 -> 2.4F; + case TIER_4 -> 2.8F; + }; + } + + // Note: the root overrides NeoForge's shouldRiderSit() (a loader extension, not vanilla) to make + // the rider STAND at the console; that hook has no cross-loader equivalent, so it is omitted here. + + /** Stand the rider so their eyes line up with the hull's window band (scaled by tier). */ + @Override + protected Vec3 getPassengerAttachmentPoint(Entity entity, EntityDimensions dimensions, float scale) { + return new Vec3(0.0D, Math.max(0.4D, 2.1875D * visualScale() - 1.62D), 0.0D); + } + + /** Convenience constructor for spawning from the rocket item. */ + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public RocketEntity(Level level, double x, double y, double z, RocketTier tier) { + this(ModEntities.ROCKET.get(), level); + this.setPos(x, y, z); + this.setTier(tier); + } + + // --- Synced data -------------------------------------------------------- + + @Override + protected void defineSynchedData(SynchedEntityData.Builder builder) { + builder.define(DATA_FUEL, 0); + builder.define(DATA_TIER, RocketTier.TIER_1.ordinal()); + builder.define(DATA_LAUNCHING, false); + builder.define(DATA_DEST, 0); + } + + public int getFuel() { + return this.entityData.get(DATA_FUEL); + } + + /** Pushes the server-side tank amount into the synced data accessor (client GUI + canLaunch). */ + private void syncFuel() { + if (!level().isClientSide()) { + this.entityData.set(DATA_FUEL, (int) this.fuelTank.getAmount()); + } + } + + /** The UI fuel-intake slot (one bucket/canister), for the menu. */ + public net.minecraft.world.Container getFuelInput() { + return this.fuelInput; + } + + /** Whether {@code stack} is accepted by the fuel-intake slot. */ + public static boolean isFuelContainer(ItemStack stack) { + return stack.is(ModItems.ROCKET_FUEL_BUCKET.get()) || stack.is(ModItems.ROCKET_FUEL_CANISTER.get()); + } + + /** Current fuel as a 0–100 percentage of the tier capacity (for the UI readout). */ + public int getFuelPercent() { + int capacity = getTier().fuelCapacity(); + return capacity == 0 ? 0 : Math.min(100, getFuel() * 100 / capacity); + } + + public RocketTier getTier() { + return RocketTier.byOrdinal(this.entityData.get(DATA_TIER)); + } + + public void setTier(RocketTier tier) { + this.entityData.set(DATA_TIER, tier.ordinal()); + this.entityData.set(DATA_DEST, tier.defaultDestinationIndex()); + } + + // --- Destination selection ---------------------------------------------- + + public int getDestinationIndex() { + return this.entityData.get(DATA_DEST); + } + + /** The currently selected destination level, or {@code null} if this tier can't fly anywhere. */ + @Nullable + public net.minecraft.resources.ResourceKey selectedDestination() { + return getTier().destination(getDestinationIndex()); + } + + /** Cycles to the next destination available to this tier (server-side; from the menu button). */ + public void cycleDestination() { + if (level().isClientSide() || isLaunching()) { + return; + } + int count = getTier().destinations().size(); + if (count > 1) { + this.entityData.set(DATA_DEST, Math.floorMod(getDestinationIndex() + 1, count)); + } + } + + /** Selects a destination directly by index (server-side; from the trajectory buttons). */ + public void setDestinationIndex(int index) { + if (level().isClientSide() || isLaunching()) { + return; + } + int count = getTier().destinations().size(); + if (count > 0) { + this.entityData.set(DATA_DEST, Math.floorMod(index, count)); + } + } + + public boolean isLaunching() { + return this.entityData.get(DATA_LAUNCHING); + } + + private void setLaunching(boolean launching) { + this.entityData.set(DATA_LAUNCHING, launching); + } + + // --- Fuel / launch logic ------------------------------------------------ + + /** @return millibuckets of fuel that could not be accepted (overflow). Caps at the tier capacity. */ + public int addFuel(int amount) { + int room = Math.max(0, getTier().fuelCapacity() - (int) this.fuelTank.getAmount()); + int toFill = Math.min(amount, room); + int filled = (int) this.fuelTank.fill((Fluid) ModFluids.ROCKET_FUEL.get(), toFill, false); + return amount - filled; + } + + /** Whether a launch could be started right now (fuelled, has a rider, and a destination selected). */ + public boolean canLaunch() { + return !isLaunching() + && selectedDestination() != null + && getFuel() >= getTier().fuelPerLaunch() + && this.getFirstPassenger() instanceof Player + && isOnValidPad(); + } + + /** + * Launch-pad gating (re-checked at launch): the rocket must stand ON a complete 3x3 pad that + * contains the rocket's position. A Tier 3 rocket additionally needs that pad ringed with Station + * Wall OR a Heavy Launch Complex; a Tier 4 rocket needs the Heavy Launch Complex specifically. + */ + public boolean isOnValidPad() { + BlockPos origin = padScanOrigin(); + Set pads = LaunchPadMultiblock.connectedPads(level(), origin); + if (LaunchPadMultiblock.fullSquareCornerContaining(pads, 3, origin) == null) { + return false; + } + if (getTier() == RocketTier.TIER_4) { + return LaunchPadMultiblock.isHeavyComplexContaining(level(), pads, origin); + } + return getTier() != RocketTier.TIER_3 + || LaunchPadMultiblock.hasStationWallRingAround(level(), pads, origin) + || LaunchPadMultiblock.isHeavyComplexContaining(level(), pads, origin); + } + + /** Where to look for the pad under the rocket. The rocket stands ON the pad's 3px plate. */ + private BlockPos padScanOrigin() { + BlockPos feet = this.blockPosition(); + return level().getBlockState(feet).getBlock() instanceof RocketLaunchPadBlock ? feet : feet.below(); + } + + /** Begins the ascent. Server-side; called from the rocket menu's Launch button. */ + public void startLaunch() { + if (level().isClientSide() || !canLaunch()) { + if (!level().isClientSide() && !isLaunching() && !isOnValidPad() + && this.getFirstPassenger() instanceof ServerPlayer rider) { + Set pads = LaunchPadMultiblock.connectedPads(level(), padScanOrigin()); + String message; + if (!LaunchPadMultiblock.isFullThreeByThree(pads)) { + message = "item.nerospace.rocket.pad_incomplete"; + } else if (getTier() == RocketTier.TIER_4) { + message = "item.nerospace.rocket.pad_heavy_required"; + } else { + message = "item.nerospace.rocket.pad_ring_required"; + } + rider.sendSystemMessage(Component.translatable(message)); + } + return; + } + setLaunching(true); + this.launchTicks = 0; + level().playSound(null, this.getX(), this.getY(), this.getZ(), + SoundEvents.FIREWORK_ROCKET_LAUNCH, SoundSource.NEUTRAL, 3.0F, 0.6F); + } + + @Override + public void tick() { + super.tick(); + + // Advance the client-side interpolation toward the latest server position (vanilla vehicle pattern). + if (level().isClientSide() && this.interpolation.hasActiveInterpolation()) { + this.interpolation.interpolate(); + } + + if (isLaunching()) { + if (level().isClientSide()) { + spawnLaunchParticles(); + } else { + double t = (double) this.launchTicks / LAUNCH_DURATION; + double speed = 0.08D + 0.5D * (t * t); + this.setDeltaMovement(0.0D, speed, 0.0D); + this.move(net.minecraft.world.entity.MoverType.SELF, this.getDeltaMovement()); + this.launchTicks++; + if (this.launchTicks >= LAUNCH_DURATION) { + completeLaunch(); + } + } + } else if (!level().isClientSide()) { + // Idle on the pad: drain any fuel container the player dropped in the intake. + consumeFuelInput(); + } + } + + /** + * If the intake slot holds a fuel container and the tank has room, drains one unit (1000 mB) into + * the tank. A bucket is returned empty; a canister is consumed. Runs once per tick, server-side. + */ + private void consumeFuelInput() { + ItemStack stack = this.fuelInput.getItem(0); + if (stack.isEmpty() || !isFuelContainer(stack)) { + return; + } + if (addFuel(CANISTER_MB) >= CANISTER_MB) { + return; // tank full — leave the container in place. + } + if (stack.is(ModItems.ROCKET_FUEL_BUCKET.get())) { + if (stack.getCount() == 1) { + this.fuelInput.setItem(0, new ItemStack(Items.BUCKET)); + } else { + stack.shrink(1); + if (level() instanceof ServerLevel server) { + this.spawnAtLocation(server, new ItemStack(Items.BUCKET)); + } + } + } else { + stack.shrink(1); // canister consumed + } + level().playSound(null, this.getX(), this.getY(), this.getZ(), + SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 0.6F, 1.0F); + } + + /** Drops the intake slot's contents into the world (called before the rocket is discarded). */ + private void dropFuelInput() { + if (level() instanceof ServerLevel server) { + ItemStack stack = this.fuelInput.removeItemNoUpdate(0); + if (!stack.isEmpty()) { + this.spawnAtLocation(server, stack); + } + } + } + + private void spawnLaunchParticles() { + double bx = this.getX(); + double by = this.getY(); + double bz = this.getZ(); + for (int i = 0; i < 4; i++) { + double ox = (this.random.nextDouble() - 0.5D) * 0.4D; + double oz = (this.random.nextDouble() - 0.5D) * 0.4D; + level().addParticle(ParticleTypes.FLAME, bx + ox, by - 0.2D, bz + oz, 0.0D, -0.1D, 0.0D); + level().addParticle(ParticleTypes.LARGE_SMOKE, bx + ox, by - 0.4D, bz + oz, 0.0D, -0.05D, 0.0D); + } + } + + private void completeLaunch() { + net.minecraft.resources.ResourceKey targetKey = selectedDestination(); + if (targetKey == null) { + setLaunching(false); + return; + } + + this.fuelTank.drain(getTier().fuelPerLaunch(), false); + + Entity passenger = this.getFirstPassenger(); + if (passenger instanceof ServerPlayer player && level() instanceof ServerLevel current) { + MinecraftServer server = current.getServer(); + ServerLevel destination = server.getLevel(targetKey); + if (destination != null) { + player.stopRiding(); + + double arrivalX; + double arrivalY; + double arrivalZ; + Component arrivalMessage; + if (targetKey.equals(ModDimensions.STATION_LEVEL)) { + // Origin = the shared public platform (the multi-station founding system is deferred). + BlockPos centre = new BlockPos(0, PLATFORM_Y, 0); + arrivalMessage = Component.translatable("entity.nerospace.rocket.docked"); + destination.getChunk(centre.getX() >> 4, centre.getZ() >> 4); + if (!destination.getBlockState(centre).is(ModBlocks.STATION_FLOOR.get())) { + buildStationPlatform(destination, centre.getX(), centre.getY(), centre.getZ()); + } + arrivalX = centre.getX() + 0.5D; + arrivalY = centre.getY() + 1.0D; + arrivalZ = centre.getZ() + 0.5D; + } else { + int blockX = Mth.floor(player.getX()); + int blockZ = Mth.floor(player.getZ()); + destination.getChunk(blockX >> 4, blockZ >> 4); + arrivalX = player.getX(); + arrivalZ = player.getZ(); + arrivalY = destination.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, blockX, blockZ) + 1.0D; + arrivalMessage = Component.translatable("entity.nerospace.rocket.arrived"); + } + + player.teleportTo(destination, arrivalX, arrivalY, arrivalZ, Set.of(), player.getYRot(), player.getXRot(), true); + player.sendSystemMessage(arrivalMessage); + } + } + + // The rocket is expended on launch; a return trip needs a pad + rocket on the destination. + dropFuelInput(); + this.discard(); + } + + /** Lay a 7x7 station-floor landing pad so a rider arriving in the void station has solid ground. */ + private static void buildStationPlatform(ServerLevel level, int centerX, int y, int centerZ) { + BlockState floor = ModBlocks.STATION_FLOOR.get().defaultBlockState(); + for (int dx = -3; dx <= 3; dx++) { + for (int dz = -3; dz <= 3; dz++) { + level.setBlockAndUpdate(new BlockPos(centerX + dx, y, centerZ + dz), floor); + } + } + } + + // --- Interaction -------------------------------------------------------- + + @Override + public InteractionResult interact(Player player, InteractionHand hand, Vec3 hitLocation) { + if (isLaunching()) { + return InteractionResult.PASS; + } + + ItemStack held = player.getItemInHand(hand); + if (held.is(ModItems.ROCKET_FUEL_BUCKET.get())) { + if (!level().isClientSide()) { + int overflow = addFuel(1_000); + if (overflow < 1_000) { + if (!player.getAbilities().instabuild) { + player.setItemInHand(hand, new ItemStack(Items.BUCKET)); + } + level().playSound(null, this.getX(), this.getY(), this.getZ(), + SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 0.8F, 1.0F); + } + } + return InteractionResult.SUCCESS; + } + if (held.is(ModItems.ROCKET_FUEL_CANISTER.get())) { + if (!level().isClientSide()) { + int overflow = addFuel(CANISTER_MB); + if (overflow < CANISTER_MB) { + if (!player.getAbilities().instabuild) { + held.shrink(1); + } + level().playSound(null, this.getX(), this.getY(), this.getZ(), + SoundEvents.BUCKET_EMPTY, SoundSource.NEUTRAL, 0.7F, 1.0F); + } + } + return InteractionResult.SUCCESS; + } + + if (!level().isClientSide()) { + if (this.getFirstPassenger() == null) { + player.startRiding(this); + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(this); + } + } + return InteractionResult.SUCCESS; + } + + // --- Passenger handling ------------------------------------------------- + + @Override + protected boolean canAddPassenger(Entity passenger) { + return this.getPassengers().isEmpty(); + } + + @Override + public boolean isPickable() { + return !this.isRemoved(); + } + + @Override + @Nullable + public ItemStack getPickResult() { + return new ItemStack(itemForTier()); + } + + private net.minecraft.world.item.Item itemForTier() { + return switch (getTier()) { + case TIER_1 -> ModItems.ROCKET_TIER_1.get(); + case TIER_2 -> ModItems.ROCKET_TIER_2.get(); + case TIER_3 -> ModItems.ROCKET_TIER_3.get(); + case TIER_4 -> ModItems.ROCKET_TIER_4.get(); + }; + } + + @Override + public boolean hurtServer(ServerLevel level, net.minecraft.world.damagesource.DamageSource damageSource, float amount) { + if (this.isRemoved() || isLaunching()) { + return false; + } + if (damageSource.getEntity() instanceof Player player) { + if (!player.getAbilities().instabuild) { + this.spawnAtLocation(level, new ItemStack(itemForTier())); + } + dropFuelInput(); + this.discard(); + return true; + } + return false; + } + + // --- Persistence (Value I/O) ------------------------------------------- + + @Override + protected void readAdditionalSaveData(ValueInput input) { + this.entityData.set(DATA_TIER, input.getIntOr("Tier", RocketTier.TIER_1.ordinal())); + this.entityData.set(DATA_DEST, input.getIntOr("Destination", getTier().defaultDestinationIndex())); + Fluid fluid = BuiltInRegistries.FLUID.getValue( + Identifier.parse(input.getStringOr("FuelFluid", "minecraft:empty"))); + this.fuelTank.setRaw(fluid, input.getIntOr("FuelAmount", 0)); + this.fuelInput.setItem(0, input.read("FuelInput", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + syncFuel(); + setLaunching(input.getBooleanOr("Launching", false)); + this.launchTicks = input.getIntOr("LaunchTicks", 0); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("Tier", getTier().ordinal()); + output.putInt("Destination", getDestinationIndex()); + output.putString("FuelFluid", BuiltInRegistries.FLUID.getKey(this.fuelTank.getRawFluid()).toString()); + output.putInt("FuelAmount", this.fuelTank.getRawAmount()); + output.store("FuelInput", ItemStack.OPTIONAL_CODEC, this.fuelInput.getItem(0)); + output.putBoolean("Launching", isLaunching()); + output.putInt("LaunchTicks", this.launchTicks); + } + + // --- MenuProvider ------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.rocket"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new RocketMenu(containerId, playerInventory, this, this.dataAccess); + } + + /** Exposed for the menu's server constructor. */ + public ContainerData getDataAccess() { + return this.dataAccess; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketItem.java new file mode 100644 index 0000000..c8bab0f --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketItem.java @@ -0,0 +1,99 @@ +package za.co.neroland.nerospace.rocket; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Places a {@link RocketEntity} of a fixed {@link RocketTier} onto a {@link RocketLaunchPadBlock}. + * Using it anywhere else does nothing, so a launch pad is required to deploy a rocket. + */ +public class RocketItem extends Item { + + private final RocketTier tier; + + public RocketItem(Properties properties, RocketTier tier) { + super(properties); + this.tier = tier; + } + + public RocketTier tier() { + return this.tier; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + BlockState state = level.getBlockState(pos); + + if (!(state.getBlock() instanceof RocketLaunchPadBlock)) { + return InteractionResult.PASS; + } + + if (!level.isClientSide()) { + // Multiblock gating: deploying needs a properly formed 3x3 pad; a Tier 3 rocket + // additionally needs the pad ringed with Station Wall (the same checks re-run at launch). + Player player = context.getPlayer(); + java.util.Set pads = LaunchPadMultiblock.connectedPads(level, pos); + if (!LaunchPadMultiblock.isFullThreeByThree(pads)) { + if (player != null) { + player.sendSystemMessage(Component.translatable("item.nerospace.rocket.pad_incomplete")); + } + return InteractionResult.SUCCESS; + } + // Tier 3 needs the Station-Wall ring OR a Heavy Launch Complex (5x5 + gantry). + if (this.tier == RocketTier.TIER_3 + && !LaunchPadMultiblock.hasStationWallRing(level, pads) + && !LaunchPadMultiblock.isHeavyComplex(level, pads)) { + if (player != null) { + player.sendSystemMessage(Component.translatable("item.nerospace.rocket.pad_ring_required")); + } + return InteractionResult.SUCCESS; + } + // Tier 4 needs the Heavy Launch Complex specifically (no ring shortcut). + if (this.tier == RocketTier.TIER_4 && !LaunchPadMultiblock.isHeavyComplex(level, pads)) { + if (player != null) { + player.sendSystemMessage(Component.translatable("item.nerospace.rocket.pad_heavy_required")); + } + return InteractionResult.SUCCESS; + } + + // One rocket per pad: reject a second deploy onto an occupied cluster. + if (LaunchPadMultiblock.rocketAbove(level, pads) != null) { + if (player != null) { + player.sendSystemMessage(Component.translatable("item.nerospace.rocket.pad_occupied")); + } + return InteractionResult.SUCCESS; + } + + // Centre the rocket on the formed square (the 5x5 when present, else the 3x3) and stand it + // on the pad's plate surface (the pad is a 3px platform, not a cube). + BlockPos corner5 = LaunchPadMultiblock.fullSquareCorner(pads, 5); + BlockPos corner = corner5 != null ? corner5 : LaunchPadMultiblock.fullSquareCorner(pads, 3); + int size = corner5 != null ? 5 : 3; + BlockPos centre = corner == null ? pos + : new BlockPos(corner.getX() + size / 2, corner.getY(), corner.getZ() + size / 2); + RocketEntity rocket = new RocketEntity( + level, centre.getX() + 0.5D, centre.getY() + RocketLaunchPadBlock.SURFACE_HEIGHT, + centre.getZ() + 0.5D, this.tier); + level.addFreshEntity(rocket); + + ItemStack stack = context.getItemInHand(); + if (player != null && !player.getAbilities().instabuild) { + stack.shrink(1); + } + if (player != null) { + player.sendSystemMessage(Component.translatable("item.nerospace.rocket.deployed")); + } + } + + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketLaunchPadBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketLaunchPadBlock.java new file mode 100644 index 0000000..a1352a1 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketLaunchPadBlock.java @@ -0,0 +1,75 @@ +package za.co.neroland.nerospace.rocket; + +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +/** + * The craftable launch mount: a short pad a rocket is assembled onto. A {@link RocketItem} may only + * place a {@link RocketEntity} directly above one of these blocks, so the pad doubles as the + * "assembly point" gate for a launch. Empty-hand right-click prints a formation report (cluster size, + * largest formed square, modules, and what is missing). + */ +public class RocketLaunchPadBlock extends Block { + + /** The plate's top surface, in blocks — rockets stand at pad Y + this. */ + public static final double SURFACE_HEIGHT = 3.0D / 16.0D; + + private static final VoxelShape SHAPE = Block.box(0.0D, 0.0D, 0.0D, 16.0D, 3.0D, 16.0D); + + public RocketLaunchPadBlock(Properties properties) { + super(properties); + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + @Override + protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + // Only report on a truly empty hand: this path also runs while HOLDING items (the 26.1 + // interaction order tries it before Item#useOn), and consuming the click here would swallow + // rocket-item deployment onto the pad. + if (!player.getMainHandItem().isEmpty()) { + return InteractionResult.PASS; + } + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + Set pads = LaunchPadMultiblock.connectedPads(level, pos); + BlockPos corner5 = LaunchPadMultiblock.fullSquareCorner(pads, 5); + boolean full3 = LaunchPadMultiblock.isFullThreeByThree(pads); + boolean heavy = LaunchPadMultiblock.isHeavyComplex(level, pads); + boolean ring = LaunchPadMultiblock.hasStationWallRing(level, pads); + + String formed = heavy ? "heavy" : (corner5 != null ? "5x5" : (full3 ? "3x3" : "none")); + player.sendSystemMessage(Component.translatable( + "block.nerospace.rocket_launch_pad.report." + formed, pads.size())); + if (corner5 != null && !heavy) { + player.sendSystemMessage(Component.translatable( + "block.nerospace.rocket_launch_pad.report.need_gantry")); + } + if (full3 || heavy) { + player.sendSystemMessage(Component.translatable(heavy || ring + ? "block.nerospace.rocket_launch_pad.report.t3_ready" + : "block.nerospace.rocket_launch_pad.report.t3_not_ready")); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java new file mode 100644 index 0000000..dc46b79 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java @@ -0,0 +1,186 @@ +package za.co.neroland.nerospace.rocket; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * Menu for the in-rocket UI. Carries the player inventory, a single fuel-intake slot (drop a fuel + * bucket/canister to fuel the rocket), and five synced data values describing the rocket (fuel, + * capacity, tier, launch-readiness, destination index). + * + *

Buttons route through {@link #clickMenuButton} so no custom packet is needed: Launch, the cycle + * button, and direct destination selection ({@code SELECT_DEST_BASE + index}) are all handled + * server-side, where the menu holds a reference to the {@link RocketEntity}.

+ * + *

Cross-loader note: this is a plain (non-extended) menu. The client never needs the entity — it + * displays from the synced {@link ContainerData} and routes buttons to the server menu — so the + * server opens it with the vanilla {@code openMenu(MenuProvider)} and we avoid the loader-divergent + * extended-menu API. The multi-station founding rows are deferred with that subsystem.

+ */ +public class RocketMenu extends AbstractContainerMenu { + + public static final int BUTTON_LAUNCH = 0; + public static final int BUTTON_CYCLE_DEST = 1; + /** Select destination {@code n} via button id {@code SELECT_DEST_BASE + n}. */ + public static final int SELECT_DEST_BASE = 100; + + private static final int DATA_COUNT = 5; + private static final int FUEL_SLOT_INDEX = 0; + private static final int PLAYER_INV_START = 1; + private static final int PLAYER_INV_END = PLAYER_INV_START + 36; // exclusive + + private static final int FUEL_SLOT_X = 148; + private static final int FUEL_SLOT_Y = 17; + + private final ContainerData data; + private final Container fuelContainer; + @Nullable + private final RocketEntity rocket; + + /** Client constructor (referenced by the menu type); rocket state arrives via the synced data. */ + public RocketMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, null, new SimpleContainerData(DATA_COUNT)); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public RocketMenu(int containerId, Inventory playerInventory, @Nullable RocketEntity rocket, ContainerData data) { + super(ModMenuTypes.ROCKET.get(), containerId); + checkContainerDataCount(data, DATA_COUNT); + this.rocket = rocket; + this.data = data; + this.fuelContainer = rocket != null ? rocket.getFuelInput() : new SimpleContainer(1); + + this.addSlot(new FuelSlot(this.fuelContainer, 0, FUEL_SLOT_X, FUEL_SLOT_Y)); + this.addStandardInventorySlots(playerInventory, 8, 84); + this.addDataSlots(data); + } + + @Override + public boolean clickMenuButton(Player player, int id) { + RocketEntity current = this.rocket; // local copy so the null check holds for the analyzer + if (current == null) { + return false; + } + if (id == BUTTON_LAUNCH) { + current.startLaunch(); + return true; + } + if (id == BUTTON_CYCLE_DEST) { + current.cycleDestination(); + return true; + } + if (id >= SELECT_DEST_BASE) { + current.setDestinationIndex(id - SELECT_DEST_BASE); + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + RocketEntity current = this.rocket; + if (current == null) { + return true; + } + return current.isAlive() && !current.isRemoved() && player.distanceTo(current) < 8.0F; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack moved = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack raw = slot.getItem(); + moved = raw.copy(); + if (index == FUEL_SLOT_INDEX) { + if (!this.moveItemStackTo(raw, PLAYER_INV_START, PLAYER_INV_END, true)) { + return ItemStack.EMPTY; + } + } else if (RocketEntity.isFuelContainer(raw)) { + if (!this.moveItemStackTo(raw, FUEL_SLOT_INDEX, FUEL_SLOT_INDEX + 1, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (raw.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + if (raw.getCount() == moved.getCount()) { + return ItemStack.EMPTY; + } + slot.onTake(player, raw); + } + return moved; + } + + // --- Screen helpers ----------------------------------------------------- + + public int getFuel() { + return this.data.get(0); + } + + public int getCapacity() { + return this.data.get(1); + } + + public RocketTier getTier() { + return RocketTier.byOrdinal(this.data.get(2)); + } + + public boolean isLaunchable() { + return this.data.get(3) != 0; + } + + public int getDestinationIndex() { + return this.data.get(4); + } + + /** Current fuel as a 0–100 percentage of the tier capacity. */ + public int getFuelPercent() { + int capacity = getCapacity(); + return capacity == 0 ? 0 : Math.min(100, getFuel() * 100 / capacity); + } + + /** Display name of the currently selected destination. */ + public String getDestinationName() { + net.minecraft.resources.ResourceKey key = + getTier().destination(getDestinationIndex()); + return key == null ? "—" : Destinations.name(key); + } + + public boolean hasMultipleDestinations() { + return getTier().destinations().size() > 1; + } + + /** Fuel-intake slot: only rocket fuel buckets/canisters may be placed. */ + private static class FuelSlot extends Slot { + FuelSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return RocketEntity.isFuelContainer(stack); + } + + @Override + public int getMaxStackSize() { + return 16; + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java new file mode 100644 index 0000000..a29d73b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java @@ -0,0 +1,92 @@ +package za.co.neroland.nerospace.rocket; + +import java.util.List; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** + * Rocket progression tiers. Each tier defines its fuel tank capacity, the fuel burned per launch + * (both in millibuckets), and the ordered list of destinations it can reach. Destinations are + * cumulative: a higher tier can still fly to every lower-tier destination, and the player picks the + * target in the in-rocket UI. The tier's signature destination (the newest one it unlocks) + * is the last entry and the default selection. + * + *

Progression: Tier 1 reaches the Orbital Station; Tier 2 adds Greenxertz; Tier 3 adds Cindara; + * Tier 4 adds Glacira (and deploys only on the Heavy Launch Complex).

+ * + *

Cross-loader port note: the root scales these by {@code Config}/{@code Tuning} multipliers; the + * multiloader inlines the base values (identity multiplier) until the config seam is ported.

+ */ +public enum RocketTier { + + TIER_1(1, 3_000, 1_000, List.of(ModDimensions.STATION_LEVEL)), + TIER_2(2, 6_000, 2_000, List.of(ModDimensions.STATION_LEVEL, ModDimensions.GREENXERTZ_LEVEL)), + TIER_3(3, 12_000, 4_000, List.of( + ModDimensions.STATION_LEVEL, ModDimensions.GREENXERTZ_LEVEL, ModDimensions.CINDARA_LEVEL)), + TIER_4(4, 24_000, 8_000, List.of( + ModDimensions.STATION_LEVEL, ModDimensions.GREENXERTZ_LEVEL, ModDimensions.CINDARA_LEVEL, + ModDimensions.GLACIRA_LEVEL)); + + private final int level; + private final int fuelCapacity; + private final int fuelPerLaunch; + private final List> destinations; + + RocketTier(int level, int fuelCapacity, int fuelPerLaunch, List> destinations) { + this.level = level; + this.fuelCapacity = fuelCapacity; + this.fuelPerLaunch = fuelPerLaunch; + this.destinations = destinations; + } + + /** Human-facing tier number (1-based). */ + public int level() { + return this.level; + } + + /** Fuel tank capacity, in millibuckets. */ + public int fuelCapacity() { + return this.fuelCapacity; + } + + /** Fuel consumed by a single launch, in millibuckets (clamped to the tank so a launch is always possible). */ + public int fuelPerLaunch() { + return Math.min(this.fuelPerLaunch, this.fuelCapacity); + } + + /** Ordered list of reachable destinations (lowest unlock first, signature destination last). */ + public List> destinations() { + return this.destinations; + } + + /** Whether this tier can fly anywhere. */ + public boolean hasDestination() { + return !this.destinations.isEmpty(); + } + + /** Default selection: the tier's signature (newest) destination. */ + public int defaultDestinationIndex() { + return Math.max(0, this.destinations.size() - 1); + } + + /** Safe destination lookup by index (clamped). */ + public ResourceKey destination(int index) { + if (this.destinations.isEmpty()) { + return null; + } + int clamped = Math.floorMod(index, this.destinations.size()); + return this.destinations.get(clamped); + } + + /** Safe lookup by ordinal for persistence/synced data. */ + public static RocketTier byOrdinal(int ordinal) { + RocketTier[] values = values(); + if (ordinal < 0 || ordinal >= values.length) { + return TIER_1; + } + return values[ordinal]; + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/launch_gantry.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/launch_gantry.json new file mode 100644 index 0000000..d1d6505 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/launch_gantry.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/launch_gantry" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_launch_pad.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_launch_pad.json new file mode 100644 index 0000000..f6231f1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/rocket_launch_pad.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/rocket_launch_pad" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/launch_gantry.json b/multiloader/common/src/main/resources/assets/nerospace/items/launch_gantry.json new file mode 100644 index 0000000..eea3327 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/launch_gantry.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/launch_gantry" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_launch_pad.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_launch_pad.json new file mode 100644 index 0000000..3f872b3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_launch_pad.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/rocket_launch_pad" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_1.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_1.json new file mode 100644 index 0000000..8883651 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_1.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/rocket_tier_1" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_2.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_2.json new file mode 100644 index 0000000..5027613 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_2.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/rocket_tier_2" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_3.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_3.json new file mode 100644 index 0000000..20d70b4 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_3.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/rocket_tier_3" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_4.json b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_4.json new file mode 100644 index 0000000..2eabdb0 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/rocket_tier_4.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/rocket_tier_4" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index f3f1bc7..8192db1 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -1,86 +1,111 @@ { - "block.nerospace.nerosium_ore": "Nerosium Ore", - "block.nerospace.deepslate_nerosium_ore": "Deepslate Nerosium Ore", - "block.nerospace.nerosium_block": "Block of Nerosium", - "block.nerospace.raw_nerosium_block": "Block of Raw Nerosium", - "item.nerospace.raw_nerosium": "Raw Nerosium", - "item.nerospace.nerosium_ingot": "Nerosium Ingot", - "block.nerospace.nerosteel_ore": "Nerosteel Ore", - "block.nerospace.nerosteel_block": "Block of Nerosteel", - "block.nerospace.xertz_quartz_ore": "Xertz Quartz Ore", - "block.nerospace.cindrite_ore": "Cindrite Ore", - "block.nerospace.cindrite_block": "Block of Cindrite", - "block.nerospace.glacite_ore": "Glacite Ore", - "block.nerospace.glacite_block": "Block of Glacite", - "item.nerospace.raw_nerosteel": "Raw Nerosteel", - "item.nerospace.nerosteel_ingot": "Nerosteel Ingot", - "item.nerospace.xertz_quartz": "Xertz Quartz", - "item.nerospace.cindrite": "Cindrite", - "item.nerospace.glacite": "Glacite", - "block.nerospace.station_floor": "Station Floor", - "block.nerospace.station_wall": "Station Wall", "block.nerospace.alien_bricks": "Alien Bricks", - "block.nerospace.cracked_alien_bricks": "Cracked Alien Bricks", - "block.nerospace.alien_tile": "Alien Tile", - "block.nerospace.alien_pillar": "Alien Pillar", - "block.nerospace.alien_lamp": "Alien Lamp", "block.nerospace.alien_crystal_block": "Alien Crystal Block", - "block.nerospace.meteor_rock": "Meteor Rock", - "item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe", - "item.nerospace.oxygen_suit_helmet": "Oxygen Suit Helmet", - "item.nerospace.oxygen_suit_chestplate": "Oxygen Suit Chestplate", - "item.nerospace.oxygen_suit_leggings": "Oxygen Suit Leggings", - "item.nerospace.oxygen_suit_boots": "Oxygen Suit Boots", - "item.nerospace.oxygen_suit_t2_helmet": "Tier 2 Oxygen Suit Helmet", - "item.nerospace.oxygen_suit_t2_chestplate": "Tier 2 Oxygen Suit Chestplate", - "item.nerospace.oxygen_suit_t2_leggings": "Tier 2 Oxygen Suit Leggings", - "item.nerospace.oxygen_suit_t2_boots": "Tier 2 Oxygen Suit Boots", - "item.nerospace.oxygen_suit_heat_helmet": "Thermal Suit Helmet", - "item.nerospace.oxygen_suit_heat_chestplate": "Thermal Suit Chestplate", - "item.nerospace.oxygen_suit_heat_leggings": "Thermal Suit Leggings", - "item.nerospace.oxygen_suit_heat_boots": "Thermal Suit Boots", - "item.nerospace.oxygen_suit_cold_helmet": "Cryo Suit Helmet", - "item.nerospace.oxygen_suit_cold_chestplate": "Cryo Suit Chestplate", - "item.nerospace.oxygen_suit_cold_leggings": "Cryo Suit Leggings", - "item.nerospace.oxygen_suit_cold_boots": "Cryo Suit Boots", - "item.nerospace.nerosium_dust": "Nerosium Dust", - "item.nerospace.alien_fragment": "Alien Fragment", - "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", - "item.nerospace.alien_core": "Alien Core", - "item.nerospace.rocket_fuel_canister": "Rocket Fuel Canister", - "item.nerospace.rocket_fuel_bucket": "Rocket Fuel Bucket", - "block.nerospace.rocket_fuel": "Rocket Fuel", - "fluid_type.nerospace.rocket_fuel": "Rocket Fuel", + "block.nerospace.alien_lamp": "Alien Lamp", + "block.nerospace.alien_pillar": "Alien Pillar", + "block.nerospace.alien_tile": "Alien Tile", + "block.nerospace.battery": "Battery", + "block.nerospace.cindrite_block": "Block of Cindrite", + "block.nerospace.cindrite_ore": "Cindrite Ore", + "block.nerospace.combustion_generator": "Combustion Generator", + "block.nerospace.cracked_alien_bricks": "Cracked Alien Bricks", + "block.nerospace.creative_battery": "Creative Battery", + "block.nerospace.deepslate_nerosium_ore": "Deepslate Nerosium Ore", + "block.nerospace.fluid_tank": "Fluid Tank", "block.nerospace.gas_tank": "Gas Tank", + "block.nerospace.glacite_block": "Block of Glacite", + "block.nerospace.glacite_ore": "Glacite Ore", + "block.nerospace.item_store": "Item Store", + "block.nerospace.launch_gantry": "Launch Gantry", + "block.nerospace.launch_gantry.boarded": "Boarded the rocket — strap in", + "block.nerospace.launch_gantry.no_rocket": "No rocket on the pad to board", + "block.nerospace.meteor_rock": "Meteor Rock", + "block.nerospace.nerosium_block": "Block of Nerosium", + "block.nerospace.nerosium_grinder": "Nerosium Grinder", + "block.nerospace.nerosium_ore": "Nerosium Ore", + "block.nerospace.nerosteel_block": "Block of Nerosteel", + "block.nerospace.nerosteel_ore": "Nerosteel Ore", "block.nerospace.oxygen_generator": "Oxygen Generator", + "block.nerospace.passive_generator": "Passive Generator", + "block.nerospace.raw_nerosium_block": "Block of Raw Nerosium", + "block.nerospace.rocket_fuel": "Rocket Fuel", + "block.nerospace.rocket_launch_pad": "Rocket Launch Pad", + "block.nerospace.rocket_launch_pad.report.3x3": "Pad cluster: %s block(s) — 3x3 pad formed (rockets can deploy)", + "block.nerospace.rocket_launch_pad.report.5x5": "Pad cluster: %s block(s) — full 5x5 formed", + "block.nerospace.rocket_launch_pad.report.heavy": "Pad cluster: %s block(s) — HEAVY LAUNCH COMPLEX online (12x fuel feed)", + "block.nerospace.rocket_launch_pad.report.need_gantry": "Add a Launch Gantry beside the 5x5 to complete the Heavy Launch Complex", + "block.nerospace.rocket_launch_pad.report.none": "Pad cluster: %s block(s) — no complete square yet (a rocket needs a full 3x3)", + "block.nerospace.rocket_launch_pad.report.t3_not_ready": "Tier 3 needs a Station Wall ring or a Heavy Launch Complex", + "block.nerospace.rocket_launch_pad.report.t3_ready": "Tier 3 ready: Station Wall ring or Heavy complex present", "block.nerospace.solar_panel": "Solar Panel", - "entity.nerospace.xertz_stalker": "Xertz Stalker", - "entity.nerospace.quartz_crawler": "Quartz Crawler", - "entity.nerospace.greenling": "Greenling", - "entity.nerospace.ruin_warden": "Ruin Warden", + "block.nerospace.station_floor": "Station Floor", + "block.nerospace.station_wall": "Station Wall", + "block.nerospace.trash_can": "Trash Can", + "block.nerospace.universal_pipe": "Universal Pipe", + "block.nerospace.xertz_quartz_ore": "Xertz Quartz Ore", + "container.nerospace.combustion_generator": "Combustion Generator", + "container.nerospace.item_store": "Item Store", + "container.nerospace.nerosium_grinder": "Nerosium Grinder", + "container.nerospace.passive_generator": "Passive Generator", + "container.nerospace.rocket": "Rocket", + "entity.nerospace.alien_villager": "Alien Villager", "entity.nerospace.cinder_stalker": "Cinder Stalker", + "entity.nerospace.ember_strutter": "Ember Strutter", "entity.nerospace.frost_strider": "Frost Strider", + "entity.nerospace.greenling": "Greenling", "entity.nerospace.meadow_loper": "Meadow Loper", - "entity.nerospace.ember_strutter": "Ember Strutter", + "entity.nerospace.quartz_crawler": "Quartz Crawler", + "entity.nerospace.rocket": "Rocket", + "entity.nerospace.rocket.arrived": "You have arrived on the planet", + "entity.nerospace.rocket.docked": "Docked at the Orbital Station", + "entity.nerospace.ruin_warden": "Ruin Warden", "entity.nerospace.woolly_drift": "Woolly Drift", - "entity.nerospace.alien_villager": "Alien Villager", - "item.nerospace.xertz_resonator": "Xertz Resonator", + "entity.nerospace.xertz_stalker": "Xertz Stalker", + "fluid_type.nerospace.rocket_fuel": "Rocket Fuel", "gas.nerospace.empty": "Empty", "gas.nerospace.oxygen": "Oxygen", + "gui.nerospace.rocket.launch": "Launch", + "item.nerospace.alien_core": "Alien Core", + "item.nerospace.alien_fragment": "Alien Fragment", + "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", + "item.nerospace.cindrite": "Cindrite", + "item.nerospace.drift_fleece": "Drift Fleece", "item.nerospace.frame_casing": "Frame Casing", + "item.nerospace.glacite": "Glacite", "item.nerospace.grav_striders": "Grav Striders", - "item.nerospace.drift_fleece": "Drift Fleece", - "block.nerospace.item_store": "Item Store", - "container.nerospace.item_store": "Item Store", - "block.nerospace.battery": "Battery", - "block.nerospace.fluid_tank": "Fluid Tank", - "block.nerospace.combustion_generator": "Combustion Generator", - "container.nerospace.combustion_generator": "Combustion Generator", - "block.nerospace.nerosium_grinder": "Nerosium Grinder", - "container.nerospace.nerosium_grinder": "Nerosium Grinder", - "block.nerospace.passive_generator": "Passive Generator", - "container.nerospace.passive_generator": "Passive Generator", - "block.nerospace.universal_pipe": "Universal Pipe", - "block.nerospace.trash_can": "Trash Can", - "block.nerospace.creative_battery": "Creative Battery" -} \ No newline at end of file + "item.nerospace.nerosium_dust": "Nerosium Dust", + "item.nerospace.nerosium_ingot": "Nerosium Ingot", + "item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe", + "item.nerospace.nerosteel_ingot": "Nerosteel Ingot", + "item.nerospace.oxygen_suit_boots": "Oxygen Suit Boots", + "item.nerospace.oxygen_suit_chestplate": "Oxygen Suit Chestplate", + "item.nerospace.oxygen_suit_cold_boots": "Cryo Suit Boots", + "item.nerospace.oxygen_suit_cold_chestplate": "Cryo Suit Chestplate", + "item.nerospace.oxygen_suit_cold_helmet": "Cryo Suit Helmet", + "item.nerospace.oxygen_suit_cold_leggings": "Cryo Suit Leggings", + "item.nerospace.oxygen_suit_heat_boots": "Thermal Suit Boots", + "item.nerospace.oxygen_suit_heat_chestplate": "Thermal Suit Chestplate", + "item.nerospace.oxygen_suit_heat_helmet": "Thermal Suit Helmet", + "item.nerospace.oxygen_suit_heat_leggings": "Thermal Suit Leggings", + "item.nerospace.oxygen_suit_helmet": "Oxygen Suit Helmet", + "item.nerospace.oxygen_suit_leggings": "Oxygen Suit Leggings", + "item.nerospace.oxygen_suit_t2_boots": "Tier 2 Oxygen Suit Boots", + "item.nerospace.oxygen_suit_t2_chestplate": "Tier 2 Oxygen Suit Chestplate", + "item.nerospace.oxygen_suit_t2_helmet": "Tier 2 Oxygen Suit Helmet", + "item.nerospace.oxygen_suit_t2_leggings": "Tier 2 Oxygen Suit Leggings", + "item.nerospace.raw_nerosium": "Raw Nerosium", + "item.nerospace.raw_nerosteel": "Raw Nerosteel", + "item.nerospace.rocket.deployed": "Rocket deployed on the launch pad", + "item.nerospace.rocket.pad_heavy_required": "A Tier 4 rocket launches only from a Heavy Launch Complex (full 5x5 pad with a Launch Gantry)", + "item.nerospace.rocket.pad_incomplete": "The launch pad is incomplete — a rocket needs a full 3x3 of Launch Pad blocks", + "item.nerospace.rocket.pad_occupied": "There is already a rocket on this pad", + "item.nerospace.rocket.pad_ring_required": "A Tier 3 rocket needs the 3x3 pad ringed with Station Wall — or a Heavy Launch Complex (full 5x5 pad with a Launch Gantry)", + "item.nerospace.rocket_fuel_bucket": "Rocket Fuel Bucket", + "item.nerospace.rocket_fuel_canister": "Rocket Fuel Canister", + "item.nerospace.rocket_tier_1": "Tier 1 Rocket", + "item.nerospace.rocket_tier_2": "Tier 2 Rocket", + "item.nerospace.rocket_tier_3": "Tier 3 Rocket", + "item.nerospace.rocket_tier_4": "Tier 4 Rocket", + "item.nerospace.xertz_quartz": "Xertz Quartz", + "item.nerospace.xertz_resonator": "Xertz Resonator" +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/launch_gantry.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/launch_gantry.json new file mode 100644 index 0000000..6ac36bb --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/launch_gantry.json @@ -0,0 +1,201 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 3, + 14, + 3 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 13, + 0, + 0 + ], + "to": [ + 16, + 14, + 3 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 13 + ], + "to": [ + 3, + 14, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 13, + 0, + 13 + ], + "to": [ + 16, + 14, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 3, + 7, + 3 + ], + "to": [ + 13, + 9, + 13 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 0 + ], + "to": [ + 16, + 16, + 16 + ] + } + ], + "textures": { + "particle": "nerospace:block/launch_gantry", + "side": "nerospace:block/launch_gantry", + "top": "nerospace:block/launch_gantry_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_launch_pad.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_launch_pad.json new file mode 100644 index 0000000..0a416e2 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/rocket_launch_pad.json @@ -0,0 +1,40 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 3, + 16 + ] + } + ], + "textures": { + "all": "nerospace:block/rocket_launch_pad", + "particle": "nerospace:block/rocket_launch_pad" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_1.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_1.json new file mode 100644 index 0000000..55417f5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_1.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/rocket_tier_1" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_2.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_2.json new file mode 100644 index 0000000..056f958 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_2.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/rocket_tier_2" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_3.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_3.json new file mode 100644 index 0000000..6c71fa9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_3.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/rocket_tier_3" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_4.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_4.json new file mode 100644 index 0000000..31298e9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/rocket_tier_4.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/rocket_tier_4" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry.png new file mode 100644 index 0000000000000000000000000000000000000000..7b01e1a532e9ee02d1824d473632f66942bc1ede GIT binary patch literal 391 zcmV;20eJq2P)P7lBXG{qWGQ9a*|LZj;iF|EP&8S}dVl!k)j3B9 zf$Df-SL8V7%+{nR-mgz3c*hhV&tFH<0oMBiO_l;swjH}7AB89)e13l12;i2mO7-5e zD{{)aohD1+$Fc^S=%Id(;Jvq2LI|eRLev9=TG$miW!ss2?>*jooBN*-SF0EEr9BUQ ziU=VD%C@sRpdlth`NN)vw@>BQ%LUc(G_srMfO^*9A%>po<%0G8U~>t4_Z(xiVG$WU lBCStE?CroGL)^~hd;uK{!_&l1-I)LY002ovPDHLkV1k3&vnv1q literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry_top.png new file mode 100644 index 0000000000000000000000000000000000000000..1d764ca542b2d957f8a8cfda449909468bdd5b79 GIT binary patch literal 436 zcmV;l0ZaagP)99j@s$d5a%pPMd*GGK($(u=!|{6 zAuq;Q(*UrqHvlLV@&3G~o0b;mj?*O8+X{dv?)hPoCpsf9#t1rEXKm6S^4-*2gUPJq zVHp4O2Wks2E`qKcS7i&)0Wq*X*f88bC&&fO;>ykRZOBY-yl+? zQ6SCYL?F$p&&5UIU#?WEpz&Yz}kpj`L`#IJ$7Z)mZ zX=#slFnx;He60a|{4WCe$Zm!Jp1MSVFcA8(gKk<{zj-MLRqUHX72}-6n&wRV2_3Do e_S#?gJpTbR2I_gZu&L1i0000-B`o3$LKtxv6(NNPDpCX!QUz5h{VXo?EO`Qx(nU%YS082(LLB%I zhgp{4aACLb-#2@APtpA(D=#SoCedz&2 z0+W1(HWq-Ub^zqlf~I!l(}LhzI`QPw0#}s)oQVX+$^f8^#Z@K2w~UqH?&*z0`T<6uS^?DQ#(Y$sRCD(Y_jK**I(Mj{gxeYU91w& ztXM2*YDckHg4-n7$*yejV=Ga~q1lr~;s9$qXZIrTl);>}oueUvJnO{MiN{rC+JWC; zaA@{S@)@JI?>_ZP=>)N<9d}2NX^fY109MZfvbb=;w*$mDi70DrhlJhn)d)=;M;BwY zu|t8lOr7{7(^VxaGL+ItRZ=XL>An+>NZ`=y|HEhc*YErjC@1nE@hP}#00000NkvXX Hu0mjfdS%y- literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t1.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t1.png new file mode 100644 index 0000000000000000000000000000000000000000..fbe19c664c1ae739d9e553b10c8ff63733a7202e GIT binary patch literal 2521 zcmai$c{J4f8^^zPk| z$xJHBHpy;iICf*5VayoJPxqYr_wSs~^M01|oadkS>yOW)o3_`*j>#Vb06@(8hNZ(% zj`` z7W`T{%VzldO^XP9?H~IVx2wA`GY!9G!1n_-v)<^ODdjp}x3C4unrrf0nb^%r4 zr09b~g?%`M%iR?UJl8<9g-!9CesiD*->+G>rT*1>@kcc4;rpqA-ph;=J>`GKM3}e_Q~;%tTJ?GxeSwDa=3hCeMb_rF?pT*v<<21`m)<(Us$iwC8-|1_*x`}q;X8Dzv3>>~h!fa6 z@2v!4H_pRTX6_1@MM^O2Atpp<6;i5mPDm(erpJjUOq#L?CQ!^Pj(Rv{Vk|Ve7GzjO z8ucSk;KC#Rpa&;VAt9&VPW8_-QD!tFbvJ{u!9h=pSRu|xBO|4FjcFQ_ow2|o=}qPP zq|1f-W+uh#Q#Q=7uqJ{*Ti_9_h-Y~o2GS>(sX+u;Q<;y(A37U<)!q7d>GQ8UF;42lw*Rtlkd#r0W3q4mRj;1s#~Ei^X)z$hseP&}#2%*6_N9@RwxdCz z@(fS()@N#Inyu|2sCistbt76jklUll6|v{`1Qq}HRB{E4yFKW}<_g$r^Nd>% z|59Ll%0~G^z>a7U^WZDH%~|Hho>AjO*=yy{LJ5 zxmS1NiL;WnoOIDfhPHn<+8uL7H&ybq=ul$P>_kSg?5Ax2ykP^3`qj2TsRvlOT$UTG4n)P8KDY7Y&O4 z<7@o|4=(2pDo*s_`#QK|d78$9H*|PSQ)Emi0%!?if65}Wwq9nPGEXTvJ~_$?4FsPd zOk9!79A0M69!@%AYDEH?(&Mb^*#HMbt#?6qB{)BTrzZ|<7k zyetTOM+k@&Qg`Ld5Ix94Nayqm$#mt_C}&QN$ny+3NBm6b{6wN(t$6$DVDG!W*~am7 ze2`>wQ9^PKzOgPa?nWr-j)EE~mDB^gZ^iq4*kdYA4I|168r zUNTN%iEkLIUmmr)HW%LHkg$*%y_V2 z3=-p8EOy|`bS<@jTXq+#58SVIQrWvAW9YUK@-2yR&-$Nycl)P}H#4cbAIloQY zD^uE*63F&1{%Uf>7J#mALs46*qPbm%?csxjdu^g8t4de(@v`15D>-Q7eCEb+2z)Ny zqF<(SL@{|@QYLBd)M{>K8wtlYDq1wT88EY`E>qlI-;|kKhGw|b3PHy?NGAKR4_rQ9 zkBo%~k$PJ8Pw6xYHUL$xR&m&XEvo=+wS2Us5eLNB>AenUcWjxKrHE%XWGI3`h6qpP6ldWu-|isYA*#Y zkom&l<#@*kAcAm?QHah5w@X|Kyj? aT;L9}L8`_zg?#j71FWrVEo-lO#r+RZiU*wl literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t2.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t2.png new file mode 100644 index 0000000000000000000000000000000000000000..b93f301e8a32f668a134518b4aa35932f9548e01 GIT binary patch literal 2789 zcmb7`c{J3G8pnTjFQ!qhCA%o1Fk;3&ma-+;iODwEWvMqB#AKQ3Wo#+xwKle_V;RfH zzGfR`CtD>n24RLV;d<{q=l*}s`8?n6^XGHE=ljR=!qUPR#3jrH0079;ofK!?mh}Si_z^`u5Ccxi1w~~8g}7~DsYFO^m+}nT!>0#k zu5Fccc-f{#i-gd=t%F0DLrXYyp{&&Y4`fZlJ~2b)QJvF$8B?8t2B-Ug9 zH9c1v^CK9>`d{1;-SvwC-VCs(XttUj{Emb|MM=NtcF&EkW|%)Xx&=uE@DBKxx_5h` zyR#4FuQ<7gzbY^&?+nQGMljid@*Wj9)pDj6H#tTmv?9tT&aD3FvlNtbJ1yMIaz~Gl zHesuM+kIkREUr}~*>3P(FJ7nu=n_qD&uYkN8mGG^4v& z_h5^ao(Q)IA)2s%Fni9(#;abB0O=IuVP5p?-#)*$Ju9Ni`%vdiAsPI7W^#wV%nIs9 z9@6#FKC=CTZ0whK0Y+MEl3iy-gpf@Y%J%xu$(Au(YWhp0IIH`^U6fqyOaNxt^K@&J zNJiby5e^RMiZyy^rkf(hh2B$k!x{mXV^dKTsnnYfBO^cAFycrhq{6N^HS5I|GA(n8 zNUYx3;MHSlTHn8foCB&MBT#L$xwxcGeW_)ML&#Wnudm4B^FlH`$`A33UF;lz@4MK= zD-{(-A(sNpT{bqOuGx`3iJOlX5E7*vTLr{}r!gS|pg0m4jTi zNc%Om4^F~|M_=^R$jxr)V1O%9Tq9`59cOd2qtZ+JqZAA5--$stcJUwP=Yxt$(Nrq- zedTKA@z)-1RpG&9p~e~}m)74GzS9rMRn~pjUX{ry*!Iqfpp8zKYI=Q`3)OzB3NY!T zEJpjpV{G_In92TJeqf0hqBid|IB5@5vIbF)&5DY*QJjOMvQ=J!6uNO9*T!hdLbVvb z0q3_ULF0YxM5jZj zUEA6j9@+IuOG2un_>x(+HF`}L0v55}>hXVBkaT*#<pS^K$8k8jLBFWkYIsTrO<*mEVR=!-dnLtmXm zCwd+2Y_7ugM-Ae^FJ&H;S3D6Nu4`u8Eqa<(kd&-@IN$yo`gKa_WOCBtF?%W92-y^L zED?2Z+g%ksnSkAffch1664*#PlAJ(Oqxwzkjr~ovaQRQAMC0MA?j4qptD{QM3$Kbw z&T1#eS#C?d^0PZB*&M_x%YVWYLp03jH%5WJK4TQT)7Ca1J&@OFIaVGnuymeZB=RrF zklqK=a?D%U0E5HWP(elDt=PC8?ThlOTP&$A?aCW5Hr@~9+kI+7aHS7_U{Rd7*Fi|; zqaDE>zD)0!v;ADfRz)QPgzBe`QLo<{Nmi{pv}WIyxEP(9*nui!M!-fkH^cIm{}cfC z`=MptBmMnl3)a7+yX9hYop15nm2T{7v|?2&Y=+IgCo3dhqi^uxR%D z_aV3<&>$*^c244MzgoPr*alPCWgyP=+NlH9lj;tX>G`X~#GBE4+2G`>++*u*S~#!b zV#6k|m$Os@RwV`W%gOzD{M{L4qD}v6Hbf5%$t`VRL9+4eVb%}rHTRz1Yq+N23A~{= z*Fz{o=#rEAp0J4gq#FC|E5$4|D6~bv$ZyuPO z_7p^-Skqdo_w38E!^JB#?rGe}1Z9P(_%pvxYC0ofLYlay1C=grMykClS+NXmtWQ~Q zr;g?gwyE8{4_;_CkwCmP9v~i%2s>)FlIcT}UWoV@qym!ed~(?j*^OcG7wO)lm|M`l z?`aDsxHi6Ios+DWjGKP6sdtFuzM_Pod3DlRBaS+=Wu)bFS

6Gmn4$QF=Flf*k@Q z)DXDpEc&>ad5QSerVS zm3~S#`b26DAt>*Xsr4KBP`1GdMx7G{9|f31J9fTF`t03y+=r;*IM^vB&4aRHKGR0Z zayEHtB7S*nE=aa5oB@Rvro6@}Bs~^el`JO&;V>V<7UsHEv++7#N>?Ws3QwM~`U|e1 z$jr>^^AJ^@nSRa7<4Q(=BoD+dx=~hHr{S0nlJrgee>B9W9<#dy*x|8Uvz96+`xl*i zyb7>Vnos;4xSTsi@ikkEO1g${K?(89P#ZAb~*V%%UM6!+=MEK59=JF{$=+u843uFjq9Cm&-4|q*uH0bUDa%8AN^O2dz=;5J#k3Y_jltcmQ-?o2Dt})u) zaIG7~Bpmq)4GS`A?Tug-cu>{S%rg5(j&`T)96VmtTlK-Bpzp8hBjxL z=U5bDPw^vQP6=#?b*jiYe=h4Q#Fm9u>&G=vqW~AQb(Yhj`n)MHou^0qG}C1#=SK+l ziJSs@yw`YxQFJH|(@)S^H5W&+ueN57*FBKiJbbCNp^VojwwXy?;-D4nY_Ss5Hp`{s z6stK)Cctc{WPBbbw==SiZBJJp`TKyoE&p1|=avX8Lan{0=Z!z`-#GqnwL0dw*D<6u Um$#X4`o03DH!QAK8o0&$8wXl&Bme*a literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t3.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t3.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9aba5ad1b24f33e067f355f58183296b81f0af GIT binary patch literal 3065 zcmbW3`#;l*AICooG1rq@E-_J*TNkp#ToO5Hl3RzF3DH>Y3p1uB<$md$V&-~8D@P0qO4!4s8D}w<5khH&IbK{^B z{+psA2lXAyrw;(cJMC?(T%rp&1Y4U>WI8WR4dH6zrng4w2eeVWfu)~8RViCGEJQE6vx7b+ow>u2?! ztSw#)SY3Q&oD&B_l`Bmvr7slX;-@3*u( z3-iCsrpE$DhVkjP=H`WKA=4gI3EVYyXmoUB)mYX3Q>4m%a!3c=oVn75rk1I`9}vu1 zLd-yC2Hj}n8JE=sSg|fN-S%jW+r{a^6I(kcMPoU!{i(K?Cl6khMf*mVwJ@-zpI2bu zPkK`bba|FSF-q^)C2=4m1KR*jbS-jYZbW3n7G7n3H>!Ud_=*!{sH}MhXj(ZXqfvsg z+c3{^=TA{g@E+AC81){Y!M)d#e$4{2@sgHjIl3gNOX37hT)TT)E%gaUu8Hdw0uN2fJ>uiiQKJ#f>r_pIYEDo|ih z%2~Q~sPfP|%N5n|gi5>;QPpD8(|WAqN}8Wwq|JC7A^tX5`(n=bO2ey=6o)7LY6X@%Iz*QFJsN5M6{-Bl1E6#Vl-L=Y^UQ3)ek;G zEmQACzqu@TKih#YfmPWPVA*72p-w!s@rRdUd0Nby{y~+rihmb}wx9ZYzlXkw=cB8q zc6)ldj%P`4f8Q0(+d_1?I`i0#3oS94y-DFA+go94tK_N0mpm?Ol8!gK>D~8r!;x`o zstu=YZqoGkKaNG~D7?POYgqhFL7_MKLuL_-{ViQ*!F}rWCc0YXMScB+(e(|uWH%+W z#@CNo#8I7GGQAa?ybe9sPZHSyT!knMx*==FMrXxPz<$2CyB0USuWl#JN0ap%hu;!e zFp^+}$e@(8zx*ZGeYM6%KCG8jumgmNlB+@a`pVY0sT{Z*3O+wfdegLdjL!A+H2fqR z92{K1W3@BU6rLkveu~Q~*)_lw*3>9gRRo?V<*m)vA7H=d!H`C$+S(31b?b^CQuEzD z&mLIE1e6IvNLMbHr$)3V?nsOdkMd3bZB_4%T}X8+K7*lJ?#of+WmHbovz60h?`~z1 zD@uw=7hIj4T>?=11?!apTq#V`#0B+Y9c{H`h`GA)wAY*XO8P_WM}eO~3}2&=>m$hi2j|pWi&bjnOP2GKV;3aa?V@an$m;o9RO<`DW6mVa!)J9d4k2sNBk%nu!ryUwTLa24#+~X%BYTEVZM+$&<^-!22efv7R=%2Gwjd`{t%$G1_ zwX+~4^gV; zh{rKY2%IAhECp$W#VSx{+H4i3LevkJY>JfRcg6{S8nC%*BX{{qj!@WCPsEQuk5&i@ zKsbRI1~yEULWWz-1?6Zd&FPR_K;wdG!;MhIfJSyOUVTNtXl5b4`PK7z2OXBopmpLW zxpghdgEptZ4nh+worj0n@GS6S+?j^5XGYS+++Wuvi|^7DUI;@xPE)x+|H*NW&aCru z+o6+g!j36LTSXOXQSSw(on~4e>Kx^uPrbwhs`n=rn)D64-+Ss*DMvX^&)58krzhQ_KqfmVq%^&dayt#UIYKa#?YiG!oV@ zCToe6>ij;rVeT!)C~3DG;`Ua&O@;|{vwR_*bWkK7`Tp99>@0fp5OGY!;k82XcPSkb z)ii3kDuVQr6z!w!d^6g>^dVKiXkVHbv2*z|hA$Lg_9+Dd@x0rQDQ?Meci&#NmNv~v zJMr@(H08ot2%~3vXL{?+XkMhPcCOe6d@%g(#aph~89(K%<{Kb~VV-H~ zDKAT0`Tbh1c}WWr58z7`y)mVt-DpvW>MHAf#)-I>X;bLK^JEJp^qV=lt(+aY+hmX^ zEfY(18*?GPZAQibyjECe4|CtI(kyP1*IIjMoWDm2;&f$R^~WvrA0Jz-5`$o0hIv_J zHE~~K9qp>G{WiILD`?G_O@UWMU5|lzt3cIUn>H+8Ie%W|EEE`)F5OZwu5_-vU(C^K z?i~k<6AGkfzoK&ICM=G9d(Acpv5fyUx3*+iHMVl@T#a2KrE!uuu z<^}ttL@GhNQ|5(fV0Ng$5t)oVY#aIH&a-<6K%^X5M{%`CEf+=r4?F*g(-%||mUuJ@ zGB}JECNMq;H+=OeFl%61Sz^K5xpzEwWy8sw^PbaYRRO#ke8YS|!cWj>Q{0c1fQd)a z5-5Bj>5Cy@@=Npc%QIX*h^$Qhqm%g`$k^xbx(g4jj>JX%oEhtoVkDT1hxw23w|yZ3 zqRhQXxU~L{XSPER@q>|-rLBqylSoGJWwLVGzHxOI4+R3 zoGZfClql`ixU1X{ei6w0hJj1nxR3VmYCowcd1p^red}D^2Hcw$86F;{bZSE|>;dNivHITI1AXnP;-<1hIc24CE)GSH8%zK4p2hPtqzQj(}wo zyLj@oF`65MM}vXVQOX3=k-#D)6xl{s>uzqJ_6E&|e%J)71)9_8v<0Vl-GG60O*--( zF~Z|_x{Rg2h-oxORP#rVBbq^&KU%;x*(mP(0IQfvrezd)J+V)511BqFP#+I&U%u^s zM6F};J^LQ%R}@d`K-#SQty~zqpR*bvQugz?&pQK7xa{da{&&H)vR5zPnaS!m%$GaH z-+ky)27o*3Chz?qt-%!AVXQr2oW-8}3S%!P#+%S%AJ0H}DenLY$ z<*cjAC?1fJnUl#>Jdxx&q^zNnzbKZJxO`6HZ1$HYm#(R|FG4fe2<@HN+3mHug}1d# zW=NLS0zgPj6{68T13*S*2kOoprM^DhD>)GegccM!9$}d9aLH4)dtem)^*kZc0?^af zXU~^9lVifN57>J2qGERTs7OODYaMGraM6V}Uh|ghbaHfMWJ^cd?ys*a0%R$7f!T$D z;9+-Q{f1?h@kgqkMrP@O=T vQN7Q@dDt=+$(BDyDew(gXIF;Wo9_sQdo`bdUDl literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t4.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t4.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e97839f0b8f5465e22e4a4b890c9511586a6eb GIT binary patch literal 3327 zcma)9`#%%@_us_kPA$nLq6?)j%4KdD6(w?sQZA!hrioFxO){6LWW6i58k35-3`J%x zi^5he8@U^!#4t0`Ol|mhf4`qU;QM%-^L#xXuXE1h@jSO5&O=w{vvSg^(f|NJ&f(md zi`y^eUzG%HW1IE0E&xE5>Tt%^BQ~E~a6CkPUWvA~%Ui!ISc)q7U9xuX?L0jnbt+l% ztf9V7p8w~k*JXBAN!yao&s^8@q^f&1L_}swrAI#1_Iau;d7xYCvsUWyj9w`%c`ehf znoFw1ioE{B&DG%OLtL*na~n2`i``J8$!@_%0c%?;XCuy=LSqz!8$hqh;IUzz!nf#( z!PI>`0XU>S_f`-&AD+VC2*B%$wH*!pLwu|CncO}tz;85jUS<8Ni_j|Cmse27be!Xp zfcxvZ$ie|YS>9m%gsV$=zwirF1|%g6ZJJj#D?ZP&d> zJQl@UnGRmQt37qj@NfdYWz*O!KbV3~V0DiR1`cE5Yms6`Kv2a22*{dFTO5nVdoF1g zor<^&F${B_sOVNR=|ocy;0vznu}{a4buY8`Npv@+S5<|pmj@7dDqk#8hz|S>IK`_F z3o_JcziI86rgtNI`6&J6srdU`1maiCMN{n`0$ye_^^X`~40fLqyf5 zX-8Kh$QjOIsm@_pB+sgW6O)?qF)I;d+-S?K;+#>0FgrS4N>9(_n~-}ar++iN9@iPw zH1V6|c^Utf>x?j`xLD?zKbLZis$$yQTU1yF9#GOS-Dwk5VB_BR$fCJXGw)bLZDV6( zNKi%CuGU#0dX43)`i*OsLJr=f*E}c0PZ5ypBD# zT1m9}>WeEZoUD_t>$)x*gk`@l!C<+AZ347_to!1n3?r^g+Dd&*%x`Uim0=NLLt>YM z{1M)k8E`*dD6R8UFX9^j?lNmn(m-A8UU2Tggok40v|HOev1wx@lV=2ahH=4YLcB7b zx;;tHNyp{-!wn8?C7C?CRbumxGzO~aYT7-WdtKr6X{t8!l9C5xVKu@9!Sopb{x**y z%l)JzRIeVne7PDHv$d&NVu7txg^8ByUr5sz7O2>=&z~qgB@bc83~*{<%wmbvP*7_Zy-!YT^j_VQZS#0@lCheQHFn7Az zkJzIRxB*R+ELA@qDkyIEHvfwWBbSi@tAPVeb$pj4%Lvpo^_SDDmtvV2d)Jzejz>R+ z@ik__rKbMK@vcAGn%3dP8Z_wp6VbL+X0X!tiM@;qE^ES+gXg^BnAJ}WqTcPrU!Sch z6ia|}C#*Es__B|-ZWPUy@mHv(QAT2TAPfjNfY6i{^zbx)_st(8NWag$A!KrkyRQsW zKLBpcBn4D^2yp30g>oi{SAQFJcl=Q|Oh*@7eWI}xHC(cArrMhFysY}q-bHaV7T(wE6#!n?@itY_Ab)=J*hdvd zVUODIbM7?!-=s$m0dBzjybq*D7|$<=->Jtt*xnJpr7Sid-6@~*J+()ZTBt|Lu_*>0aWeA3FG z=acgcBZy&WvkK`(O%oGWCe`&*{N?V23(CsOBMS()#Xp${I$_ZrF1`nOQGZbjt zdQk2wbMZw^5wc}c^YJCd!hVXkHzV`Jqf+!PhwMDCXeZ`1GVkPgWNB@%oL98IX1_=f z$sd%w>e_7;k)P@|v_D!jo5^XAEsQj1)cxWH!l|(|Vhr@WgW<^m&!qfK!su#mfSOcv zX^ci;S5bhxQ9L{Db7?tc?%tkv(p2{yHP}JUFzWduk|d6>aJ4{yX0FdNJsTQ!gi4wM zEvQ>7v_wMumB1&}0jJvBPFp@X@EjRz*1LPR5tQ4!R$dghW}9PVATOtHss+f+SHw6v zOST>&taS{zm@6=wLva&3uqm+FVsCRmgWC@9&6_RGxn8;>xJkCqE3OIaeC%p!Z}0eL zgnMGpmfzcJuk`0@8e%M~-0^9-OEX+gV1I($>N@MpGx|}qV7gJf;~-?}P}oQp$^2eH zhJj4aOPd>M)%Fr5)3k*jK^mOY-8AjT{b%+W7_GKOV*M(jvTDP3!r+nRdKb-f6wNv% ze=I8Lp&IVCuAKMmG5-(-0eH`R9%hI2xDD`Qy{vT)&xkK9;{O0_NC-6P%I&S#zx5#r zNjaXHnD6Q^Uw8>e^26pOUpEgWB&H?x;p*1t*(s`<_u^<=6Fyt;tuZdRPu*b+()ml_ z%7eozSCfyeHX^}a4&NOiP&g4mHxzA z3ji@>7~KI0toLDGv@W+}kpR@oyaJ1!%zDmz0wvJVn&AY3$WMLE+ggsS_<%W-{H|wT zI#RL?T_wL|ypdrdk5HU{fRSLh3$f%4% z&+h-53V5so0rx7GRzx?m5HnT^6I1r?T2pw=q1$M4%uUnvrtHl8)~_#cYu`%BiySMH zkJafEZb6mX*jFg}2EhjTgLnEYHXN?6-}Qy7m3mQEkC>&L8{QTBuji@_tLP$=YeL2& zBF1e5oH3XQ6rYQt7X|ISeRAaNiYlxonFfLFgoxWJ{;x!o<;6Z|-_mh)C&zjda#XNt z9(?wsff|Hx-}{|i0M`OXGm3-jYst7qY&snS46VaI>q5!7*JQI8>L50)U99!CXq;^I zoTe3c;l%g+bb=NgZJiG^h!cGYJRW)zBU;89YiOilVxMqQ>n06GO z0@{?QOoS5eK#96@%r~KI^||-FMg?6zv)A9RX;`3F#N^Z}`uBzkgB|}?PSxC3!9!>P zi5J#?g4Xiw408N;7pJTB=na>=eh(5_YFz;!?JB~^^~j$Lta4Z=!=UER%g3FBAU-i&?=Q@987x-akO=dQjm%B2xabk0I)Fq2zpYZA}@NlUVL+iwCS z0zc$3tv5W6d$D>qh$`#%g2enO&2y`LxWY(&Ok!sO=;}9J%%%6`!5cSXxdll5-(cJ$ zu~V#*>#8=Jup4O*GvD!Zuc*sO#DEexdISseSLyCatOi=lJqxBN2lYkPZW7M^x7qx> zkvAWsICL$*04MW9bzjJJ6bdXOGi<0XZ>TCa5vJ!^4ebpv2DaC5%MBx~0Z>thaeIjI zovuYL*WXyR#q0Nt~vs+i|9l$s)tV0X12 zxz_f5ze#?WLV8N~iv61xsw{Lv*#?@jGtZ1<}yTV^=x*;*B$cm|QG_;)E vqs+`60yH&IdIA3}=l^Zc{|`R}{Y+fGu^toX=F9@=yK`j)alu literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/rocket.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/rocket.png new file mode 100644 index 0000000000000000000000000000000000000000..576d988b3fab778fe34cef93facb29b1ad14085d GIT binary patch literal 1604 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJDXZci7-kcv5P@9xbHO_gE) z5bVrxgQ@EXBd=PUkOz-qsfuD^dO||Mf>R;F5F5LdS+dqH)=eO_o+B((2zmFe!6LYSRL9&GD!EASaHNCPoM`}L)-9BA?o$|U5hqdFd^uhJOU;^|x%=;b&J`(r9&b}_ce|MX^DOtj zdrcg}8DcDJw(KbSk#U}P-=8ndLF^`PKFeAg+6Ue}yG>p-Cn(2!UGMquYkXl}&zqiD zu=C^#w|5M$k}l;XZ+?56ce{~u;pVvoSDs6kyRWl)v+~}R&8rzS&oO>^{9})#-~7E7 zI%NMp`JxxjC^TaQbH?qDKMmSNE&I2xzR)sveu)}a!#3Y(j_sny{mz&xok)n7I+uCI zmnE^Gzy7wKuk1A5dS$N9ZR^F`R_QRSY z$F%I?cIFk%x8t5l6#d<^xWu*e$J{T$-#>1g$JgL?iE+ur&CDx)Z?yXPL;Jp*E%Soh z=|4P{`-VF&dc8;bg6%bvlkMiyiuf0bS}{IgZLP3k{BTwN`*qt5pWe@(%Fk9c^XAV} zyZ-OroSwgI;+E6X;`noF4mN(vd~>_8@ucl_#x>8RStqpeF)#@;2+Uw;IKaZdqQ;Mu!9@2BuV9b7f~}L|C|4fn#g3UfA&_ z1(mJ;FHF4SoWOFA$HmCl`n$;fdr{|XdE6ezz}>l1RD$U;#Jd=w0W~uFHRGDH`s zJdB^p$7W`{H@WXwtCfbTF{|#*|xmdk+C!8YU|zK zfAi~udhPmj386LLADuZJ8$bL0@3g|g%8eDjQc|@JPl!6)!;jya_iqiy z5w0Al~!*J7fY-J*?G zGF@a(JycNurLYap@9{jz(l$-ao92cRW2hm|-4sy$EUmHx81Jr*nGD67b;4MJmJ60@ zxTm`=(0>x47o8ZFpdua#uXBNlcKZS=n80a=wGW`S3P7 ze$_o}5564A{2f}bO(#ydfFX%laJBe9ch!W918T<(G#uwHEcDd)<0GgyXWh{aoYT2C zbQ(W0$(VJlo~@mMim^U^(-$*}2cOH8Bao{Eo8?RorXZ(~}{sPls-@_Xy-1+Eft4%^?Jn0VUa=?19= zo~DX@Mqes59+oot>38T?F{P&)Nf@L|%sOz|Leir8?k-)6*oLdzv#&l+{2@A9B2N6s zQPqVXiY09}tU7GqQ1-~24G7v|cZV4+l34KWqr6%>kA$Jif(QRidjIqAF$$b(gTe~DWM4fIdx+v literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_3.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_3.png new file mode 100644 index 0000000000000000000000000000000000000000..b5138b565ec899bb2a53bff4908b9153cdafe6f9 GIT binary patch literal 272 zcmV+r0q_2aP)$PdM=s-(!)gFB2;%w5(C49qPYG@>a6E{Cv!jd{7_vw;;D^EzID6f? zEes3{8?dUw>V=ADJs5t)CWhAqadAl~e4_v}tm-ffcogu2fq{X6Vb`uzDDiH@VD|q} zz!QwXz?qQ#Pt*9%;1B@e9N56Xm~{iETAbkjG>!lNr)l8LOgO`Ufq{YHvl^3 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_4.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_4.png new file mode 100644 index 0000000000000000000000000000000000000000..99d02e46bdd401d920493d433dccf7ea472dbf73 GIT binary patch literal 250 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`2R&UJLn`JZCoE94`1JDsf93iA z{vTi7)*0koAk~m#Y!`0uXwr`_?4iQ)+{$5Fb$0Ol`2N)V@Era%JRso2zU@&^+soEU z?rjTh)Vw>8I;SXt*MUuwX}{z$){FG3QtSap>1 v-}&I5|Lf;*-8<7_%p(B=dh%7y7lasIZ}M3_L-;Z?&@&94u6{1-oD!M<{ApsL literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/launch_gantry.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/launch_gantry.json new file mode 100644 index 0000000..49cb024 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/launch_gantry.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:launch_gantry" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/launch_gantry" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/rocket_launch_pad.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/rocket_launch_pad.json new file mode 100644 index 0000000..9d0be7b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/rocket_launch_pad.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:rocket_launch_pad" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/rocket_launch_pad" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/launch_gantry.json b/multiloader/common/src/main/resources/data/nerospace/recipe/launch_gantry.json new file mode 100644 index 0000000..e6d267c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/launch_gantry.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "I": "minecraft:iron_bars", + "N": "#c:ingots/nerosteel", + "S": "nerospace:station_wall" + }, + "pattern": [ + "N N", + "NIN", + "NSN" + ], + "result": { + "id": "nerospace:launch_gantry" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_launch_pad.json b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_launch_pad.json new file mode 100644 index 0000000..2eebfd7 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_launch_pad.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "B": "#c:storage_blocks/nerosium", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NNN", + "NBN", + "NNN" + ], + "result": { + "id": "nerospace:rocket_launch_pad" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_1.json b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_1.json new file mode 100644 index 0000000..7dbc87a --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_1.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/nerosteel", + "C": "nerospace:rocket_fuel_canister", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + " N ", + "NCN", + "NBN" + ], + "result": { + "id": "nerospace:rocket_tier_1" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_2.json b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_2.json new file mode 100644 index 0000000..08bd06b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_2.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/nerosteel", + "C": "nerospace:rocket_fuel_canister", + "N": "#c:ingots/nerosteel", + "T": "nerospace:rocket_tier_1" + }, + "pattern": [ + "NTN", + "NCN", + "NBN" + ], + "result": { + "id": "nerospace:rocket_tier_2" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_3.json b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_3.json new file mode 100644 index 0000000..0548b9e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_3.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/nerosteel", + "C": "nerospace:rocket_fuel_canister", + "D": "nerospace:station_wall", + "N": "#c:ingots/nerosteel", + "T": "nerospace:rocket_tier_2" + }, + "pattern": [ + "NTN", + "DCD", + "NBN" + ], + "result": { + "id": "nerospace:rocket_tier_3" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_4.json b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_4.json new file mode 100644 index 0000000..74a4b16 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/rocket_tier_4.json @@ -0,0 +1,19 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "equipment", + "key": { + "B": "#c:storage_blocks/nerosteel", + "C": "nerospace:rocket_fuel_canister", + "G": "#c:gems/cindrite", + "N": "#c:ingots/nerosteel", + "T": "nerospace:rocket_tier_3" + }, + "pattern": [ + "NTN", + "GCG", + "NBN" + ], + "result": { + "id": "nerospace:rocket_tier_4" + } +} \ No newline at end of file diff --git a/multiloader/fabric/build.gradle b/multiloader/fabric/build.gradle index e39e2bb..cd9b779 100644 --- a/multiloader/fabric/build.gradle +++ b/multiloader/fabric/build.gradle @@ -61,3 +61,18 @@ loom { } } } + +// Loom creates each run's `runDir` lazily on first launch, but VS Code opens the +// debug terminal in that cwd BEFORE the program starts — so a never-yet-run config +// fails with "Starting directory (cwd) ... does not exist". Pre-create the dirs so +// the cwd always exists. Wired into (a) the Fabric preLaunchTask `configureLaunch` +// (runs before each launch) and (b) eclipse.synchronizationTasks (fresh IDE import). +def createFabricRunDirs = tasks.register('createFabricRunDirs') { + description = 'Pre-create Loom run directories so the IDE debug-terminal cwd exists before first launch.' + doLast { + file('runs/client').mkdirs() + file('runs/server').mkdirs() + } +} +eclipse.synchronizationTasks createFabricRunDirs +tasks.matching { it.name == 'configureLaunch' }.configureEach { dependsOn createFabricRunDirs } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 6c0fd5a..ce86f23 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -12,6 +12,7 @@ import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; /** Fabric client entry point — screen + entity-renderer registration. */ @@ -23,6 +24,7 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); MenuScreens.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); MenuScreens.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); + MenuScreens.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { @Override diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle index 613dac7..8eb0fbf 100644 --- a/multiloader/neoforge/build.gradle +++ b/multiloader/neoforge/build.gradle @@ -59,6 +59,15 @@ neoForge { server() gameDirectory = file('runs/server') } + + // Stop ModDevGradle from (re)generating absolute, machine-specific + // "neoforge - Client/Server" entries into multiloader/.vscode/launch.json + // on every Gradle/IDE sync. The committed relative configs + // (${workspaceFolder} + preLaunchTask) are the source of truth. + // Mirrors the root build's runs.configureEach { disableIdeRun() }. + configureEach { + disableIdeRun() + } } mods { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 3e3a3b9..7736a72 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -17,6 +17,7 @@ import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.fluid.ModFluids; import za.co.neroland.nerospace.registry.ModMenuTypes; @@ -45,6 +46,7 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); event.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); event.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); + event.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); } /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ diff --git a/wiki/Block-of-Glacite.md b/wiki/Block-of-Glacite.md index 5a75aa4..26298f4 100644 --- a/wiki/Block-of-Glacite.md +++ b/wiki/Block-of-Glacite.md @@ -12,6 +12,5 @@ A pale, ice-blue storage block of the Glacira crystal. - **Unpack:** craft the block alone to get **9 Glacite** back. ## Details - - ID: `nerospace:glacite_block` - Tool: pickaxe, iron tier · Drops: itself From d6714bd30d74aee7367b88de24c0cee503decbc5 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:03:41 +0200 Subject: [PATCH 36/82] Add fuel tank and refinery machines Port fuel-related machines to the multiloader: add FuelTank and FuelRefinery blocks, block entities, menus and client screens, plus UI helper (segmented gauge) in TexturedContainerScreen. Register block entities/items/menus and wire energy/fluid/item hookups using shared EnergyBuffer/FluidTank APIs. Add assets (models, textures, blockstates, GUIs), loot tables and recipes, and four lang keys. Also update docs and CI pins/status to reflect NeoForm/NeoForge/Fabric 26.2 availability (pin NeoForge 26.2.0.6-beta) and mark fuel machines as ported in the port-checklist and multiloader README. --- .github/workflows/multiloader.yml | 6 +- docs/MULTILOADER.md | 22 +- docs/MULTILOADER_PORT_CHECKLIST.md | 23 +- multiloader/README.md | 16 +- .../nerospace/client/FuelRefineryScreen.java | 46 ++ .../nerospace/client/FuelTankScreen.java | 36 ++ .../client/TexturedContainerScreen.java | 29 ++ .../nerospace/machine/FuelRefineryBlock.java | 75 ++++ .../machine/FuelRefineryBlockEntity.java | 267 +++++++++++ .../nerospace/machine/FuelTankBlock.java | 125 ++++++ .../machine/FuelTankBlockEntity.java | 328 ++++++++++++++ .../nerospace/menu/FuelRefineryMenu.java | 118 +++++ .../neroland/nerospace/menu/FuelTankMenu.java | 59 +++ .../nerospace/registry/ModBlockEntities.java | 10 + .../nerospace/registry/ModBlocks.java | 13 + .../neroland/nerospace/registry/ModItems.java | 4 +- .../nerospace/registry/ModMenuTypes.java | 10 + .../nerospace/blockstates/fuel_refinery.json | 7 + .../nerospace/blockstates/fuel_tank.json | 7 + .../assets/nerospace/items/fuel_refinery.json | 6 + .../assets/nerospace/items/fuel_tank.json | 6 + .../assets/nerospace/lang/en_us.json | 4 + .../nerospace/models/block/fuel_refinery.json | 72 +++ .../nerospace/models/block/fuel_tank.json | 425 ++++++++++++++++++ .../textures/block/fuel_refinery.png | Bin 0 -> 509 bytes .../nerospace/textures/block/fuel_tank.png | Bin 0 -> 467 bytes .../textures/block/fuel_tank_core.png | Bin 0 -> 271 bytes .../nerospace/textures/gui/fuel_refinery.png | Bin 0 -> 5771 bytes .../nerospace/textures/gui/fuel_tank.png | Bin 0 -> 5682 bytes .../loot_table/blocks/fuel_refinery.json | 21 + .../loot_table/blocks/fuel_tank.json | 21 + .../data/nerospace/recipe/fuel_refinery.json | 18 + .../data/nerospace/recipe/fuel_tank.json | 17 + .../nerospace/fabric/NerospaceFabric.java | 19 + .../fabric/NerospaceFabricClient.java | 4 + multiloader/gradle.properties | 4 +- .../neoforge/NeoForgeCapabilities.java | 28 ++ .../neoforge/NeoForgeClientSetup.java | 4 + 38 files changed, 1820 insertions(+), 30 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelRefineryScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelTankScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelRefineryMenu.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelTankMenu.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_refinery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/fuel_refinery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/fuel_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_refinery.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_refinery.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_refinery.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_tank.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_refinery.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/fuel_refinery.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/fuel_tank.json diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml index 73d5d87..5b07d7b 100644 --- a/.github/workflows/multiloader.yml +++ b/.github/workflows/multiloader.yml @@ -6,11 +6,11 @@ name: Multiloader Build # # STRICT: there is no continue-on-error, so ANY matrix cell that fails to build # fails the whole workflow. All four loader x version cells are verified buildable -# from public artifacts (gradle MCP, 2026-06-19): +# from public artifacts (gradle MCP, 2026-06-20): # - neoforge @ 26.1.2 -> NeoForge 26.1.2.76 -# - neoforge @ 26.2 -> NeoForge 26.2.0.3-beta (now on the public NeoForged Maven) +# - neoforge @ 26.2 -> NeoForge 26.2.0.6-beta (now on the public NeoForged Maven) # - fabric @ 26.2 -> Fabric Loom 1.17 + fabric-api 0.152.1+26.2 (de-obf, no mappings) -# - fabric @ 26.1.2 -> Fabric Loom 1.17 + fabric-api 0.150.0+26.1.2; needs the access +# - fabric @ 26.1.2 -> Fabric Loom 1.17 + fabric-api 0.151.0+26.1.2; needs the access # widener (fabric/src/main/resources/nerospace.accesswidener) because # vanilla MC 26.1.2 kept BlockEntityType's constructor private # (Mojang made it public in 26.2; NeoForge widens it on both). diff --git a/docs/MULTILOADER.md b/docs/MULTILOADER.md index ab52c59..7fcb592 100644 --- a/docs/MULTILOADER.md +++ b/docs/MULTILOADER.md @@ -34,24 +34,22 @@ Hard facts gathered while trying to unblock 26.2, so the next person doesn't re- - **Both native toolchains support de-obf 26.x; architectury-loom is the only one that doesn't** — confirmed: the root NeoForge ModDevGradle build compiles against 26.1.2, and Fabric's official template builds 26.2. -- **26.2 Maven reality (checked 2026-06-18) — what is and isn't published.** Gradle compiles against *published artifacts*, not a git branch, so what matters is which jars are on the NeoForged/Fabric Maven: - - **NeoForm 26.2: published** (`net.neoforged:neoform`, latest `26.2-snapshot-8-1`). NeoForm is the de-obfuscated vanilla base the MultiLoader-Template's `common` (ModDevGradle) compiles against — so **`common` builds on 26.2.** +- **26.2 Maven reality (checked 2026-06-20) — what is and isn't published.** Gradle compiles against *published artifacts*, not a git branch, so what matters is which jars are on the NeoForged/Fabric Maven: + - **NeoForm 26.2: published** (`net.neoforged:neoform`, pinned `26.2-1`). NeoForm is the de-obfuscated vanilla base the MultiLoader-Template's `common` (ModDevGradle) compiles against — so **`common` builds on 26.2.** - **Fabric 26.2: published** (Fabric Loader `0.19.3` + Fabric API `0.152.1+26.2`) — so **`fabric` builds on 26.2.** - - **NeoForge loader userdev 26.2: NOT published** (`net.neoforged:neoforge` metadata still tops out at `26.1.2.76`) — so the **`neoforge` module is the one blocked cell.** - - The `neoforged/NeoForge` `26.2.x` branch *exists and is buildable*, but Gradle needs its compiled userdev jar in a resolvable repo. CI hasn't pushed it to the public releases Maven yet — so either **self-build it** (clone `26.2.x` → `./gradlew publishToMavenLocal` → add `mavenLocal()` → set `neo_version_26.2` to the published version) or wait for NeoForge's CI (likely soon, given NeoForm 26.2 is already up). So 3 of 4 cells (common + both Fabric cells) build on 26.2 today; only NeoForge-26.2 needs a self-built or awaited userdev artifact. + - **NeoForge loader userdev 26.2: published** (`net.neoforged:neoforge`, latest `26.2.0.6-beta`) — so **`neoforge` builds on 26.2** from the public NeoForged Maven; no self-build needed. + - All four cells (common + both Fabric cells + NeoForge) build on 26.2 from public artifacts. (Historically NeoForge 26.2 lagged NeoForm; if a future pin ever fails to resolve, fall back to self-building the `26.2.x` branch → `./gradlew publishToMavenLocal` → `mavenLocal()` → set `neo_version_26.2`.) - **Build-unblock ≠ mod port.** Getting a Fabric 26.2 jar to *compile* is separate from porting Nerospace's NeoForge-specific systems (capabilities/transfer, attachments, fluids, networking) to Fabric — that migration (§2) is the real effort and is unchanged by the toolchain choice. -**Status: IMPLEMENTED and verified (2026-06-18).** The `multiloader/` scaffold now +**Status: IMPLEMENTED and verified (2026-06-20).** The `multiloader/` scaffold now uses the MultiLoader-Template layout (ModDevGradle `common` on NeoForm + Fabric Loom `fabric` + ModDevGradle `neoforge`) — architectury-loom is gone. -`./gradlew :common:build :fabric:build -Pminecraft_version=26.2` is **BUILD -SUCCESSFUL** on this machine: `common` against NeoForm `26.2-1`, `fabric` against -Fabric Loom `1.17.11` + Fabric API `0.152.1+26.2` (no `mappings`). The **`neoforge` -cell** is the only one still pending — it needs the NeoForge 26.2 userdev -(pinned to the `26.2.0.1-beta` Maven default, or self-build `26.2.x` to -`mavenLocal()`); swapping to the official jar when it lands is a one-line version -change. See `multiloader/README.md` for the per-cell status and the self-build step. +`./gradlew :neoforge:build :fabric:build -Pminecraft_version=26.2` is **BUILD +SUCCESSFUL** on this machine: `common`/`neoforge` against NeoForm `26.2-1` + +NeoForge `26.2.0.6-beta`, `fabric` against Fabric Loom `1.17.11` + Fabric API +`0.152.1+26.2` (no `mappings`). All four loader × version cells now build from +public artifacts. See `multiloader/README.md` for the per-cell status. ### Field-notes sources diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 74308d7..455f92d 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,18 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~102 classes ported, ~162 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~110 classes ported, ~154 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-20 update — fuel machines ported.** All 4 cells green. Added 8 classes: +> `machine/{FuelTankBlock, FuelTankBlockEntity, FuelRefineryBlock, FuelRefineryBlockEntity}` + +> `menu/{FuelTankMenu, FuelRefineryMenu}` + `client/{FuelTankScreen, FuelRefineryScreen}`, registered +> the 2 blocks / BEs / menus / block-items and wired Energy/Item/Fluid caps on both loaders. Rebuilt on +> the shared `FluidTank` + `EnergyBuffer` + vanilla `WorldlyContainer` slots (no `MachineItemHandler` in +> the multiloader); `Tuning` values inlined. Assets + 4 lang keys copied. The Fuel Tank closes the loop +> with the rockets batch: refinery (coal + blaze powder + power → fuel) → pipe → fuel tank → auto-fuels a +> padded rocket. + > **2026-06-20 update — rockets (core) ported.** All 4 cells green. Added 17 classes: > `rocket/{RocketTier, Destinations, LaunchPadMultiblock, RocketLaunchPadBlock, LaunchGantryBlock, > RocketItem, RocketEntity, RocketMenu}` + `client/{RocketModel, RocketT2/T3/T4Model, RocketRenderState, @@ -67,9 +76,15 @@ checked by a headless build). `MinerTier`, `OutputFilter`, `PlanetMiningProfile`, `QuarryChunkLoader`. Risk: **high** (chunk-loading, fake-player-style mining, multiblock). Chunk-loading needs a cross-loader seam (NeoForge ticket API vs Fabric). -### Fuel machines (`machine/Fuel*` — depends on the ported rocket-fuel fluid) -- [ ] `FuelTankBlock`(+BE +menu), `FuelRefineryBlock`(+BE +menu) + their screens — refine inputs → rocket fuel, - pump into a docked rocket. Rebuild on cross-loader `FluidTank` (root uses NeoForge transfer). +### Fuel machines (`machine/Fuel*` — depends on the ported rocket-fuel fluid) — **DONE (4 cells green)** +- [x] `FuelTankBlock`(+BE +menu +screen): stores `rocket_fuel`, accepts buckets/canisters, auto-fuels a + rocket on an adjacent pad (4x on a full 3x3, 12x on a Heavy complex), comparator out. Rebuilt on the + shared `FluidTank`; canister slot is a vanilla `WorldlyContainer` (Item cap on both loaders); Fluid cap + exposed for pipe filling. Pump FX uses a vanilla sound (root's `ModSounds.FUEL_TANK_PUMP` alias not ported). +- [x] `FuelRefineryBlock`(+BE +menu +screen): coal/charcoal + blaze powder + grid power → liquid + `rocket_fuel` over a work cycle; Energy (insert-only) + Fluid (extract) + Item caps on both loaders. + Rebuilt on `EnergyBuffer` + `FluidTank` + a vanilla `WorldlyContainer`; `Tuning` values inlined. + Assets (textures, models, blockstates, loot, recipes) + 4 lang keys copied. ### Atmosphere / terraforming (`world/Oxygen*`, `world/Terraform*`, `machine/Terraform*`, `HydrationModule`) - [ ] Oxygen field (airless-dimension survival): `OxygenField`, `OxygenFieldManager`, `OxygenFieldEvents`, diff --git a/multiloader/README.md b/multiloader/README.md index 5169a4c..41b96e7 100644 --- a/multiloader/README.md +++ b/multiloader/README.md @@ -7,15 +7,15 @@ from one shared codebase, on the **de-obfuscated Minecraft 26.x** toolchain. > repo root is untouched. This is a parallel scaffold you promote to the root > when ready. Full migration plan: [`docs/MULTILOADER.md`](../docs/MULTILOADER.md). -## Status (verified 2026-06-18) +## Status (verified 2026-06-20) Built via the gradle MCP on this machine: | Cell | Toolchain | 26.2 | 26.1.2 | | --- | --- | --- | --- | | `common` | ModDevGradle (NeoForm) | ✅ builds (`26.2-1`) | NeoForm `26.1.2-1` | -| `fabric` | Fabric Loom `1.17.11` | ✅ **builds** (`fabric-api 0.152.1+26.2`) | ✅ builds (`fabric-api 0.150.0+26.1.2`; needs access widener — see below) | -| `neoforge` | ModDevGradle (NeoForge) | ✅ builds (`26.2.0.3-beta`, on public NeoForged Maven) | NeoForge `26.1.2.76` | +| `fabric` | Fabric Loom `1.17.11` | ✅ **builds** (`fabric-api 0.152.1+26.2`) | ✅ builds (`fabric-api 0.151.0+26.1.2`; needs access widener — see below) | +| `neoforge` | ModDevGradle (NeoForge) | ✅ builds (`26.2.0.6-beta`, on public NeoForged Maven) | NeoForge `26.1.2.76` | `./gradlew :common:build :fabric:build -Pminecraft_version=26.2` → **BUILD SUCCESSFUL**. @@ -66,9 +66,9 @@ Jars land in `multiloader//build/libs/`. ## All four cells build (was: NeoForge 26.2 pending) -NeoForge's own loader userdev for 26.2 may already be on Maven as a beta -(`neo_version_26.2=26.2.0.1-beta` is pinned — the official MultiLoader-Template's -default). If it doesn't resolve yet, **self-build it** until it publishes: +NeoForge's own loader userdev for 26.2 is on the public Maven as a beta +(`neo_version_26.2=26.2.0.6-beta` is pinned and resolves from the NeoForged +Maven). If a future pin ever fails to resolve, **self-build it**: ```bash git clone https://github.com/neoforged/NeoForge && cd NeoForge @@ -112,14 +112,14 @@ build. Until then the root build remains the source of truth. - [jaredlll08/MultiLoader-Template](https://github.com/jaredlll08/MultiLoader-Template) · [official Fabric example (de-obf)](https://github.com/FabricMC/fabric-example-mod) - [NeoForm](https://projects.neoforged.net/neoforged/neoform) · [NeoForge](https://projects.neoforged.net/neoforged/neoforge) · [Fabric develop](https://fabricmc.net/develop) -## Build matrix status (2026-06-19) +## Build matrix status (2026-06-20) All four loader × version cells build from **public artifacts** (verified via the gradle MCP), and CI (`.github/workflows/multiloader.yml`) builds all four strictly (any failure fails the run): | | 26.1.2 | 26.2 | | --- | --- | --- | -| **neoforge** | ✅ `26.1.2.76` | ✅ `26.2.0.3-beta` (public Maven) | +| **neoforge** | ✅ `26.1.2.76` | ✅ `26.2.0.6-beta` (public Maven) | | **fabric** | ✅ (access widener) | ✅ `fabric-api 0.152.1+26.2` | Fabric @ 26.1.2 needs `fabric/src/main/resources/nerospace.accesswidener` because vanilla diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelRefineryScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelRefineryScreen.java new file mode 100644 index 0000000..7313fad --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelRefineryScreen.java @@ -0,0 +1,46 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.menu.FuelRefineryMenu; + +/** + * Screen for the Fuel Refinery: a power gauge, a refining-progress arrow between the carbon and + * catalyst slots, and a fuel-output gauge with a millibucket readout. + */ +public class FuelRefineryScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/fuel_refinery.png"); + private static final int ACCENT = 0xFFF0A030; // fuel orange + private static final int FLAME = 0xFFF0703C; // refining heat + + public FuelRefineryScreen(FuelRefineryMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 166); + this.titleLabelX = 10; + this.inventoryLabelX = 10; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int energy = this.menu.getEnergy(); + int max = this.menu.getMaxEnergy(); + int pct = max == 0 ? 0 : energy * 100 / max; + float frac = max == 0 ? 0f : (float) energy / max; + + label(g, Component.literal("Power: " + pct + "%"), 8, 20, 0xFFFFE0B0); + segGauge(g, 8, 31, 160, 6, frac, ACCENT); + + // Refining-progress arrow between the two input slots. + hGauge(g, 78, 40, 22, 4, this.menu.getScaledProgress(1000) / 1000f, FLAME); + + int cap = this.menu.getFuelCapacity(); + float fuelFrac = cap == 0 ? 0f : (float) this.menu.getFuel() / cap; + fluidGauge(g, 8, 56, 160, 10, fuelFrac, ACCENT); + label(g, Component.literal(this.menu.getFuel() + " / " + cap + " mB"), 8, 68, 0xFFB9C6D4); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelTankScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelTankScreen.java new file mode 100644 index 0000000..f38030b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FuelTankScreen.java @@ -0,0 +1,36 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.menu.FuelTankMenu; + +/** + * Screen for the Fuel Tank: a large sci-fi fuel gauge with a percentage + millibucket readout. + * Filling is still done by right-clicking the block with a fuel bucket/canister. + */ +public class FuelTankScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/fuel_tank.png"); + private static final int ACCENT = 0xFFF0A030; // fuel orange + + public FuelTankScreen(FuelTankMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 166); + this.titleLabelX = 10; + this.inventoryLabelX = 10; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int pct = this.menu.getFuelPercent(); + float frac = pct / 100f; + + label(g, Component.literal("Fuel: " + pct + "%"), 8, 20, 0xFFFFD9A0); + fluidGauge(g, 8, 31, 160, 12, frac, ACCENT); + label(g, Component.literal(this.menu.getFuel() + " / " + this.menu.getCapacity() + " mB"), 8, 50, 0xFFB9C6D4); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java index 4cb9ef8..feb5228 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java @@ -68,6 +68,35 @@ protected void hGauge(GuiGraphicsExtractor g, int dx, int dy, int w, int h, floa } } + /** + * A segmented gauge: the trough divided into ticked cells with an animated leading edge — energy + * buffers read at a glance, exact values stay on the labels. + */ + protected void segGauge(GuiGraphicsExtractor g, int dx, int dy, int w, int h, float frac, int fill) { + int x = this.leftPos + dx; + int y = this.topPos + dy; + g.fill(x - 1, y - 1, x + w + 1, y + h + 1, INK); + g.fill(x, y, x + w, y + h, TROUGH); + int segments = Math.max(4, w / 10); + int segW = w / segments; + int fw = Math.max(0, Math.min(w, Math.round(w * frac))); + for (int s = 0; s < segments; s++) { + int sx = x + s * segW; + int sw = (s == segments - 1) ? (x + w - sx) : segW - 1; // 1px tick gap between cells + int lit = Math.max(0, Math.min(sw, fw - s * segW)); + if (lit > 0) { + g.fill(sx, y, sx + lit, y + h, fill); + g.fill(sx, y, sx + lit, y + 1, 0x55FFFFFF); + } + } + if (fw > 0 && fw < w) { + long time = System.currentTimeMillis() / 250L; + if ((time & 1L) == 0L) { + g.fill(x + fw - 1, y, x + fw, y + h, 0xAAFFFFFF); + } + } + } + /** * A liquid gauge: two-tone wave fill so tank contents read as FLUID, not paint — pass the content * colour (fuel amber, O₂ cyan, water blue, meltwater frost). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlock.java new file mode 100644 index 0000000..7c43a7f --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlock.java @@ -0,0 +1,75 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Fuel Refinery block: refines coal + blaze powder + grid energy into liquid rocket fuel. Right-click + * opens its GUI; emits a comparator signal scaled to its fuel level. + */ +public class FuelRefineryBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(FuelRefineryBlock::new); + + public FuelRefineryBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new FuelRefineryBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.FUEL_REFINERY.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof FuelRefineryBlockEntity refinery) { + serverPlayer.openMenu(refinery); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof FuelRefineryBlockEntity refinery ? refinery.comparatorSignal() : 0; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java new file mode 100644 index 0000000..964989a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java @@ -0,0 +1,267 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.fluid.FluidTank; +import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.menu.FuelRefineryMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Fuel Refinery: the logistics-grade rocket-fuel source. It takes grid power (energy, insert-only), + * a carbon feed (coal/charcoal) and a catalyst (blaze powder), and over a work cycle refines + * them into liquid {@code rocket_fuel} in an internal tank exposed via the Fluid capability — so pipes + * carry the fuel to a Fuel Tank or straight to a padded rocket. + * + *

Cross-loader port note: rebuilt on the shared {@link EnergyBuffer} + {@link FluidTank} and a vanilla + * {@link WorldlyContainer} for the two input slots (the root used the NeoForge transfer API). Tuning + * values are inlined (identity multiplier).

+ */ +public class FuelRefineryBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int CARBON_SLOT = 0; + public static final int CATALYST_SLOT = 1; + public static final int SIZE = 2; + public static final int DATA_COUNT = 6; + + /** Inlined Tuning base values. */ + public static final int ENERGY_BUFFER = 40_000; + public static final int ENERGY_MAX_INSERT = 1_000; + public static final int TANK_CAPACITY = 8_000; + public static final int FE_PER_TICK = 40; + public static final int MB_PER_BATCH = 2_000; + public static final int WORK_TICKS = 100; + + private static final int[] SLOTS = {CARBON_SLOT, CATALYST_SLOT}; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private final EnergyBuffer energy = new EnergyBuffer(ENERGY_BUFFER, ENERGY_MAX_INSERT, 0, this::setChanged); + private final FluidTank tank = new FluidTank(TANK_CAPACITY, this::setChanged); + private int progress; + + /** Synced to the menu: [0]=energy [1]=energyCap [2]=fuel [3]=fuelCap [4]=progress [5]=maxProgress. */ + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> energy.getRaw(); + case 1 -> ENERGY_BUFFER; + case 2 -> (int) tank.getAmount(); + case 3 -> (int) tank.getCapacity(); + case 4 -> progress; + case 5 -> WORK_TICKS; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + if (index == 4) { + progress = value; + } + } + + @Override + public int getCount() { + return DATA_COUNT; + } + }; + + public FuelRefineryBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.FUEL_REFINERY.get(), pos, state); + } + + private static Fluid rocketFuel() { + return (Fluid) ModFluids.ROCKET_FUEL.get(); + } + + private static boolean slotAccepts(int index, ItemStack stack) { + return switch (index) { + case CARBON_SLOT -> stack.is(Items.COAL) || stack.is(Items.CHARCOAL); + case CATALYST_SLOT -> stack.is(Items.BLAZE_POWDER); + default -> false; + }; + } + + /** Exposed via the mod's energy capability/lookup (insert-only — grid powered). */ + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + /** Exposed via the mod's fluid capability/lookup — pipes pull the refined fuel away. */ + public NerospaceFluidStorage getTank() { + return this.tank; + } + + public ContainerData getDataAccess() { + return this.dataAccess; + } + + public int comparatorSignal() { + long stored = this.tank.getAmount(); + return stored <= 0 ? 0 : 1 + (int) (stored / (double) this.tank.getCapacity() * 14.0D); + } + + public boolean canRun() { + return !this.items.get(CARBON_SLOT).isEmpty() + && !this.items.get(CATALYST_SLOT).isEmpty() + && this.energy.getAmount() >= FE_PER_TICK + && this.tank.getCapacity() - this.tank.getAmount() >= MB_PER_BATCH; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + if (!canRun()) { + if (this.progress != 0) { + this.progress = 0; + setChanged(); + } + return; + } + + this.energy.consume(FE_PER_TICK); + this.progress++; + if (this.progress >= WORK_TICKS) { + this.progress = 0; + this.items.get(CARBON_SLOT).shrink(1); + this.items.get(CATALYST_SLOT).shrink(1); + this.tank.fill(rocketFuel(), MB_PER_BATCH, false); + } + setChanged(); + } + + // --- Persistence -------------------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + output.putString("Fluid", BuiltInRegistries.FLUID.getKey(this.tank.getRawFluid()).toString()); + output.putInt("Amount", this.tank.getRawAmount()); + output.putInt("Progress", this.progress); + output.store("Carbon", ItemStack.OPTIONAL_CODEC, this.items.get(CARBON_SLOT)); + output.store("Catalyst", ItemStack.OPTIONAL_CODEC, this.items.get(CATALYST_SLOT)); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + Fluid fluid = BuiltInRegistries.FLUID.getValue(Identifier.parse(input.getStringOr("Fluid", "minecraft:empty"))); + this.tank.setRaw(fluid, input.getIntOr("Amount", 0)); + this.progress = input.getIntOr("Progress", 0); + this.items.set(CARBON_SLOT, input.read("Carbon", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + this.items.set(CATALYST_SLOT, input.read("Catalyst", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + + // --- MenuProvider ------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.fuel_refinery"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new FuelRefineryMenu(containerId, playerInventory, this, this.dataAccess); + } + + // --- WorldlyContainer: coal + blaze powder in only ---------------------- + + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return slotAccepts(slot, stack); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return false; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return slotAccepts(slot, stack); + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.get(CARBON_SLOT).isEmpty() && this.items.get(CATALYST_SLOT).isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + if (this.level == null || this.level.getBlockEntity(this.worldPosition) != this) { + return false; + } + return player.distanceToSqr(this.worldPosition.getX() + 0.5, + this.worldPosition.getY() + 0.5, this.worldPosition.getZ() + 0.5) <= 64.0; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlock.java new file mode 100644 index 0000000..6644814 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlock.java @@ -0,0 +1,125 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Fuel Tank: a fuel-storage machine that auto-fuels a rocket sitting on an adjacent launch pad. + * Right-click with a fuel bucket/canister to deposit, with an empty bucket to draw a bucket back out, + * or empty-handed to open its readout GUI. Emits a comparator signal scaled to its fill level. + */ +public class FuelTankBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(FuelTankBlock::new); + + public FuelTankBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new FuelTankBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.FUEL_TANK.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + if (!(level.getBlockEntity(pos) instanceof FuelTankBlockEntity tank)) { + return InteractionResult.TRY_WITH_EMPTY_HAND; + } + + if (stack.is(ModItems.ROCKET_FUEL_BUCKET.get())) { + if (!level.isClientSide() && tank.tryFillContainer()) { + if (!player.getAbilities().instabuild) { + player.setItemInHand(hand, new ItemStack(Items.BUCKET)); + } + playGlug(level, pos); + } + return InteractionResult.SUCCESS; + } + if (stack.is(ModItems.ROCKET_FUEL_CANISTER.get())) { + if (!level.isClientSide() && tank.tryFillContainer()) { + if (!player.getAbilities().instabuild) { + stack.shrink(1); + } + playGlug(level, pos); + } + return InteractionResult.SUCCESS; + } + if (stack.is(Items.BUCKET)) { + if (!level.isClientSide() && tank.tryDrainBucket()) { + if (!player.getAbilities().instabuild) { + stack.shrink(1); + player.getInventory().placeItemBackInInventory( + new ItemStack(ModItems.ROCKET_FUEL_BUCKET.get())); + } + playGlug(level, pos); + } + return InteractionResult.SUCCESS; + } + + return InteractionResult.TRY_WITH_EMPTY_HAND; + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof net.minecraft.server.level.ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof FuelTankBlockEntity tank) { + serverPlayer.openMenu(tank); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof FuelTankBlockEntity tank ? tank.comparatorSignal() : 0; + } + + private static void playGlug(Level level, BlockPos pos) { + level.playSound(null, pos, SoundEvents.BUCKET_EMPTY, SoundSource.BLOCKS, 0.7F, 1.0F); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlockEntity.java new file mode 100644 index 0000000..a989184 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelTankBlockEntity.java @@ -0,0 +1,328 @@ +package za.co.neroland.nerospace.machine; + +import java.util.Set; +import java.util.stream.IntStream; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.Mth; +import net.minecraft.core.NonNullList; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.fluid.FluidTank; +import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.menu.FuelTankMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.rocket.LaunchPadMultiblock; +import za.co.neroland.nerospace.rocket.RocketEntity; + +/** + * Block entity for the {@link FuelTankBlock}. It stores a large buffer of {@code rocket_fuel} and, each + * server tick, automatically feeds a rocket standing on an adjacent launch pad — the multiblock-pad + * machinery. A complete 3x3 pad pumps faster (4x), a Heavy Launch Complex faster still (12x). + * + *

Cross-loader port note: the root binds the tank and the canister intake to the NeoForge transfer + * API. The multiloader rebuilds the tank on the shared {@link FluidTank} and the single canister slot + * as a vanilla {@link WorldlyContainer} (exposed via {@code Capabilities.Item.BLOCK} / Fabric + * {@code ContainerStorage}); fuel values are inlined (identity-multiplier). The pump FX uses a vanilla + * sound (the root's {@code ModSounds.FUEL_TANK_PUMP} alias is not ported).

+ */ +public class FuelTankBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + /** One bucket / canister of fuel, in millibuckets. */ + public static final int CONTAINER_MB = 1_000; + /** Tank capacity, mB (base value; the root scales by the energy-rate multiplier). */ + public static final int CAPACITY = 32_000; + + /** Pump rates by pad footprint (base / full 3x3 / Heavy complex). */ + private static final int PUMP_RATE = 40; + private static final int PUMP_RATE_FULL_PAD = 160; + private static final int PUMP_RATE_HEAVY_PAD = 480; + + private static final int FX_PARTICLE_INTERVAL = 8; + private static final int FX_SOUND_INTERVAL = 24; + + public static final int CANISTER_SLOT = 0; + public static final int SIZE = 1; + private static final int[] SLOTS = IntStream.range(0, SIZE).toArray(); + + private int fxTick; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private final FluidTank tank = new FluidTank(CAPACITY, this::setChanged); + + /** Synced to the open menu: [0]=fuel, [1]=capacity. */ + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return index == 0 ? (int) tank.getAmount() : (int) tank.getCapacity(); + } + + @Override + public void set(int index, int value) { + // Read-only from the client. + } + + @Override + public int getCount() { + return 2; + } + }; + + public FuelTankBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.FUEL_TANK.get(), pos, state); + } + + public ContainerData getDataAccess() { + return this.dataAccess; + } + + private static Fluid rocketFuel() { + return (Fluid) ModFluids.ROCKET_FUEL.get(); + } + + // --- MenuProvider ------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.fuel_tank"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new FuelTankMenu(containerId, playerInventory, this.dataAccess); + } + + // --- Fuel access (used by the block's item interaction + the fluid capability) ----- + + /** The tank, exposed via the mod's fluid capability/lookup (pipe filling). */ + public NerospaceFluidStorage getTank() { + return this.tank; + } + + public int getFluidAmount() { + return (int) this.tank.getAmount(); + } + + public int getCapacity() { + return (int) this.tank.getCapacity(); + } + + /** Tries to add one container (bucket/canister) of fuel; {@code true} if the whole lot fit. */ + public boolean tryFillContainer() { + return this.tank.fill(rocketFuel(), CONTAINER_MB, false) == CONTAINER_MB; + } + + /** Tries to draw one bucket of fuel out of the tank (for refilling an empty bucket). */ + public boolean tryDrainBucket() { + return this.tank.drain(CONTAINER_MB, false) == CONTAINER_MB; + } + + /** Comparator output: 0 (empty) .. 15 (full), scaled by fill fraction. */ + public int comparatorSignal() { + long amount = this.tank.getAmount(); + if (amount <= 0) { + return 0; + } + return 1 + (int) (amount / (double) this.tank.getCapacity() * 14.0D); + } + + // --- Ticking ------------------------------------------------------------ + + public void tick(Level level, BlockPos pos, BlockState state) { + if (level.isClientSide()) { + return; + } + + drawFromCanister(); + + if (this.tank.getAmount() <= 0) { + return; + } + + BlockPos padPos = LaunchPadMultiblock.adjacentPad(level, pos); + if (padPos == null) { + return; + } + + Set pads = LaunchPadMultiblock.connectedPads(level, padPos); + RocketEntity rocket = LaunchPadMultiblock.rocketAbove(level, pads); + if (rocket == null) { + return; + } + + int toPump = (int) Math.min(pumpRate(level, pads), this.tank.getAmount()); + if (toPump <= 0) { + return; + } + + int drained = (int) this.tank.drain(toPump, false); + int overflow = rocket.addFuel(drained); + if (overflow > 0) { + this.tank.fill(rocketFuel(), overflow, false); + } + if (drained - overflow > 0 && level instanceof ServerLevel serverLevel) { + pumpingFx(serverLevel, pos, rocket); + } + } + + /** Consume one buffered canister into {@link #CONTAINER_MB} of fuel if the whole lot fits. */ + private void drawFromCanister() { + ItemStack canister = this.items.get(CANISTER_SLOT); + if (canister.isEmpty()) { + return; + } + if (this.tank.getCapacity() - this.tank.getAmount() < CONTAINER_MB) { + return; + } + this.tank.fill(rocketFuel(), CONTAINER_MB, false); + canister.shrink(1); + this.items.set(CANISTER_SLOT, canister.isEmpty() ? ItemStack.EMPTY : canister); + } + + /** Pump rate by pad footprint: base on a partial cluster, 4x on the 3x3, 12x on a Heavy complex. */ + public static int pumpRate(Level level, Set pads) { + if (LaunchPadMultiblock.isHeavyComplex(level, pads)) { + return PUMP_RATE_HEAVY_PAD; + } + return LaunchPadMultiblock.isFullThreeByThree(pads) ? PUMP_RATE_FULL_PAD : PUMP_RATE; + } + + private void pumpingFx(ServerLevel level, BlockPos pos, RocketEntity rocket) { + this.fxTick++; + double sx = pos.getX() + 0.5D; + double sy = pos.getY() + 1.0D; + double sz = pos.getZ() + 0.5D; + double ex = rocket.getX(); + double ey = rocket.getY() + 0.5D; + double ez = rocket.getZ(); + + if (this.fxTick % FX_PARTICLE_INTERVAL == 0) { + for (double t = 0.15D; t < 1.0D; t += 0.3D) { + level.sendParticles(ParticleTypes.CLOUD, + Mth.lerp(t, sx, ex), Mth.lerp(t, sy, ey), Mth.lerp(t, sz, ez), + 1, 0.05D, 0.05D, 0.05D, 0.0D); + } + level.sendParticles(ParticleTypes.SMALL_FLAME, ex, ey, ez, 1, 0.15D, 0.1D, 0.15D, 0.0D); + } + if (this.fxTick % FX_SOUND_INTERVAL == 0) { + level.playSound(null, (sx + ex) / 2.0D, (sy + ey) / 2.0D, (sz + ez) / 2.0D, + SoundEvents.BREWING_STAND_BREW, SoundSource.BLOCKS, + 0.35F, 0.85F + level.getRandom().nextFloat() * 0.25F); + } + } + + // --- Persistence (Value I/O) ------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putString("Fluid", BuiltInRegistries.FLUID.getKey(this.tank.getRawFluid()).toString()); + output.putInt("Amount", this.tank.getRawAmount()); + output.store("Canister", ItemStack.OPTIONAL_CODEC, this.items.get(CANISTER_SLOT)); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + Fluid fluid = BuiltInRegistries.FLUID.getValue(Identifier.parse(input.getStringOr("Fluid", "minecraft:empty"))); + this.tank.setRaw(fluid, input.getIntOr("Amount", 0)); + this.items.set(CANISTER_SLOT, input.read("Canister", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + + // --- WorldlyContainer: canister in only --------------------------------- + + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return isCanister(stack); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return false; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return isCanister(stack); + } + + private static boolean isCanister(ItemStack stack) { + return stack.is(ModItems.ROCKET_FUEL_CANISTER.get()); + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.get(CANISTER_SLOT).isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelRefineryMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelRefineryMenu.java new file mode 100644 index 0000000..9844053 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelRefineryMenu.java @@ -0,0 +1,118 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import za.co.neroland.nerospace.machine.FuelRefineryBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** Menu for the Fuel Refinery: a carbon slot + a catalyst slot, plus energy/fuel/progress data. */ +public class FuelRefineryMenu extends AbstractContainerMenu { + + private static final int CARBON_SLOT = 0; + private static final int CATALYST_SLOT = 1; + private static final int PLAYER_INV_START = 2; + private static final int PLAYER_INV_END = PLAYER_INV_START + 36; + + private final Container container; + private final ContainerData data; + + public FuelRefineryMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, new SimpleContainer(FuelRefineryBlockEntity.SIZE), + new SimpleContainerData(FuelRefineryBlockEntity.DATA_COUNT)); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public FuelRefineryMenu(int containerId, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.FUEL_REFINERY.get(), containerId); + checkContainerSize(container, FuelRefineryBlockEntity.SIZE); + checkContainerDataCount(data, FuelRefineryBlockEntity.DATA_COUNT); + this.container = container; + this.data = data; + this.addSlot(new FilterSlot(container, FuelRefineryBlockEntity.CARBON_SLOT, 56, 35)); + this.addSlot(new FilterSlot(container, FuelRefineryBlockEntity.CATALYST_SLOT, 104, 35)); + this.addStandardInventorySlots(playerInventory, 8, 84); + this.addDataSlots(data); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack moved = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack raw = slot.getItem(); + moved = raw.copy(); + if (index == CARBON_SLOT || index == CATALYST_SLOT) { + if (!this.moveItemStackTo(raw, PLAYER_INV_START, PLAYER_INV_END, true)) { + return ItemStack.EMPTY; + } + } else if (raw.is(Items.BLAZE_POWDER)) { + if (!this.moveItemStackTo(raw, CATALYST_SLOT, CATALYST_SLOT + 1, false)) { + return ItemStack.EMPTY; + } + } else if (raw.is(Items.COAL) || raw.is(Items.CHARCOAL)) { + if (!this.moveItemStackTo(raw, CARBON_SLOT, CARBON_SLOT + 1, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + if (raw.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + if (raw.getCount() == moved.getCount()) { + return ItemStack.EMPTY; + } + slot.onTake(player, raw); + } + return moved; + } + + public int getEnergy() { + return this.data.get(0); + } + + public int getMaxEnergy() { + return this.data.get(1); + } + + public int getFuel() { + return this.data.get(2); + } + + public int getFuelCapacity() { + return this.data.get(3); + } + + public int getScaledProgress(int pixels) { + int max = this.data.get(5); + int cur = this.data.get(4); + return (max != 0 && cur != 0) ? cur * pixels / max : 0; + } + + private static class FilterSlot extends Slot { + FilterSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return this.container.canPlaceItem(this.getContainerSlot(), stack); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelTankMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelTankMenu.java new file mode 100644 index 0000000..929c5aa --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/FuelTankMenu.java @@ -0,0 +1,59 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * Menu for the Fuel Tank: no machine slots (it holds a fluid, not items), just the player inventory + * and two synced data values (fuel, capacity) for the screen's readout. + */ +public class FuelTankMenu extends AbstractContainerMenu { + + private final ContainerData data; + + /** Client constructor (referenced by the {@code MenuType}). */ + public FuelTankMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, new SimpleContainerData(2)); + } + + /** Server constructor. */ + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public FuelTankMenu(int containerId, Inventory playerInventory, ContainerData data) { + super(ModMenuTypes.FUEL_TANK.get(), containerId); + checkContainerDataCount(data, 2); + this.data = data; + this.addStandardInventorySlots(playerInventory, 8, 84); + this.addDataSlots(data); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; // no machine slots to shuttle to/from + } + + // --- Screen helpers ----------------------------------------------------- + + public int getFuel() { + return this.data.get(0); + } + + public int getCapacity() { + return this.data.get(1); + } + + public int getFuelPercent() { + int cap = getCapacity(); + return cap == 0 ? 0 : Math.min(100, getFuel() * 100 / cap); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index cbd7d83..2e70a23 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -6,6 +6,8 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; +import za.co.neroland.nerospace.machine.FuelRefineryBlockEntity; +import za.co.neroland.nerospace.machine.FuelTankBlockEntity; import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; import za.co.neroland.nerospace.machine.OxygenGeneratorBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; @@ -76,6 +78,14 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("solar_panel", key -> new BlockEntityType<>(SolarPanelBlockEntity::new, java.util.Set.of(ModBlocks.SOLAR_PANEL.get()))); + public static final RegistryEntry> FUEL_TANK = + BLOCK_ENTITIES.register("fuel_tank", + key -> new BlockEntityType<>(FuelTankBlockEntity::new, java.util.Set.of(ModBlocks.FUEL_TANK.get()))); + + public static final RegistryEntry> FUEL_REFINERY = + BLOCK_ENTITIES.register("fuel_refinery", + key -> new BlockEntityType<>(FuelRefineryBlockEntity::new, java.util.Set.of(ModBlocks.FUEL_REFINERY.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index ecdef41..380a678 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -14,6 +14,8 @@ import za.co.neroland.nerospace.fluid.ModFluids; import za.co.neroland.nerospace.fluid.RocketFuelLiquidBlock; import za.co.neroland.nerospace.machine.CombustionGeneratorBlock; +import za.co.neroland.nerospace.machine.FuelRefineryBlock; +import za.co.neroland.nerospace.machine.FuelTankBlock; import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; import za.co.neroland.nerospace.machine.OxygenGeneratorBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; @@ -168,6 +170,17 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + // --- Fuel machines ------------------------------------------------------ + public static final RegistryEntry FUEL_TANK = BLOCKS.register("fuel_tank", + key -> new FuelTankBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL))); + + public static final RegistryEntry FUEL_REFINERY = BLOCKS.register("fuel_refinery", + key -> new FuelRefineryBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL))); + // --- Rockets ------------------------------------------------------------ public static final RegistryEntry ROCKET_LAUNCH_PAD = BLOCKS.register("rocket_launch_pad", key -> new RocketLaunchPadBlock(BlockBehaviour.Properties.of() diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 5f52853..373eb56 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -76,6 +76,8 @@ public final class ModItems { public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); public static final RegistryEntry ROCKET_LAUNCH_PAD_ITEM = blockItem("rocket_launch_pad", ModBlocks.ROCKET_LAUNCH_PAD); public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); + public static final RegistryEntry FUEL_TANK_ITEM = blockItem("fuel_tank", ModBlocks.FUEL_TANK); + public static final RegistryEntry FUEL_REFINERY_ITEM = blockItem("fuel_refinery", ModBlocks.FUEL_REFINERY); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -207,7 +209,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 751f8f4..5b648d3 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -7,6 +7,8 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; +import za.co.neroland.nerospace.menu.FuelRefineryMenu; +import za.co.neroland.nerospace.menu.FuelTankMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; import za.co.neroland.nerospace.rocket.RocketMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -33,6 +35,14 @@ public final class ModMenuTypes { MENUS.register("rocket", key -> new MenuType<>(RocketMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> FUEL_TANK = + MENUS.register("fuel_tank", + key -> new MenuType<>(FuelTankMenu::new, FeatureFlags.VANILLA_SET)); + + public static final RegistryEntry> FUEL_REFINERY = + MENUS.register("fuel_refinery", + key -> new MenuType<>(FuelRefineryMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_refinery.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_refinery.json new file mode 100644 index 0000000..4764a25 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_refinery.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/fuel_refinery" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_tank.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_tank.json new file mode 100644 index 0000000..553509c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/fuel_tank.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/fuel_tank" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/fuel_refinery.json b/multiloader/common/src/main/resources/assets/nerospace/items/fuel_refinery.json new file mode 100644 index 0000000..cdc3db6 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/fuel_refinery.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/fuel_refinery" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/fuel_tank.json b/multiloader/common/src/main/resources/assets/nerospace/items/fuel_tank.json new file mode 100644 index 0000000..ec38b6d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/fuel_tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/fuel_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 8192db1..8e4e0c8 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -12,6 +12,8 @@ "block.nerospace.creative_battery": "Creative Battery", "block.nerospace.deepslate_nerosium_ore": "Deepslate Nerosium Ore", "block.nerospace.fluid_tank": "Fluid Tank", + "block.nerospace.fuel_refinery": "Fuel Refinery", + "block.nerospace.fuel_tank": "Fuel Tank", "block.nerospace.gas_tank": "Gas Tank", "block.nerospace.glacite_block": "Block of Glacite", "block.nerospace.glacite_ore": "Glacite Ore", @@ -44,6 +46,8 @@ "block.nerospace.universal_pipe": "Universal Pipe", "block.nerospace.xertz_quartz_ore": "Xertz Quartz Ore", "container.nerospace.combustion_generator": "Combustion Generator", + "container.nerospace.fuel_refinery": "Fuel Refinery", + "container.nerospace.fuel_tank": "Fuel Tank", "container.nerospace.item_store": "Item Store", "container.nerospace.nerosium_grinder": "Nerosium Grinder", "container.nerospace.passive_generator": "Passive Generator", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_refinery.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_refinery.json new file mode 100644 index 0000000..79c3bd4 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_refinery.json @@ -0,0 +1,72 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 13, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 5, + 13, + 5 + ], + "to": [ + 11, + 16, + 11 + ] + } + ], + "textures": { + "particle": "nerospace:block/fuel_refinery", + "side": "nerospace:block/fuel_refinery" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_tank.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_tank.json new file mode 100644 index 0000000..95d7045 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/fuel_tank.json @@ -0,0 +1,425 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#core" + }, + "east": { + "texture": "#core" + }, + "north": { + "texture": "#core" + }, + "south": { + "texture": "#core" + }, + "up": { + "texture": "#core" + }, + "west": { + "texture": "#core" + } + }, + "from": [ + 2, + 2, + 2 + ], + "to": [ + 14, + 14, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 2, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 14 + ], + "to": [ + 2, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 0 + ], + "to": [ + 14, + 2, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 14 + ], + "to": [ + 14, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 2 + ], + "to": [ + 2, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 2 + ], + "to": [ + 16, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 0 + ], + "to": [ + 14, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 14 + ], + "to": [ + 14, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "core": "nerospace:block/fuel_tank_core", + "particle": "nerospace:block/fuel_tank", + "side": "nerospace:block/fuel_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_refinery.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_refinery.png new file mode 100644 index 0000000000000000000000000000000000000000..92d5e6a8db3a887eac7cdc75c2f6f479e1f0ca39 GIT binary patch literal 509 zcmV=D25L)BGl-P7qmJVIosbmf&Q}FDenBb}SPbe8o@sPn} z=+aE3Eg^#mo{Z5Dj0hr#BE-W(mvoTzWW^Z*{orx$j^o~U-`(3d9Zl}}4^#lo&jzJn zn#B6PPzko<0uY5k={~=iVdT!c4+j7Q(=hdt6O~HwoojG_OqwO_Jht?A4F;D%S%dtQ z-tcOT?ql$F>i~rYIh|*gjqBC4;`zu{$NCVJRj1_}4=7(}#-W6Zu!=OTLPW?W(Hq`A zsfBv8T4rz;&2haNknMytExmPV5+hBEG>LUyFdZX=EDGeoZTmUjU;ltT4}MQ!@9)xj z+BUj@o*GcBa=X(9VE^(B`~7Vi7cY7LWe1KrG`gF-8oi*=-NbfW4v#-+1{(z7{dG6KEZ&`uP@y92L_Za>GQt!v*b-0b-00000NkvXXu0mjf8e8r5 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank.png new file mode 100644 index 0000000000000000000000000000000000000000..8966baa24059f8dd7c59bf3360ea6175061df003 GIT binary patch literal 467 zcmV;^0WAKBP)DVBbQSUhSEWi7Tpq4Xo*;$GBe=TCge&|2SKSk&a^d1g zIB|#&j0<6eS(e3BASs+3M@mxN4}z6uXXg87bnx@npJ)CXN-pRq zmUFo(Fx?CQXDjl%IW2&!*Jn7o0-(qjY`sNCG3#=PvlY%(6!}7u&&~%@xE-p^rW3g^ zzx~Z-^#H)?mBv3k3CZ>NQqC=ab-5J8xA;v35`e^{dxD@q@R_#WlJ)x1ObTSZzElq0 z{?13WclAN0BcCk<(uN zd8G>kTW|LSVTiz{0 z>4bH;w?b literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_refinery.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_refinery.png new file mode 100644 index 0000000000000000000000000000000000000000..69be3c3356e887a1e6bd4e456790e09f385a3397 GIT binary patch literal 5771 zcmdT|_d6S2)K9EhvDGTAYO57OjZh-AYOmI&<`*@;Mp3axts-<8C3cCTq(v!WrS=Fl z+uAEsBO>;E^L^jH;=RAz`#k49&pqdJ&*yXQJue>_X@i&rnE?O*NLS~f2><}3E`b0> z2I^${to$heaM?un;e9g%ikOE_bJ6!tTz-Ba!*9p*y|cZuHeP(V~h&q;y(h zb5c@cHHbDWsQ9g*^jShSrh_+C{Br8ueQgE6i7oL%S$_+aX~PV?!8LOnw|gw zi-g<@iuGB(Q!we7tdCx|<~-rRBYe+JuUm!GhtFwUUabCpFKVFC;}@lQEhXXgk<5?D zmGm-w(%9*AvjZiVB#>^*_w>MyGBD7EBe$&Y++V)JNhWNn(WCIUd8O3Ps*uu4t=G~M zn;%=*#bm^)%pm=wAvOJ2&XWu>0_I(P)O@}8--qWR-!FK+vSe_Qw8~vjwvRmKRCh(N zZQNY>JpyG3wSDY;Vupz<y_sir*OVEA-kNuRA+$r|r$Ok08T;xGi zX3ECjCQGIo8bh#l-y=$Xbce!3^j1wo7m#KJ0=$JEubkV3m53JR*u#X1E&e9i0Oe*{ zd||%7bb^O)KeP(TBL`R5gKwT4p_q}_;0<4iX{~8w*@w|)`V<|iHO`#ZhF0&@%LGAj<9_u&JlR(~mnSNx zO5Y&8#ETX|sx_+-X{}N6E*>|i2V4;XmV4+V0C2dIvIcSRSD39;r8<9zq@Y=u>DVfcB|>Y{ zX!P7Ds4E^64s&J1plv=fPo`ORabzC&hDMm`P9W;suKhp+Ni_Eifs+$HB3roKtL-;y zJ=w{66{J0a>afq-^&K;zSHD&EQxm$fZ{a`Y*qM1h-cT{0A6p~Oy0hM69e2tR)FXz8 z2P#BLOw0Zq&Zi~F$w$jV8@j)2AdO6-d|4|&T|#yHT%m0hBc7()nB9hOU-+^HgXL;{ zrwRQHTCN?7fd79Mt=b47&AuZw;{^=JP~%~zHWsB3{f zXApY&cduK3Pgw8ypJq?4uzh~e6AHgjJ1ha<_u`FSAZgY%9*kcrC@+E=^vZM-x)-u} ze}W4wn1x?d=%EgpWs;`%oAPcx5yCu~jo^Rf{=#(N>lbvFyNw(xNRcv}D3!y2&n34o ziI@*XJZ)dDz{PICqvp_>6Pa>be83MI@E7CYEA)r% z1+Dv^0IO$ZO`8NC+j;8&bI{xkL=Ps*|6i$9Xofl6d&VmY;j3qN-i>c7%BPJ5@xJo; zJJwAE*-e~Qqvv;vk&jJ??8;J>e-&=cXf@eIG@L$OtjWn`F|8Lv*q3kn3o)bc<(zqo zn&7PTv6RxuadMLK#i!*dBi=#3me-ZXjqiWRI?rn`z|*I6w4J8|Kw`!F-l2v>fg^|I zlANSOqH3wpeP!i`KXWtuPEwfTMw13}st-Jm+(VE`(@G9w)inR$Z#Um*sFH)$^j|54!lo zt)72X%;7tHIY@mhi-#7H&?a6oF00Uelsyv`oAhY{`j1!Xu;1Hi^rVcL;a)yoNK^65 zO5&Y6$Fjg(&y7QqFtPL!^y5Uf^PzcR~L29i5@}Q{_g}w6yoIJ_1FZR0LzKye3<{{Gq;yh zhwqA_%inQ+-taIt-A-I)J-RatCRG#J?wGq?(QzfTu%TT9)X-q*KU?0WZw>Rj-d zEgX8v9^ylq>q%HV2*Pjp*6w&e&6Zo*oo?DGIyx(fI?X!7ks`S{`2lXG&uP=HI$+a& z=WaHBKcq<+0Rb1ow+?8g>sZ>81HW<|BKjrUt?HaFJ4Ewt$7}n+b3N4S09LO*DuxrEb$$B6xL>1YJ~tpF$U zXQupn0g!_*U>cU96>aXw;0PD;Dj*eHvI6cYMuQwhh?0PIPu4b&{P{;7;45cISpka= z0oBZe$C)(2_RoM8*JcZyN^fEiYAg<0&7Z09bWrF^~3#>GHDCe3t0f^cF4%&u%subUr=3Q1uke7ibINkgC&fBuQr_(>+du8D+r@abQ)c;=1%3x zSxa20lqo0keqG!+-ox97A=&YhLRAm7M3vZ^s1Y8^M>Bq<#pMKwtKg1id>Oa~BqT0Z zG44I1LY=Cqo~Uv|N>aDte2`|a#DI3q2z*1TN~_w;Re#$ySuJ`?uARBuE}6|!#Pzb{ zX|EpGiZ#e;K(ZYMWxEfrFU?dtB^wv0FuRd|EVFx!ecd&772Mw0`J);3e_;Fqte+9S zSYP7k-~dc&uvd35$fP6nWn}p*Zn%|am*xz}(EQn`bdjmjVgtf67B?gcH*qPzf@sPe zFfiydeY+mDerwRSb10=FHC{2^kgQ@79t($RzgXmAwYY(d`fHIl(9NJuVh5W*EAJYr zBL8r%*Uh|PO)&2e8?oJ@k3ObV?~3i})l`?sdIkZoNI9B+A)BfZOymKX)}8$y^!=XF zu05UvmV5a{XY_XCSto3v;0n`A~E4T6Z03iT(=bd=r zQ2L=#$>XWg_WAQ>>1Fn|6T^FNr7qBvS3b@Ozd7p`c5{@2cRPhy;4Xs%{fq+%+}xWr z)|xQ9)rI5v+HQ!N(q4rB1c4IV)-a%f3?|TP;}8bc?x+E?8m5yi)kwoJ^Mq7%uvg1& z>tH1$-_Ch>(*`QSHdwT>($%$EMqZ!R2k&U~C}h#75f}PHW)_2VB!;vadr!U;AD`?B z|9VurCXkT|jg{c%Rzf2iAhE~wv_FHT^R z!^99L4OS~PumyycOz!xb+m7vVxDq_X)jz#Q=USIgirxJ}`9}QH`A;|>;%>?43B!liCO?F(2-L)3M7;Gh2!?p`8!9dvWlJw z-f%}#ZeMzV85Xp7sB#lZH{`m^@m-PjY`aG-Z|8caC@Ni3mF_N#N^?=2hj!18URh@K zY_AbjsFvE!J3fmt03%n%XjSdP%8&$66kZzjcW>5#y66^rL?^)NwFp_TW>QLjF8J#1 z<*K+Qgj<3qL?{Zt$nKqIM0-A6_b(hZh|(&OnqB zFV8|abQ3WBFFp8;&7Dd!!m}tyd!Mk9g@qNU?g!dgOwZbfsq#&&slj7>Nfn)BqKqtS zbiB^cJ7Rml9XL>I%Z-MgpaURZetlRTqtX@yjhl` zi)6c-CixPFe-s>$6644_Bdin@AVMTBaJtiC*qRK5mv?QU(C8$`ft!+DO8jP5Cfl#{ z%NOQj^J@YBmjZHEh(|T{|N9WRvlzJ*P9Bnu+!<c#rcB*6lMtTOZEBCp##$hWQY zl>VqG`R2EmrQSlM-xy_kS=qzSMUd&}HpCgzU?tM(nq=nzrNX zk>>+yk&CO0>1ftntkY(3p{@7=QnL8?->bYPm%Ub&KsA_SgM__D@uT$%g81qmPhmY# zFO8vWHC4HTuVt`z1NG4ZCq`_N5hLDt%F_>{ab+wI4CilQuRGSaz}hAR z(Du^ex{5-9Ek1~EM5f1lg09>gM)%9cegT^9P1%aQViRbIK08l}BtGBpH_}(Br`d@8 zKyEP?RPB@mTQq3df+xrbcluap^(m#hoMZ}mFqRc{eA?CJ%K0>O$7J|xs*GXzbC4;$ zZmYIpAL7LxY!qoPre^TSD;*X(Z+a-!`bEmpIAao#9^BiJzZ}_DU}AqAC7XIW8_zdb zq{@E-Y-}-H%|&qIWw0ZasU}pE9-;PWIDb1O7MK@0t)1P`2|P9l`#p?UAx+B2M{4YO zXBK+GcGo@mY{YzSdGBCL)pPReg!)}jb+5e+-y7TC`dLV6W{!JiF9OFpABz z1XDJKjk0z?zEB<<*>2fo(C?$pW*==nx?w<-zmmW7w*%%FfmU=5I^7(OB3)n%gpJw@ zs9caUV+F2_D$`Gp0C-U51-e9;KBIm^$J&a1wz+U1X)!#Rb0jztSQWi>wF9b<^-Mn7 zcMxb5vy>h?7bTG^8lBI{h@sgMqX7kp5INN4sFI{4eCTl-3>=`B6}Vh<=usGuqxX}g zaL%*W#ftvGNcV zoL5Q;gyLCg9~soU11Gg}9#puHUp8^pLq*Pn8z5?># z1ST713-y`(3lA2XoQQXkMqy?BYVc95=iK%&;*V0Jm+{~?N_0pW@%MQFePxRT(iWdg zP4JV~6P9_c;E36b#$@)k(d|KONSQ8f=w!U(sfCUm|LQ>~t$*nZ!g`7;mY89uHpI#|jI zRL=UviA`|}`mwQvssBf$XkU$x+*+V zD}k2^y_EgnuA+|~+69@zJ~v$rc|CcI3gzD7$D(wPhBtlPPVcB<6%0xb8+;{F%PgsR z=}s#5m}R|bME(Q2;?_)Z>qHKEt4QX2e<}>yuTZty(>b*qQkAH(@w9PdXw+xs{u7{Q$$rAxIn%6R zh$3|AZcW9MpjuEC@z;ZdqR4Hz{NprM<2JXsZ~G|UkFUm{p(*A!N3r9!bcTThFwBFvb|M{+~Y4ouA Ifn)6d00)Hm{r~^~ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_tank.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_tank.png new file mode 100644 index 0000000000000000000000000000000000000000..c5df5070aae8e79c9eb3959922dc83569de1cad4 GIT binary patch literal 5682 zcmdT|_g524uuVcQp(#a5=!k%TNCYK@-a94%kt*=f1f&Rrj!IJz8&#_Ggg}%g)kqPf z7zhLu>4jEy!F~BsH~68EZl)&d$#DQ)^Pu`Ry>%<;JnYN3RWp(^lrQoJ48k zJF1o~Q{U&{_{;9X(UJbNqD^%mAiwYt~X#HXQtE=7UCkxBaL80B{{#wNBc^a5x zr{y}8@P*MnYb0Ju*201lymg2m0&v!f?ao#&!Kcs&8r@z1uxlWpx^C)PC_4ArWymI0$?pSwz;J}}LcVu{d zVt+Sk9f~YO^8S=BW)tCck{fc^Upi@h?fuKKy~U`_|6Mto$l&`7Y{uI+PoFbijrNZ{ zRksvsV!g;aZ~szG*|Byci0t{i`Gd~Q>hW8z`T}Fm_Sqy>F~YO1|495eqa#)ku!7)S z1jsMz<(`kV_O3(bo>NM3$C77m&Fw^#8NXeisNbJ_=W#{E{*F^?Mak<1+9`QE1gBfa zWr@vf^b+25WUwkhrWf46mtfXsve^{;>f-bH(Q6(7|GmmI_VTa(zO)|WU$TNuJ@SP| zFiD^vFWk%m96jF6FT*Yh`8;s=oyL2^+qcO!1RC4>E}LUows^~jlaJ!tZPMUZ^r<1k zkF>uA4%skIwi9Q?ufFRE>R3M{faa6kTKqpN@jU8SEKO`d@| zV|WpZcE1j9M|0O%Q(y&h!W+N=R|*qCqD62e)c+9Y)hll1AiW2fTIau=<0G;*yK)S>NOmVsq|K1BnOk^Pd*oH#ygJ_4(O*cjo zo_L92`h4vJouPE|u))m3mkw+KQM!NGk*g{<9X=k3rYZvJl1QvS?K9TwUG0VBk(NJm zZO_9|RwJ{odfM{S+jv7y{&ZakF49Qd1KY@BAmhj0i(p1#;~qkhPE`IbKS zpPwjG-zjjm=smfB6UU3h547QL6mQ>{_5073qbJoCaz4QCwO&^fzXtp}%n*lhw?`j` z``#$=AWi2P<5F+OM4_C5pll9pp}>&07p#PeHCjm9_CK7;yP@h|_a^HEv6WNIEw8@U{Ew2yEvP$zVrs|rHb%xh0?-gS!D*^r= z&*W=+*i%dEU}<*KZv+ha21CD%exTrOMqWzxPopLtn}b zoKFbraTo`&))r9)BZwjXuD?0U5;O*+G}D!_!kNZd*>pBgLH=AHnWaEc@rUIVZ1F8Ra$(reh3phIXCOo*xnbK75C~raqvJ$robKYOp zB<;WuKE9KZH!GxwCbq>m9V)a__VEjalDTS=?M;K4wvgACZ2)A(h!g)L5PQnXYeTN31|vAe!Xf6xJnj*iS%x{x7rlt3dlhQQeU}z~ z)Tk4Ma2gYnO(p4f^TMCpsyI*)UzGFCy})ZBR69`}IISG&Z=a~ae*!;=T@Pf_Mu8)x z4a-<4B`i&a%tDUBHB{IOl`!p!b$uwGh~uwMXS=l|P;stg-u{cAA7~|#H!k6#!qVLU>CpuM>Osp8VvCAp7O=R9Ov)$!^M^*M<8Rvcsd>+fQ ztlm0QMwRldtUE!fyZ3ANKCvU6`y6IW7k6HG3Jao2n@x$UF>l>5#`aWx(zJblX%Gpy z-h3q4^~YETKcsq%?(Z9)mnQ$tPLB)9do4RWBfGDhvp z<`F%8SKRHuc^_G}1?V3CRCSC)ZruIw5Ozf>>j#L=j>Wl`Ay~iw_zI7g*f~wpwdY{R z%M}%oj))$1992Lm07{!*did_3Bp4IXD;t-t#g=6$$WZ9wid3~?5hTD!vVhtu7mtd9 z?Mr`w$}G&N5+?1hn$Yw*4P^qxD@4qUjAj@AdK?>tp?mLs#TnA<;bY3~_2@9d!7q9G-^P%WRaW3;yQ2^@C))d1L z%Iv>n=BFVEyw-#7iU{a;M<6Q#UeI1?lWw4RQt$;RkvGMH_39%?F~DUIs=&Gp`1>|5 zZs!;;drcS0SM^mmNKXt_g_pHfsw@b;ltI^+C7{uW-Jzk>OBEdQi{q911783wFT#Q<`7y`(US|ZErJNhaPEk*JSS=k^3 z6?S;Rk5;K|@{~6+Ao07d$WeuE`mCN=`Uk0?#NER2SYeyYG)@4DL6p4kDmM=Ik%w!H zer9yxRVb4`Zq7YkI^_y=bg-#pDn6x=QC`;we;@@G3w8GR%234!J`0qkdkm;z_67cZ z`xl5myH#Z}YFanaRFV}3;rSO98?oztiOwpdOfGK}W@DTf0`}}rn~D9M<}1ZFRSI=7 z;uF73{2RVukYelVq>S17;U{dtdo*D&9%E`m;;t5B)%LwN8JWmn|2>X&mdn)r=p z#<6f2I%kIaRs)@x-dCD&EaA;Y!d1ajD)RJ=f0an4i*-tzj}E`p(mYcW8;NJ;sXzL0 z0rTQcM=h-*`Ij#k1e(Lx;Bn{@YXGj!{&gK;H|s|J*u-1}i!vr&xSmvMb}^7yw^xc> z!^IU5VA&&YPfhS&NPBb6)pIQ+oB1;?7Uluf`_o&B=1W(mJfaU$J z9+_D`bht+eIN77_zTcZta}F!|YD9Km6pdTw+Ic^78q=3*loSGaws+V?mu32l3BRZX z3O5;z?P6>QzSQxU>7q*2(;G2nacC(=xnsca(M(S$eYir1I`3eJ;&+zbHF><80#l4| zVg&&Q{ik5ZBOJGF4AlNW@%+S4_0*@>3l#hv9(VuQWx}1#hTXIt?{a^cgG=1WCqP$eiJTBZG8snFD;$$`u;T>t zTlA=As)!l+4m2h{+StFy9FLZAGYO;X`0*HNB$WN&K>;JDWDSGM8wLfV3B{_P`m7KO z72ti0Gy}VQQ65juvI7Y*JBF`uF~1)sCV?U@;;+BD`iXGyoH(xjafR##xpXLX>U0Io zW^rm!RMR@|i}~&tIoKs+eN; zL-n-jf#Bgq)heAAfVyw(>9yxCYzTDo{c2(*Dw=eWQ!LbJxWnm+3i~pQ7$SkvPh%m- z5>y$%h4mSGuJ``WUk9f$ia&&XXX3Kg`som$^ds{*yY4OG^}IjK?l1dP6Q|-o*1LFY zSPqs7Gd@U#0`Yvec(kT+dkRf=PsShC5hT2-`t~`3jqb#|fw*}1P!jV9(!Mlk96R;Zk*2nb;s8{-1uIc~AYO4xoC<^XBw#JjqEHF(v>@Ae9Ra{k9YZqpSp zxCETy@q@PH6t zecf7pCa+zMDCn{gXk43^mPTIBbDDF^*69;ste0qRbjZ)R;lB=%4tvVnJ9mh?zPkyP zCn$564O3dr6*JfVMYtpM-CRGFMrh2!-2Z{SRC2Zwk=Z zVR}pV_@A?pQaF}=(@}l^wSY9&AzFX^h~%DE^J7<3_%Qr!Ytd;~ns_@SmziniSdTwD z{P1{&jZsG;t|A-v!eVlVzPEHP|m(JX_KU zTRYBnnDJgELCue4ky=i%C3?&QlGy-iDT5tDMx`di1K|>vJ~XFy&ewky2`W5V+MOo*li(~}R+h36 zlS{q{NbU0j=JZ?WQ-Hh`+4w)u4{>vPolhOl_CvgcI zJY0Dt%tTUS;a3#GeUr!``#=|7#TiH8jM)BnF6#NJtV5jhoN)>2(ddi}?oQYnS=?hd z00u?2GG*!V10Yc#7ioVs8xUu)FoV}B9u{7@gfH2KjZ}i3bqGfwW;rzv!o?nE}(S3H=~tgiVmK&@Rqfkj7iq+->&T59p$_co6) zvc0DRhew4Rkz(Zg?XB(dC!=>$q1VIAeE5uz%&|0j5tXby}#wFdNs}2gyRn zJM&5g%Hu0~j-Cf$1m7h&(GttOz1=DWzp#myCVd_`D|Kui zx>Us&r+?0%f(#qXU$mdSX%T%(UhE81_{$Z(!H8o%RayASwW=nViLeI`ErUvFFUNF! z3P&(owzpH2V8hQQI!pP}e|CBvJ;Wr5)_@!zT#eZ5ZMJ6231xNjpX=Ps#ceFL#T!g3 zQ1QOk?tOJbYS})P$T#dCk33c9>%m5KZob$AlrWJtNVwEmdZbrLaxGu0_>bqT++bb# zO4}Q0+vhD7?F%$BpLSInvY5>@a^YiQ`iBem+~-tX`yw!qYvBc=d3KBAA#;~!ieDgp z4}Yn?nb(`oECXwGNL*?r_0~mQtX$cMYjDH5 z6R=+bV1^a#m=~-N2F6AgC{KsAQKCm$i6?73pcBQ#sPV{2N@KaY=-6Cc{TTe{qM_V* gGxh)5AdGBlP6je72!2bH`rq7*^)2+ObzC0*4+xc*fB*mh literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_refinery.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_refinery.json new file mode 100644 index 0000000..904d137 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_refinery.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:fuel_refinery" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/fuel_refinery" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_tank.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_tank.json new file mode 100644 index 0000000..644d4bd --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fuel_tank.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:fuel_tank" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/fuel_tank" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/fuel_refinery.json b/multiloader/common/src/main/resources/data/nerospace/recipe/fuel_refinery.json new file mode 100644 index 0000000..cf4a6f9 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/fuel_refinery.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "F": "minecraft:furnace", + "G": "#c:glass_blocks/colorless", + "N": "#c:ingots/nerosteel", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "NGN", + "NFN", + "NRN" + ], + "result": { + "id": "nerospace:fuel_refinery" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/fuel_tank.json b/multiloader/common/src/main/resources/data/nerospace/recipe/fuel_tank.json new file mode 100644 index 0000000..fef1559 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/fuel_tank.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "nerospace:rocket_fuel_canister", + "G": "#c:glass_blocks/colorless", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NGN", + "GCG", + "NGN" + ], + "result": { + "id": "nerospace:fuel_tank" + } +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 217703c..aa61650 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -133,6 +133,25 @@ public void onInitialize() { ENERGY.registerForBlockEntity( (be, direction) -> be.getEnergy(), ModBlockEntities.CREATIVE_BATTERY.get()); + + // Fuel Tank: fluid out (pipes), canister in (hoppers/pipes). + FLUID.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.FUEL_TANK.get()); + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.FUEL_TANK.get()); + + // Fuel Refinery: grid power in, refined fuel out, coal + blaze powder in. + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.FUEL_REFINERY.get()); + FLUID.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.FUEL_REFINERY.get()); + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.FUEL_REFINERY.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index ce86f23..dd35183 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -11,6 +11,8 @@ import za.co.neroland.nerospace.client.ClientEntityRenderers; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; +import za.co.neroland.nerospace.client.FuelRefineryScreen; +import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; @@ -25,6 +27,8 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); MenuScreens.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); MenuScreens.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); + MenuScreens.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); + MenuScreens.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { @Override diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties index 7df1f60..376d7d3 100644 --- a/multiloader/gradle.properties +++ b/multiloader/gradle.properties @@ -45,10 +45,10 @@ neo_version_26.1.2=26.1.2.76 # 26.2 beta is on Maven per the official MultiLoader-Template default. If it ever # fails to resolve, self-build the 26.2.x branch to mavenLocal() (see README) and # set this to the version it publishes — that is the ONLY change needed. -neo_version_26.2=26.2.0.3-beta +neo_version_26.2=26.2.0.6-beta ## Fabric (https://fabricmc.net/develop) ------------------------------------ fabric_loader_version=0.19.3 # VERIFY the 26.1.2 value at fabricmc.net/develop (placeholder until confirmed). -fabric_api_version_26.1.2=0.150.0+26.1.2 +fabric_api_version_26.1.2=0.151.0+26.1.2 fabric_api_version_26.2=0.152.1+26.2 diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index e0a13d4..6e7b739 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -151,5 +151,33 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ENERGY, ModBlockEntities.CREATIVE_BATTERY.get(), (be, side) -> be.getEnergy()); + + // Fuel Tank: fluid out (pipes), canister in (hoppers/pipes). + event.registerBlockEntity( + FLUID, + ModBlockEntities.FUEL_TANK.get(), + (be, side) -> be.getTank()); + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.FUEL_TANK.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + + // Fuel Refinery: grid power in, refined fuel out, coal + blaze powder in. + event.registerBlockEntity( + ENERGY, + ModBlockEntities.FUEL_REFINERY.get(), + (be, side) -> be.getEnergy()); + event.registerBlockEntity( + FLUID, + ModBlockEntities.FUEL_REFINERY.get(), + (be, side) -> be.getTank()); + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.FUEL_REFINERY.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 7736a72..7582395 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -16,6 +16,8 @@ import za.co.neroland.nerospace.client.ClientEntityRenderers; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; +import za.co.neroland.nerospace.client.FuelRefineryScreen; +import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.fluid.ModFluids; @@ -47,6 +49,8 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); event.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); event.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); + event.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); + event.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); } /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ From 217aa8548dc1b022515e89a374be1daa29be3125 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:15:39 +0200 Subject: [PATCH 37/82] Add quarry machine, blocks, BE, UI Introduce a complete quarry machine: controller block and block entity implementing the mining logic, frame and landmark blocks/entities, region discovery, chunk forcing, auto-ejecting output, fluid sucking, energy/fluid buffers, persistence and container access. Add UI and menu (QuarryScreen, QuarryMenu) and supporting types (MinerTier, PlanetMiningProfile, OutputFilter, QuarryRegion). Update registries and NeoForge capabilities to wire the new parts. This implements the core quarry functionality (frame building, layered excavation, buffering/ejection and status syncing) necessary to run and control the machine. --- docs/MULTILOADER_PORT_CHECKLIST.md | 25 +- .../nerospace/client/QuarryScreen.java | 55 ++ .../nerospace/machine/quarry/MinerTier.java | 75 ++ .../machine/quarry/OutputFilter.java | 16 + .../machine/quarry/PlanetMiningProfile.java | 31 + .../machine/quarry/QuarryControllerBlock.java | 77 ++ .../quarry/QuarryControllerBlockEntity.java | 737 ++++++++++++++++++ .../machine/quarry/QuarryFrameBlock.java | 24 + .../machine/quarry/QuarryLandmarkBlock.java | 63 ++ .../quarry/QuarryLandmarkBlockEntity.java | 43 + .../nerospace/machine/quarry/QuarryMenu.java | 155 ++++ .../machine/quarry/QuarryRegion.java | 190 +++++ .../nerospace/registry/ModBlockEntities.java | 10 + .../nerospace/registry/ModBlocks.java | 20 + .../neroland/nerospace/registry/ModItems.java | 4 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../blockstates/quarry_controller.json | 7 + .../nerospace/blockstates/quarry_frame.json | 7 + .../blockstates/quarry_landmark.json | 7 + .../nerospace/items/quarry_controller.json | 6 + .../nerospace/items/quarry_landmark.json | 6 + .../assets/nerospace/lang/en_us.json | 9 + .../models/block/quarry_controller.json | 6 + .../nerospace/models/block/quarry_frame.json | 393 ++++++++++ .../models/block/quarry_landmark.json | 6 + .../textures/block/quarry_controller.png | Bin 0 -> 463 bytes .../nerospace/textures/block/quarry_drill.png | Bin 0 -> 417 bytes .../nerospace/textures/block/quarry_frame.png | Bin 0 -> 441 bytes .../textures/block/quarry_gantry.png | Bin 0 -> 464 bytes .../textures/block/quarry_landmark.png | Bin 0 -> 371 bytes .../assets/nerospace/textures/gui/quarry.png | Bin 0 -> 10877 bytes .../loot_table/blocks/quarry_controller.json | 21 + .../loot_table/blocks/quarry_landmark.json | 21 + .../nerospace/recipe/quarry_controller.json | 18 + .../nerospace/recipe/quarry_landmark.json | 18 + .../nerospace/fabric/NerospaceFabric.java | 11 + .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeCapabilities.java | 16 + .../neoforge/NeoForgeClientSetup.java | 2 + 39 files changed, 2080 insertions(+), 6 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuarryScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/MinerTier.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/OutputFilter.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/PlanetMiningProfile.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryFrameBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryRegion.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_controller.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_frame.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_landmark.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/quarry_controller.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/quarry_landmark.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_controller.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_frame.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_landmark.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_controller.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_drill.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_frame.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_gantry.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_landmark.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/quarry.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_controller.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_landmark.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/quarry_controller.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/recipe/quarry_landmark.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 455f92d..9338adc 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,17 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~110 classes ported, ~154 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~121 classes ported, ~143 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-20 update — quarry ported.** All 4 cells green. Added 11 classes: +> `machine/quarry/{MinerTier, QuarryRegion, OutputFilter, PlanetMiningProfile, QuarryFrameBlock, +> QuarryLandmarkBlock, QuarryLandmarkBlockEntity, QuarryControllerBlock, QuarryControllerBlockEntity, +> QuarryMenu}` + `client/QuarryScreen`. The 1000-line controller was rebuilt on the shared +> `EnergyBuffer`/`FluidTank` + a vanilla `WorldlyContainer` (frame in, output out); force-loads via +> vanilla `ServerLevel.setChunkForced` (no ticket seam); modules + the drill-head BER + fluid auto-eject +> deferred. Energy/Item/Fluid caps wired on both loaders; assets + 9 lang keys copied. + > **2026-06-20 update — fuel machines ported.** All 4 cells green. Added 8 classes: > `machine/{FuelTankBlock, FuelTankBlockEntity, FuelRefineryBlock, FuelRefineryBlockEntity}` + > `menu/{FuelTankMenu, FuelRefineryMenu}` + `client/{FuelTankScreen, FuelRefineryScreen}`, registered @@ -71,10 +79,17 @@ checked by a headless build). criterion — **deferred**: needs the data-attachment + criteria seams (+ structures). The Orbital Station destination currently docks the rider at the shared origin platform. -### Quarry (`machine/quarry/` 11 + client) -- [ ] Area miner: controller block/BE + menu/screen, frame + landmark blocks/BE, `QuarryRegion`, - `MinerTier`, `OutputFilter`, `PlanetMiningProfile`, `QuarryChunkLoader`. Risk: **high** (chunk-loading, - fake-player-style mining, multiblock). Chunk-loading needs a cross-loader seam (NeoForge ticket API vs Fabric). +### Quarry (`machine/quarry/` 11 + client) — **DONE (4 cells green); modules + BER deferred** +- [x] Area miner ported: `QuarryControllerBlock`(+BE) + `QuarryMenu`/`QuarryScreen`, `QuarryFrameBlock`, + `QuarryLandmarkBlock`(+BE, client laser ticker), `QuarryRegion`, `MinerTier`, `OutputFilter`, + `PlanetMiningProfile`. The dig (landmarks → frame ring → layer-by-layer excavation → drops buffered/ + auto-ejected, source fluids sucked) runs server-side; Energy/Item/Fluid caps on both loaders. +- [~] **Chunk-loading**: `QuarryChunkLoader` (NeoForge `TicketController`) replaced by vanilla + `ServerLevel.setChunkForced` (works on both loaders; one chunk pinned at a time, persisted + released + on removal) — no cross-loader ticket seam needed. +- [~] **Deferred**: upgrade modules (controller runs at ×1.0 speed/energy, no Silk/Fortune, no module + slots — depends on the `module/` batch); the moving drill-head BER (`QuarryControllerRenderer`); and + fluid **auto-eject** (the fluid buffer is drained by pipes instead). `Tuning` values inlined. ### Fuel machines (`machine/Fuel*` — depends on the ported rocket-fuel fluid) — **DONE (4 cells green)** - [x] `FuelTankBlock`(+BE +menu +screen): stores `rocket_fuel`, accepts buckets/canisters, auto-fuels a diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuarryScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuarryScreen.java new file mode 100644 index 0000000..bc10dc7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/QuarryScreen.java @@ -0,0 +1,55 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.machine.quarry.MinerTier; +import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlockEntity; +import za.co.neroland.nerospace.machine.quarry.QuarryMenu; + +/** + * Screen for the quarry controller: sci-fi panel with a power gauge, the dig state, current depth and a + * fluid-buffer gauge, around the frame/output slots. + */ +public class QuarryScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/quarry.png"); + private static final int ACCENT = MinerTier.TIER_1.accentColor(); + private static final int FLUID = 0xFF4FA8FF; + + public QuarryScreen(QuarryMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 210); + this.titleLabelX = 8; + this.inventoryLabelX = 8; + this.inventoryLabelY = 116; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int energy = this.menu.getEnergy(); + int maxEnergy = this.menu.getMaxEnergy(); + float energyFrac = maxEnergy == 0 ? 0f : (float) energy / maxEnergy; + int pct = maxEnergy == 0 ? 0 : energy * 100 / maxEnergy; + + label(g, Component.literal("Power: " + pct + "%"), 8, 80, TITLE); + segGauge(g, 8, 90, 160, 3, energyFrac, ACCENT); + + QuarryControllerBlockEntity.State state = this.menu.getState(); + label(g, Component.translatable("gui.nerospace.quarry.state." + state.name().toLowerCase(java.util.Locale.ROOT)), + 8, 97, SUBTLE); + int depth = Math.max(0, this.menu.getRefY() - this.menu.getCurrentY()); + if (state != QuarryControllerBlockEntity.State.IDLE) { + label(g, Component.literal("Depth: " + depth), 110, 97, SUBTLE); + } + + int fluid = this.menu.getFluid(); + int maxFluid = this.menu.getMaxFluid(); + float fluidFrac = maxFluid == 0 ? 0f : (float) fluid / maxFluid; + label(g, Component.literal("Fluid: " + fluid + " mB"), 8, 106, 0xFFB9D7FF); + fluidGauge(g, 96, 107, 72, 3, fluidFrac, FLUID); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/MinerTier.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/MinerTier.java new file mode 100644 index 0000000..56afef5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/MinerTier.java @@ -0,0 +1,75 @@ +package za.co.neroland.nerospace.machine.quarry; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** + * Quarry progression tiers. A tier gates the largest square the quarry may claim ({@link #maxAreaSide}), + * how many upgrade-module slots it has ({@link #moduleSlots}), and the base per-cycle work ceiling + * ({@link #baseBlocksPerCycle}). Planets are gated independently: Cindara wants Tier 2, Glacira wants + * Tier 3; everything else runs at Tier 1. + */ +public enum MinerTier { + + TIER_1(1, 16, 1, 2, 0xFFE0405A), + TIER_2(2, 32, 2, 4, 0xFFB060E0), + TIER_3(3, 64, 4, 8, 0xFFE0C040); + + private final int level; + private final int maxAreaSide; + private final int moduleSlots; + private final int baseBlocksPerCycle; + private final int accentColor; + + MinerTier(int level, int maxAreaSide, int moduleSlots, int baseBlocksPerCycle, int accentColor) { + this.level = level; + this.maxAreaSide = maxAreaSide; + this.moduleSlots = moduleSlots; + this.baseBlocksPerCycle = baseBlocksPerCycle; + this.accentColor = accentColor; + } + + public int level() { + return this.level; + } + + public int maxAreaSide() { + return this.maxAreaSide; + } + + public int moduleSlots() { + return this.moduleSlots; + } + + public int baseBlocksPerCycle() { + return this.baseBlocksPerCycle; + } + + public int accentColor() { + return this.accentColor; + } + + public boolean canOperateIn(ResourceKey dimension) { + return this.level >= requiredTier(dimension); + } + + public static int requiredTier(ResourceKey dimension) { + if (ModDimensions.GLACIRA_LEVEL.equals(dimension)) { + return 3; + } + if (ModDimensions.CINDARA_LEVEL.equals(dimension)) { + return 2; + } + return 1; + } + + public static MinerTier byOrdinal(int ordinal) { + MinerTier[] values = values(); + if (ordinal < 0 || ordinal >= values.length) { + return TIER_1; + } + return values[ordinal]; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/OutputFilter.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/OutputFilter.java new file mode 100644 index 0000000..649aa72 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/OutputFilter.java @@ -0,0 +1,16 @@ +package za.co.neroland.nerospace.machine.quarry; + +import net.minecraft.world.item.ItemStack; + +/** + * The seam for the quarry output filter. Every mined drop passes through {@link #keep(ItemStack)} + * before it is buffered; the only implementation is {@link #KEEP_ALL}, so nothing is voided. + */ +@FunctionalInterface +public interface OutputFilter { + + OutputFilter KEEP_ALL = stack -> true; + + /** @return whether {@code drop} should be kept (buffered/output) rather than voided. */ + boolean keep(ItemStack drop); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/PlanetMiningProfile.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/PlanetMiningProfile.java new file mode 100644 index 0000000..63a577a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/PlanetMiningProfile.java @@ -0,0 +1,31 @@ +package za.co.neroland.nerospace.machine.quarry; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** + * Per-planet mining characteristics. The dense, deep moons mine slower; the established planets mine at + * the baseline. A pure data lookup keyed by dimension, with a default for anything unlisted. + */ +public record PlanetMiningProfile(double speedMultiplier, double bonusDropChance) { + + private static final PlanetMiningProfile DEFAULT = new PlanetMiningProfile(1.0D, 0.0D); + private static final PlanetMiningProfile GREENXERTZ = new PlanetMiningProfile(1.0D, 0.0D); + private static final PlanetMiningProfile CINDARA = new PlanetMiningProfile(0.8D, 0.0D); + private static final PlanetMiningProfile GLACIRA = new PlanetMiningProfile(0.7D, 0.0D); + + public static PlanetMiningProfile forDimension(ResourceKey dimension) { + if (ModDimensions.GREENXERTZ_LEVEL.equals(dimension)) { + return GREENXERTZ; + } + if (ModDimensions.CINDARA_LEVEL.equals(dimension)) { + return CINDARA; + } + if (ModDimensions.GLACIRA_LEVEL.equals(dimension)) { + return GLACIRA; + } + return DEFAULT; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlock.java new file mode 100644 index 0000000..213ffc5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlock.java @@ -0,0 +1,77 @@ +package za.co.neroland.nerospace.machine.quarry; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The quarry controller block. Holds its {@link MinerTier}; backed by {@link QuarryControllerBlockEntity}. + * Right-click opens the menu. Activation is automatic: with valid landmarks nearby and power available, + * it builds its frame and starts mining. + */ +public class QuarryControllerBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = + simpleCodec(props -> new QuarryControllerBlock(props, MinerTier.TIER_1)); + + private final MinerTier tier; + + public QuarryControllerBlock(Properties properties) { + this(properties, MinerTier.TIER_1); + } + + public QuarryControllerBlock(Properties properties, MinerTier tier) { + super(properties); + this.tier = tier; + } + + public MinerTier tier() { + return this.tier; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new QuarryControllerBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.QUARRY_CONTROLLER.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof QuarryControllerBlockEntity controller) { + serverPlayer.openMenu(controller); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java new file mode 100644 index 0000000..a761924 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java @@ -0,0 +1,737 @@ +package za.co.neroland.nerospace.machine.quarry; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.LiquidBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.fluid.FluidTank; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The quarry controller: the single block that runs the dig. Once landmarks mark a region, it builds a + * frame ring and excavates the rectangle layer-by-layer from the landmark plane down to the world floor. + * Mined items buffer internally and auto-eject to adjacent storage; source fluids are sucked into a + * fluid buffer (drained by pipes). Mining pauses (never loses items) when the buffers fill or power runs + * out. Throughput scales with supplied power up to the tier's ceiling × the planet's speed factor. + * + *

Cross-loader port note. The root binds the buffers to the NeoForge transfer API, uses a + * NeoForge chunk-ticket controller, and supports upgrade modules. The multiloader rebuilds the buffers + * on the shared {@link EnergyBuffer}/{@link FluidTank} and a vanilla {@link WorldlyContainer} (frame in, + * output out); force-loads via vanilla {@link ServerLevel#setChunkForced}; and defers the modules + * (speed/energy = 1.0, no Silk/Fortune, no module slots) and the moving drill-head BER. {@code Tuning} + * values are inlined. Fluids leave via pipe extraction (no auto-eject); items auto-eject to adjacent + * vanilla containers.

+ */ +public class QuarryControllerBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int OUTPUT_SLOTS = 12; + public static final int FRAME_SLOT = 0; + private static final int OUTPUT_START = 1; + public static final int ENERGY_MAX_INSERT = 10_000; + public static final int DATA_COUNT = 7; + private static final int SCAN_BUDGET_PER_TICK = 4096; + + // Inlined Tuning base values. + private static final int ENERGY_BUFFER = 200_000; + private static final int FLUID_CAPACITY = 16_000; + private static final int ENERGY_PER_BLOCK = 40; + private static final int MINE_INTERVAL = 8; + + public enum State { + IDLE, BUILDING_FRAME, MINING, DONE, PAUSED + } + + private final MinerTier tier; + private final int containerSize; + + private final EnergyBuffer energy = new EnergyBuffer(ENERGY_BUFFER, ENERGY_MAX_INSERT, 0, this::setChanged); + private final FluidTank fluidBuffer = new FluidTank(FLUID_CAPACITY, this::setChanged); + private final NonNullList items; + private final OutputFilter filter = OutputFilter.KEEP_ALL; + + private State state = State.IDLE; + private String pauseReason = ""; + @Nullable + private QuarryRegion region; + private int frameIndex; + private int currentY; + private int cursor; + private final Set skippedColumns = new HashSet<>(); + private final Set forcedChunks = new HashSet<>(); + private transient int frameTotal = -1; + + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> (int) energy.getAmount(); + case 1 -> (int) energy.getCapacity(); + case 2 -> state.ordinal(); + case 3 -> (int) fluidBuffer.getAmount(); + case 4 -> (int) fluidBuffer.getCapacity(); + case 5 -> currentY; + case 6 -> { + QuarryRegion rg = region; + yield rg == null ? 0 : rg.refY(); + } + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + // server-authoritative + } + + @Override + public int getCount() { + return DATA_COUNT; + } + }; + + public QuarryControllerBlockEntity(BlockPos pos, BlockState blockState) { + super(ModBlockEntities.QUARRY_CONTROLLER.get(), pos, blockState); + this.tier = blockState.getBlock() instanceof QuarryControllerBlock controller + ? controller.tier() : MinerTier.TIER_1; + this.containerSize = 1 + OUTPUT_SLOTS; // frame + output (modules deferred) + this.items = NonNullList.withSize(this.containerSize, ItemStack.EMPTY); + } + + // --- Capability accessors --------------------------------------------------- + + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + public NerospaceFluidStorage getTank() { + return this.fluidBuffer; + } + + public ContainerData getDataAccess() { + return this.dataAccess; + } + + public MinerTier tier() { + return this.tier; + } + + // --- Ticking ---------------------------------------------------------------- + + public void tick(Level level, BlockPos pos, BlockState blockState) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + switch (this.state) { + case IDLE -> tryActivate(serverLevel, pos); + case BUILDING_FRAME -> buildFrame(serverLevel); + case MINING -> { + if (serverLevel.getGameTime() % miningInterval(serverLevel) == 0L) { + mine(serverLevel); + } + } + case PAUSED -> resume(serverLevel, pos); + case DONE -> { } + default -> { } + } + if (this.region != null) { + autoEject(serverLevel, pos); + } + } + + private void resume(ServerLevel level, BlockPos pos) { + if (this.region == null) { + this.state = State.IDLE; + tryActivate(level, pos); + return; + } + if (!this.tier.canOperateIn(level.dimension())) { + this.pauseReason = "wrong_planet"; + return; + } + if (this.frameIndex < frameTotal()) { + this.state = State.BUILDING_FRAME; + buildFrame(level); + } else { + this.state = State.MINING; + mine(level); + } + } + + private void tryActivate(ServerLevel level, BlockPos pos) { + if (level.getGameTime() % 20L != 0L) { + return; + } + if (!this.tier.canOperateIn(level.dimension())) { + setPaused("wrong_planet"); + return; + } + BlockPos seed = QuarryRegion.findNearbyLandmark(level, pos, this.tier.maxAreaSide()); + if (seed == null) { + return; + } + QuarryRegion found = QuarryRegion.fromLandmarks(level, seed, this.tier.maxAreaSide()); + if (found == null) { + setPaused("bad_region"); + return; + } + this.region = found; + this.frameTotal = -1; + consumeLandmarks(level, found); + this.frameIndex = 0; + this.currentY = found.refY(); + this.cursor = 0; + this.skippedColumns.clear(); + this.state = State.BUILDING_FRAME; + this.pauseReason = ""; + setChanged(); + } + + private int frameTotal() { + QuarryRegion rg = this.region; + if (this.frameTotal < 0 && rg != null) { + this.frameTotal = rg.framePositions().size(); + } + return Math.max(0, this.frameTotal); + } + + private void consumeLandmarks(ServerLevel level, QuarryRegion region) { + for (int x = region.minX(); x <= region.maxX(); x++) { + for (int z = region.minZ(); z <= region.maxZ(); z++) { + BlockPos lp = new BlockPos(x, region.refY(), z); + if (level.getBlockState(lp).getBlock() instanceof QuarryLandmarkBlock) { + level.removeBlock(lp, false); + } + } + } + } + + private void buildFrame(ServerLevel level) { + QuarryRegion region = this.region; + if (region == null) { + this.state = State.IDLE; + return; + } + List ring = region.framePositions(); + int placedThisTick = 0; + boolean changed = false; + while (this.frameIndex < ring.size() && placedThisTick < 8) { + BlockPos fp = ring.get(this.frameIndex); + BlockState existing = level.getBlockState(fp); + if (existing.getBlock() instanceof QuarryFrameBlock) { + this.frameIndex++; + continue; + } + if (fp.equals(this.worldPosition) || existing.hasBlockEntity()) { + this.frameIndex++; + continue; + } + ItemStack casing = this.items.get(FRAME_SLOT); + if (casing.isEmpty()) { + setPaused("need_material"); + return; + } + level.setBlock(fp, ModBlocks.QUARRY_FRAME.get().defaultBlockState(), Block.UPDATE_CLIENTS); + casing.shrink(1); + placedThisTick++; + this.frameIndex++; + changed = true; + } + if (this.frameIndex >= ring.size()) { + this.state = State.MINING; + this.currentY = region.refY() - 1; + this.pauseReason = ""; + changed = true; + } + if (changed) { + setChanged(); + } + } + + private void mine(ServerLevel level) { + QuarryRegion region = this.region; + if (region == null) { + this.state = State.IDLE; + return; + } + int floor = level.getMinY(); + int energyPerBlock = ENERGY_PER_BLOCK; + ItemStack tool = new ItemStack(Items.NETHERITE_PICKAXE); + int columns = region.columns(); + boolean changed = false; + + int scanned = 0; + for (int processed = 0; processed < 1 && scanned < SCAN_BUDGET_PER_TICK; ) { + scanned++; + if (this.currentY < floor) { + this.state = State.DONE; + releaseForcedChunks(level); + changed = true; + break; + } + if (this.cursor >= columns) { + this.cursor = 0; + this.currentY--; + continue; + } + BlockPos target = region.columnPos(this.cursor, this.currentY); + int x = target.getX(); + int z = target.getZ(); + + if (region.isPerimeter(x, z)) { + this.cursor++; + continue; + } + if (isColumnSkipped(x, z)) { + this.cursor++; + continue; + } + + int cx = x >> 4; + int cz = z >> 4; + if (!level.hasChunk(cx, cz)) { + forceLoad(level, cx, cz); + changed = true; + break; + } + + BlockState state = level.getBlockState(target); + if (state.isAir() || state.getBlock() instanceof QuarryFrameBlock) { + this.cursor++; + continue; + } + if (state.hasBlockEntity()) { + markColumnSkipped(x, z); + this.cursor++; + continue; + } + + FluidState fluidState = state.getFluidState(); + if (state.getBlock() instanceof LiquidBlock && fluidState.isSource()) { + if (!suckFluid(level, target, fluidState)) { + setPaused("fluid_full"); + changed = true; + break; + } + this.cursor++; + changed = true; + continue; + } + + if (state.getDestroySpeed(level, target) < 0.0F) { + this.cursor++; + continue; + } + + if (this.energy.getAmount() < energyPerBlock) { + setPaused("no_power"); + changed = true; + break; + } + + List drops = Block.getDrops(state, level, target, null, null, tool); + if (!acceptDrops(drops)) { + setPaused("buffer_full"); + changed = true; + break; + } + level.removeBlock(target, false); + spawnDrillFx(level, target); + this.energy.consume(energyPerBlock); + this.cursor++; + processed++; + changed = true; + } + + if (changed) { + setChanged(); + } + } + + private long miningInterval(ServerLevel level) { + double planet = PlanetMiningProfile.forDimension(level.dimension()).speedMultiplier(); + double rate = this.tier.baseBlocksPerCycle() * planet; // modules deferred (×1.0) + return Math.max(1L, Math.round(MINE_INTERVAL / Math.max(0.01, rate))); + } + + /** Try to buffer all kept drops atomically into the output slots; filtered-out drops are voided. */ + private boolean acceptDrops(List drops) { + List kept = new ArrayList<>(); + for (ItemStack drop : drops) { + if (!drop.isEmpty() && this.filter.keep(drop)) { + kept.add(drop.copy()); + } + } + if (kept.isEmpty()) { + return true; + } + ItemStack[] sim = new ItemStack[OUTPUT_SLOTS]; + for (int i = 0; i < OUTPUT_SLOTS; i++) { + sim[i] = this.items.get(OUTPUT_START + i).copy(); + } + for (ItemStack drop : kept) { + if (!mergeInto(sim, drop)) { + return false; + } + } + for (int i = 0; i < OUTPUT_SLOTS; i++) { + this.items.set(OUTPUT_START + i, sim[i]); + } + return true; + } + + private static boolean mergeInto(ItemStack[] slots, ItemStack stack) { + for (int i = 0; i < slots.length && !stack.isEmpty(); i++) { + ItemStack slot = slots[i]; + if (!slot.isEmpty() && ItemStack.isSameItemSameComponents(slot, stack)) { + int room = Math.min(slot.getMaxStackSize(), 64) - slot.getCount(); + int moved = Math.min(room, stack.getCount()); + if (moved > 0) { + slot.grow(moved); + stack.shrink(moved); + } + } + } + for (int i = 0; i < slots.length && !stack.isEmpty(); i++) { + if (slots[i].isEmpty()) { + slots[i] = stack.copy(); + stack.setCount(0); + } + } + return stack.isEmpty(); + } + + private void spawnDrillFx(ServerLevel level, BlockPos target) { + double cx = target.getX() + 0.5; + double cy = target.getY() + 0.5; + double cz = target.getZ() + 0.5; + level.sendParticles(net.minecraft.core.particles.ParticleTypes.ELECTRIC_SPARK, + cx, cy, cz, 3, 0.2, 0.2, 0.2, 0.0); + } + + private boolean suckFluid(ServerLevel level, BlockPos pos, FluidState fluidState) { + Fluid fluid = fluidState.getType(); + if (this.fluidBuffer.fill(fluid, 1000, true) >= 1000) { + this.fluidBuffer.fill(fluid, 1000, false); + level.removeBlock(pos, false); + return true; + } + return false; + } + + // --- Skipped-column bookkeeping -------------------------------------------- + + private int columnKey(int x, int z) { + QuarryRegion region = this.region; + if (region == null) { + return -1; + } + return (x - region.minX()) * 128 + (z - region.minZ()); + } + + private boolean isColumnSkipped(int x, int z) { + return this.skippedColumns.contains(columnKey(x, z)); + } + + private void markColumnSkipped(int x, int z) { + this.skippedColumns.add(columnKey(x, z)); + } + + // --- Auto-eject (items → adjacent vanilla containers) ----------------------- + + private void autoEject(ServerLevel level, BlockPos pos) { + for (Direction dir : Direction.values()) { + if (level.getBlockEntity(pos.relative(dir)) instanceof Container target && !(target instanceof QuarryControllerBlockEntity)) { + ejectInto(target); + } + } + } + + /** Push output stacks into a neighbour container (best-effort merge). */ + private void ejectInto(Container target) { + for (int i = 0; i < OUTPUT_SLOTS; i++) { + ItemStack stack = this.items.get(OUTPUT_START + i); + if (stack.isEmpty()) { + continue; + } + for (int t = 0; t < target.getContainerSize() && !stack.isEmpty(); t++) { + if (!target.canPlaceItem(t, stack)) { + continue; + } + ItemStack dest = target.getItem(t); + if (dest.isEmpty()) { + target.setItem(t, stack.copy()); + stack.setCount(0); + } else if (ItemStack.isSameItemSameComponents(dest, stack)) { + int room = Math.min(dest.getMaxStackSize(), target.getMaxStackSize()) - dest.getCount(); + int moved = Math.min(room, stack.getCount()); + if (moved > 0) { + dest.grow(moved); + stack.shrink(moved); + } + } + } + this.items.set(OUTPUT_START + i, stack.isEmpty() ? ItemStack.EMPTY : stack); + } + target.setChanged(); + setChanged(); + } + + // --- Chunk loading (vanilla force-load; one chunk pinned at a time) ---------- + + private void forceLoad(ServerLevel level, int cx, int cz) { + long key = ((long) cx << 32) | (cz & 0xFFFFFFFFL); + Iterator it = this.forcedChunks.iterator(); + while (it.hasNext()) { + long k = it.next(); + if (k != key) { + level.setChunkForced((int) (k >> 32), (int) (k & 0xFFFFFFFFL), false); + it.remove(); + } + } + if (this.forcedChunks.add(key)) { + level.setChunkForced(cx, cz, true); + } + } + + private void releaseForcedChunks(ServerLevel level) { + for (long key : this.forcedChunks) { + level.setChunkForced((int) (key >> 32), (int) (key & 0xFFFFFFFFL), false); + } + this.forcedChunks.clear(); + } + + private void setPaused(String reason) { + this.state = State.PAUSED; + this.pauseReason = reason; + setChanged(); + } + + @Override + public void setRemoved() { + if (this.level instanceof ServerLevel serverLevel) { + releaseForcedChunks(serverLevel); + } + super.setRemoved(); + } + + @Override + public void preRemoveSideEffects(BlockPos pos, BlockState state) { + super.preRemoveSideEffects(pos, state); + if (this.level instanceof ServerLevel serverLevel) { + removeFrame(serverLevel); + } + } + + private void removeFrame(ServerLevel level) { + QuarryRegion region = this.region; + if (region == null) { + return; + } + for (BlockPos fp : region.framePositions()) { + if (level.getBlockState(fp).getBlock() instanceof QuarryFrameBlock) { + level.removeBlock(fp, false); + } + } + } + + // --- Persistence ------------------------------------------------------------ + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + output.putString("Fluid", BuiltInRegistries.FLUID.getKey(this.fluidBuffer.getRawFluid()).toString()); + output.putInt("FluidAmount", this.fluidBuffer.getRawAmount()); + output.store("Frame", ItemStack.OPTIONAL_CODEC, this.items.get(FRAME_SLOT)); + for (int i = 0; i < OUTPUT_SLOTS; i++) { + output.store("Out" + i, ItemStack.OPTIONAL_CODEC, this.items.get(OUTPUT_START + i)); + } + output.putString("MinerState", this.state.name()); + output.putString("PauseReason", this.pauseReason); + output.putInt("FrameIndex", this.frameIndex); + output.putInt("CurrentY", this.currentY); + output.putInt("Cursor", this.cursor); + QuarryRegion region = this.region; + output.putBoolean("HasRegion", region != null); + if (region != null) { + region.save(output.child("Region")); + } + int[] skipped = this.skippedColumns.stream().mapToInt(Integer::intValue).toArray(); + output.putInt("SkipCount", skipped.length); + for (int i = 0; i < skipped.length; i++) { + output.putInt("Skip" + i, skipped[i]); + } + long[] chunks = this.forcedChunks.stream().mapToLong(Long::longValue).toArray(); + output.putInt("ChunkCount", chunks.length); + for (int i = 0; i < chunks.length; i++) { + output.putLong("Chunk" + i, chunks[i]); + } + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + Fluid fluid = BuiltInRegistries.FLUID.getValue(Identifier.parse(input.getStringOr("Fluid", "minecraft:empty"))); + this.fluidBuffer.setRaw(fluid, input.getIntOr("FluidAmount", 0)); + this.items.set(FRAME_SLOT, input.read("Frame", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + for (int i = 0; i < OUTPUT_SLOTS; i++) { + this.items.set(OUTPUT_START + i, input.read("Out" + i, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + this.state = parseState(input.getStringOr("MinerState", State.IDLE.name())); + this.pauseReason = input.getStringOr("PauseReason", ""); + this.frameIndex = input.getIntOr("FrameIndex", 0); + this.currentY = input.getIntOr("CurrentY", 0); + this.cursor = input.getIntOr("Cursor", 0); + this.region = input.getBooleanOr("HasRegion", false) + ? QuarryRegion.load(input.childOrEmpty("Region")) : null; + this.frameTotal = -1; + this.skippedColumns.clear(); + int skipCount = input.getIntOr("SkipCount", 0); + for (int i = 0; i < skipCount; i++) { + this.skippedColumns.add(input.getIntOr("Skip" + i, -1)); + } + this.forcedChunks.clear(); + int chunkCount = input.getIntOr("ChunkCount", 0); + for (int i = 0; i < chunkCount; i++) { + this.forcedChunks.add(input.getLongOr("Chunk" + i, 0L)); + } + } + + private static State parseState(String name) { + try { + return State.valueOf(name); + } catch (IllegalArgumentException ex) { + return State.IDLE; + } + } + + // --- MenuProvider ----------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.quarry_controller"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new QuarryMenu(containerId, playerInventory, this, this.dataAccess); + } + + // --- WorldlyContainer (slot 0 = frame in; slots 1.. = output out) ----------- + + @Override + public int[] getSlotsForFace(Direction side) { + int[] slots = new int[this.containerSize]; + for (int i = 0; i < slots.length; i++) { + slots[i] = i; + } + return slots; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return slot == FRAME_SLOT && stack.is(ModItems.FRAME_CASING.get()); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return slot >= OUTPUT_START; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return slot == FRAME_SLOT && stack.is(ModItems.FRAME_CASING.get()); + } + + @Override + public int getContainerSize() { + return this.containerSize; + } + + @Override + public boolean isEmpty() { + for (ItemStack stack : this.items) { + if (!stack.isEmpty()) { + return false; + } + } + return true; + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + if (this.level == null || this.level.getBlockEntity(this.worldPosition) != this) { + return false; + } + return player.distanceToSqr(this.worldPosition.getX() + 0.5, + this.worldPosition.getY() + 0.5, this.worldPosition.getZ() + 0.5) <= 64.0; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryFrameBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryFrameBlock.java new file mode 100644 index 0000000..3f5ad8b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryFrameBlock.java @@ -0,0 +1,24 @@ +package za.co.neroland.nerospace.machine.quarry; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.world.level.block.Block; + +/** + * The structural frame the quarry materialises around its claimed region. Built by the controller (one + * {@code frame_casing} per block) and removed when the controller is removed; carries no loot table. + * A dedicated class so the mining loop can recognise and skip frames. + */ +public class QuarryFrameBlock extends Block { + + public static final MapCodec CODEC = simpleCodec(QuarryFrameBlock::new); + + public QuarryFrameBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlock.java new file mode 100644 index 0000000..1a24e9b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlock.java @@ -0,0 +1,63 @@ +package za.co.neroland.nerospace.machine.quarry; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.shapes.CollisionContext; +import net.minecraft.world.phys.shapes.VoxelShape; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The quarry Landmark: a small marker post placed at the corners of the area to mine. Three forming an + * L define the rectangle; the controller scans them on activation and consumes them. Carries a + * {@link QuarryLandmarkBlockEntity} purely so the client can draw the projected marker lasers. + */ +public class QuarryLandmarkBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(QuarryLandmarkBlock::new); + + private static final VoxelShape SHAPE = Block.box(5.0D, 0.0D, 5.0D, 11.0D, 12.0D, 11.0D); + + public QuarryLandmarkBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new QuarryLandmarkBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (!level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.QUARRY_LANDMARK.get(), + (lvl, pos, st, be) -> be.clientTick(lvl, pos, st)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlockEntity.java new file mode 100644 index 0000000..3826243 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryLandmarkBlockEntity.java @@ -0,0 +1,43 @@ +package za.co.neroland.nerospace.machine.quarry; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Block entity for a {@link QuarryLandmarkBlock}: it exists so the client can animate the marker + * lasers — a glowing vertical beam plus marching projection dots along the four horizontal axes. + * Purely cosmetic; no server state. + */ +public class QuarryLandmarkBlockEntity extends BlockEntity { + + public QuarryLandmarkBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.QUARRY_LANDMARK.get(), pos, state); + } + + /** Client-only cosmetic tick: project the animated marker lasers. */ + public void clientTick(Level level, BlockPos pos, BlockState state) { + long time = level.getGameTime(); + if ((time & 3L) != 0L) { + return; + } + double x = pos.getX() + 0.5; + double y = pos.getY() + 0.5; + double z = pos.getZ() + 0.5; + + double rise = (time % 16L) * 0.06; + for (int i = 0; i < 3; i++) { + level.addParticle(ParticleTypes.END_ROD, x, y + i * 0.6 + rise, z, 0.0, 0.01, 0.0); + } + double march = (time % 12L) * 0.4; + for (Direction dir : Direction.Plane.HORIZONTAL) { + level.addParticle(ParticleTypes.GLOW, + x + dir.getStepX() * march, y, z + dir.getStepZ() * march, 0.0, 0.0, 0.0); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java new file mode 100644 index 0000000..d079038 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java @@ -0,0 +1,155 @@ +package za.co.neroland.nerospace.machine.quarry; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * Menu for the quarry controller. Layout: slot 0 = frame casing, then the output buffer; followed by + * the player inventory. Status (energy, state, fluid, depth) is synced through {@link ContainerData}. + * + *

Cross-loader port note: upgrade modules are deferred, so there are no module slots here.

+ */ +public class QuarryMenu extends AbstractContainerMenu { + + private static final int MACHINE_SLOTS = 1 + QuarryControllerBlockEntity.OUTPUT_SLOTS; + + private final Container container; + private final ContainerData data; + + /** Client constructor. */ + public QuarryMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, + new SimpleContainer(MACHINE_SLOTS), + new SimpleContainerData(QuarryControllerBlockEntity.DATA_COUNT)); + } + + /** Server constructor. */ + @SuppressWarnings("this-escape") + public QuarryMenu(int containerId, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.QUARRY_CONTROLLER.get(), containerId); + checkContainerSize(container, MACHINE_SLOTS); + this.container = container; + this.data = data; + + this.addSlot(new FrameSlot(container, QuarryControllerBlockEntity.FRAME_SLOT, 8, 20)); + + int outStart = 1; + for (int i = 0; i < QuarryControllerBlockEntity.OUTPUT_SLOTS; i++) { + int row = i / 6; + int col = i % 6; + this.addSlot(new OutputSlot(container, outStart + i, 8 + col * 18, 42 + row * 18)); + } + + this.addStandardInventorySlots(playerInventory, 8, 126); + this.addDataSlots(data); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack moved = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot == null || !slot.hasItem()) { + return ItemStack.EMPTY; + } + ItemStack raw = slot.getItem(); + moved = raw.copy(); + int playerStart = MACHINE_SLOTS; + int playerEnd = MACHINE_SLOTS + 36; + + if (index < MACHINE_SLOTS) { + if (!this.moveItemStackTo(raw, playerStart, playerEnd, true)) { + return ItemStack.EMPTY; + } + } else { + if (raw.is(ModItems.FRAME_CASING.get())) { + if (!this.moveItemStackTo(raw, QuarryControllerBlockEntity.FRAME_SLOT, + QuarryControllerBlockEntity.FRAME_SLOT + 1, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + } + + if (raw.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + if (raw.getCount() == moved.getCount()) { + return ItemStack.EMPTY; + } + slot.onTake(player, raw); + return moved; + } + + // --- Screen helpers --------------------------------------------------------- + + public int getEnergy() { + return this.data.get(0); + } + + public int getMaxEnergy() { + return this.data.get(1); + } + + public QuarryControllerBlockEntity.State getState() { + return QuarryControllerBlockEntity.State.values()[ + Math.floorMod(this.data.get(2), QuarryControllerBlockEntity.State.values().length)]; + } + + public int getFluid() { + return this.data.get(3); + } + + public int getMaxFluid() { + return this.data.get(4); + } + + public int getCurrentY() { + return this.data.get(5); + } + + public int getRefY() { + return this.data.get(6); + } + + // --- Slot kinds ------------------------------------------------------------- + + private static final class FrameSlot extends Slot { + FrameSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModItems.FRAME_CASING.get()); + } + } + + private static final class OutputSlot extends Slot { + OutputSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryRegion.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryRegion.java new file mode 100644 index 0000000..7d8cecb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryRegion.java @@ -0,0 +1,190 @@ +package za.co.neroland.nerospace.machine.quarry; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +/** + * The rectangular footprint a quarry mines, derived from its landmarks (3 forming an L). Landmarks + * "project" along the four horizontal axes; a flood-fill over those links collects the cluster and its + * X/Z bounding box becomes the mined rectangle. The reference plane {@link #refY()} is the landmarks' + * Y; mining runs from {@code refY - 1} down to the world floor. Immutable; persisted in NBT. + */ +public final class QuarryRegion { + + private final int minX; + private final int minZ; + private final int maxX; + private final int maxZ; + private final int refY; + + public QuarryRegion(int minX, int minZ, int maxX, int maxZ, int refY) { + this.minX = Math.min(minX, maxX); + this.minZ = Math.min(minZ, maxZ); + this.maxX = Math.max(minX, maxX); + this.maxZ = Math.max(minZ, maxZ); + this.refY = refY; + } + + public int minX() { + return this.minX; + } + + public int minZ() { + return this.minZ; + } + + public int maxX() { + return this.maxX; + } + + public int maxZ() { + return this.maxZ; + } + + public int refY() { + return this.refY; + } + + public int width() { + return this.maxX - this.minX + 1; + } + + public int length() { + return this.maxZ - this.minZ + 1; + } + + public int columns() { + return width() * length(); + } + + public boolean containsColumn(int x, int z) { + return x >= this.minX && x <= this.maxX && z >= this.minZ && z <= this.maxZ; + } + + public boolean isPerimeter(int x, int z) { + return x == this.minX || x == this.maxX || z == this.minZ || z == this.maxZ; + } + + public List framePositions() { + List out = new ArrayList<>(); + for (int x = this.minX; x <= this.maxX; x++) { + for (int z = this.minZ; z <= this.maxZ; z++) { + if (isPerimeter(x, z)) { + out.add(new BlockPos(x, this.refY, z)); + } + } + } + return out; + } + + public BlockPos columnPos(int index, int y) { + int w = width(); + int dx = index % w; + int dz = index / w; + return new BlockPos(this.minX + dx, y, this.minZ + dz); + } + + // --- Discovery from landmarks ------------------------------------------------ + + private static final int MAX_LANDMARKS = 16; + + @Nullable + public static QuarryRegion fromLandmarks(Level level, BlockPos seed, int maxSide) { + if (!isLandmark(level, seed)) { + return null; + } + Set cluster = new HashSet<>(); + Deque queue = new ArrayDeque<>(); + cluster.add(seed.immutable()); + queue.add(seed.immutable()); + int refY = seed.getY(); + + while (!queue.isEmpty() && cluster.size() < MAX_LANDMARKS) { + BlockPos pos = queue.poll(); + for (Direction dir : Direction.Plane.HORIZONTAL) { + BlockPos linked = projectToLandmark(level, pos, dir, maxSide); + if (linked != null && cluster.add(linked)) { + queue.add(linked); + } + } + } + + int minX = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (BlockPos pos : cluster) { + minX = Math.min(minX, pos.getX()); + minZ = Math.min(minZ, pos.getZ()); + maxX = Math.max(maxX, pos.getX()); + maxZ = Math.max(maxZ, pos.getZ()); + } + + int w = maxX - minX + 1; + int l = maxZ - minZ + 1; + if (cluster.size() < 2 || w < 2 || l < 2 || w > maxSide || l > maxSide) { + return null; + } + return new QuarryRegion(minX, minZ, maxX, maxZ, refY); + } + + @Nullable + public static BlockPos findNearbyLandmark(Level level, BlockPos origin, int range) { + for (Direction dir : Direction.Plane.HORIZONTAL) { + BlockPos found = projectToLandmark(level, origin, dir, range); + if (found != null) { + return found; + } + } + return null; + } + + @Nullable + private static BlockPos projectToLandmark(Level level, BlockPos from, Direction dir, int range) { + BlockPos.MutableBlockPos cursor = from.mutable(); + for (int step = 1; step <= range; step++) { + cursor.move(dir); + if (isLandmark(level, cursor)) { + return cursor.immutable(); + } + } + return null; + } + + private static boolean isLandmark(Level level, BlockPos pos) { + BlockState state = level.getBlockState(pos); + return state.getBlock() instanceof QuarryLandmarkBlock; + } + + // --- Persistence ------------------------------------------------------------- + + public void save(ValueOutput output) { + output.putInt("MinX", this.minX); + output.putInt("MinZ", this.minZ); + output.putInt("MaxX", this.maxX); + output.putInt("MaxZ", this.maxZ); + output.putInt("RefY", this.refY); + } + + public static QuarryRegion load(ValueInput input) { + return new QuarryRegion( + input.getIntOr("MinX", 0), + input.getIntOr("MinZ", 0), + input.getIntOr("MaxX", 0), + input.getIntOr("MaxZ", 0), + input.getIntOr("RefY", 0)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 2e70a23..fb92b87 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -9,6 +9,8 @@ import za.co.neroland.nerospace.machine.FuelRefineryBlockEntity; import za.co.neroland.nerospace.machine.FuelTankBlockEntity; import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; +import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlockEntity; +import za.co.neroland.nerospace.machine.quarry.QuarryLandmarkBlockEntity; import za.co.neroland.nerospace.machine.OxygenGeneratorBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; @@ -86,6 +88,14 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("fuel_refinery", key -> new BlockEntityType<>(FuelRefineryBlockEntity::new, java.util.Set.of(ModBlocks.FUEL_REFINERY.get()))); + public static final RegistryEntry> QUARRY_CONTROLLER = + BLOCK_ENTITIES.register("quarry_controller", + key -> new BlockEntityType<>(QuarryControllerBlockEntity::new, java.util.Set.of(ModBlocks.QUARRY_CONTROLLER.get()))); + + public static final RegistryEntry> QUARRY_LANDMARK = + BLOCK_ENTITIES.register("quarry_landmark", + key -> new BlockEntityType<>(QuarryLandmarkBlockEntity::new, java.util.Set.of(ModBlocks.QUARRY_LANDMARK.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 380a678..57a77b0 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -20,6 +20,10 @@ import za.co.neroland.nerospace.machine.OxygenGeneratorBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; import za.co.neroland.nerospace.machine.SolarPanelBlock; +import za.co.neroland.nerospace.machine.quarry.MinerTier; +import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlock; +import za.co.neroland.nerospace.machine.quarry.QuarryFrameBlock; +import za.co.neroland.nerospace.machine.quarry.QuarryLandmarkBlock; import za.co.neroland.nerospace.pipe.UniversalPipeBlock; import za.co.neroland.nerospace.rocket.LaunchGantryBlock; import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; @@ -181,6 +185,22 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL))); + // --- Quarry ------------------------------------------------------------- + public static final RegistryEntry QUARRY_CONTROLLER = BLOCKS.register("quarry_controller", + key -> new QuarryControllerBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL), MinerTier.TIER_1)); + + public static final RegistryEntry QUARRY_FRAME = BLOCKS.register("quarry_frame", + key -> new QuarryFrameBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.METAL).strength(1.5F, 6.0F) + .sound(SoundType.METAL).lightLevel(s -> 7).noOcclusion().noLootTable())); + + public static final RegistryEntry QUARRY_LANDMARK = BLOCKS.register("quarry_landmark", + key -> new QuarryLandmarkBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_RED).strength(1.0F, 3.0F) + .sound(SoundType.METAL).lightLevel(s -> 7).noOcclusion())); + // --- Rockets ------------------------------------------------------------ public static final RegistryEntry ROCKET_LAUNCH_PAD = BLOCKS.register("rocket_launch_pad", key -> new RocketLaunchPadBlock(BlockBehaviour.Properties.of() diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 373eb56..dee19df 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -78,6 +78,8 @@ public final class ModItems { public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); public static final RegistryEntry FUEL_TANK_ITEM = blockItem("fuel_tank", ModBlocks.FUEL_TANK); public static final RegistryEntry FUEL_REFINERY_ITEM = blockItem("fuel_refinery", ModBlocks.FUEL_REFINERY); + public static final RegistryEntry QUARRY_CONTROLLER_ITEM = blockItem("quarry_controller", ModBlocks.QUARRY_CONTROLLER); + public static final RegistryEntry QUARRY_LANDMARK_ITEM = blockItem("quarry_landmark", ModBlocks.QUARRY_LANDMARK); // --- Materials ---------------------------------------------------------- public static final RegistryEntry RAW_NEROSIUM = item("raw_nerosium"); @@ -209,7 +211,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get())); } private ModItems() { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 5b648d3..d5a8ab8 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -9,6 +9,7 @@ import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; import za.co.neroland.nerospace.menu.FuelRefineryMenu; import za.co.neroland.nerospace.menu.FuelTankMenu; +import za.co.neroland.nerospace.machine.quarry.QuarryMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; import za.co.neroland.nerospace.rocket.RocketMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -43,6 +44,10 @@ public final class ModMenuTypes { MENUS.register("fuel_refinery", key -> new MenuType<>(FuelRefineryMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> QUARRY_CONTROLLER = + MENUS.register("quarry_controller", + key -> new MenuType<>(QuarryMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_controller.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_controller.json new file mode 100644 index 0000000..0633ba2 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_controller.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/quarry_controller" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_frame.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_frame.json new file mode 100644 index 0000000..28514e7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_frame.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/quarry_frame" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_landmark.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_landmark.json new file mode 100644 index 0000000..74d3609 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/quarry_landmark.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/quarry_landmark" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/quarry_controller.json b/multiloader/common/src/main/resources/assets/nerospace/items/quarry_controller.json new file mode 100644 index 0000000..306b27c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/quarry_controller.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/quarry_controller" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/quarry_landmark.json b/multiloader/common/src/main/resources/assets/nerospace/items/quarry_landmark.json new file mode 100644 index 0000000..e72831d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/quarry_landmark.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/quarry_landmark" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 8e4e0c8..3f76993 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -29,6 +29,9 @@ "block.nerospace.nerosteel_ore": "Nerosteel Ore", "block.nerospace.oxygen_generator": "Oxygen Generator", "block.nerospace.passive_generator": "Passive Generator", + "block.nerospace.quarry_controller": "Quarry Controller", + "block.nerospace.quarry_frame": "Quarry Frame", + "block.nerospace.quarry_landmark": "Quarry Landmark", "block.nerospace.raw_nerosium_block": "Block of Raw Nerosium", "block.nerospace.rocket_fuel": "Rocket Fuel", "block.nerospace.rocket_launch_pad": "Rocket Launch Pad", @@ -51,6 +54,7 @@ "container.nerospace.item_store": "Item Store", "container.nerospace.nerosium_grinder": "Nerosium Grinder", "container.nerospace.passive_generator": "Passive Generator", + "container.nerospace.quarry_controller": "Quarry Controller", "container.nerospace.rocket": "Rocket", "entity.nerospace.alien_villager": "Alien Villager", "entity.nerospace.cinder_stalker": "Cinder Stalker", @@ -68,6 +72,11 @@ "fluid_type.nerospace.rocket_fuel": "Rocket Fuel", "gas.nerospace.empty": "Empty", "gas.nerospace.oxygen": "Oxygen", + "gui.nerospace.quarry.state.building_frame": "Building frame", + "gui.nerospace.quarry.state.done": "Finished", + "gui.nerospace.quarry.state.idle": "Idle — place landmarks", + "gui.nerospace.quarry.state.mining": "Mining", + "gui.nerospace.quarry.state.paused": "Paused", "gui.nerospace.rocket.launch": "Launch", "item.nerospace.alien_core": "Alien Core", "item.nerospace.alien_fragment": "Alien Fragment", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_controller.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_controller.json new file mode 100644 index 0000000..3e82d8e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_controller.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/quarry_controller" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_frame.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_frame.json new file mode 100644 index 0000000..d78bce8 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_frame.json @@ -0,0 +1,393 @@ +{ + "ambientocclusion": false, + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 2, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 14 + ], + "to": [ + 2, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 0 + ], + "to": [ + 14, + 2, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 14 + ], + "to": [ + 14, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 2 + ], + "to": [ + 2, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 2 + ], + "to": [ + 16, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 0 + ], + "to": [ + 14, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 14 + ], + "to": [ + 14, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "particle": "nerospace:block/quarry_frame", + "side": "nerospace:block/quarry_frame" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_landmark.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_landmark.json new file mode 100644 index 0000000..a5281f7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/quarry_landmark.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/quarry_landmark" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_controller.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_controller.png new file mode 100644 index 0000000000000000000000000000000000000000..d7c05b187b164617852f0f7f8f1a1ea63b9ef813 GIT binary patch literal 463 zcmV;=0WkiFP)4z!7=|AkLyHkYl*4E^jt(Lm9gmW!Q|HbdI+P6k0i8<*Pnk1^3?_6c9ZZM(f=s4( z@MMaHa@-(}BOHhDRB#4YN)OXGkarQ1KIwbkM;grLt1VB^2;ky;icSr7y=0;@YFA?N zg05|-UCBgeNR`mF4N@hz%Hn;;ZQ~dM=+ux-&oFtx#K+L%{5jh7}vzL?Bx+sC2>9k z=-T^&$0)>$7f6+$Q-jHigNMgLQM)oS7y&){hqUo6LP)&t?k@<`Cpu#>`qMk`?Bfi3 zbrR?AghHw$0^f6Bnm!MEb<&6U`~^}an7m+dbG>5_@`Ujr1?vI)Ff2-y#Lb5kK!>M{ z{~$03F1+tBdBNMta{T6)5cS{h934z_#;fy-*kvpfLP)x{L8m4*5JFPBl5~0&-40>* zG(SJR@5LDZrR^$A3$p4K8o{%kRf4ChlI|`Hvo)AD=F9?bt-()Wo$zG@YT+4T)1T z_b2@12U}l6adTcTaR34t;(C75K_v*`V)7I_DqUXhsHC};ZUCAFrn_k(#}Rp^7zq!6 zk?^Q=Nu|rqV}QX;7|i_%uIF3NaqS?4iyTLMs)Rlx!{k`x3{9auRv<%0!n3(RhV;X5 z8nzik(^-dwd06QZAzWKGoNmWp5yhx zwL>67(rwaM$TQU$&#bS_r_yC7Z&o@qe)3FN7X4_mFk0)=jTz*b>g+w5&d4)mJ@>T` zE{FYhW8qX57#46{r|}%fu=BM6Yrz3Xx5?G8^hwssj_%iwMQ-IEAKdL(V=5mp00000 LNkvXXu0mjf0E4(~ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_frame.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_frame.png new file mode 100644 index 0000000000000000000000000000000000000000..87be25c34638d90f6ed9515dc34b50689f1e3e65 GIT binary patch literal 441 zcmV;q0Y?6bP)e5yI~bMHOp-Z}gAxZnR?yy0=uH@O^gu=E#H3e?h63-RuGH+3UvsZ{y)8e}%j#k7Ri{=KxRt{)7WPgJ$&v j3@+=NDsTY(vG4H?Zk_}_UM*7m00000NkvXXu0mjf@&3xR literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_gantry.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_gantry.png new file mode 100644 index 0000000000000000000000000000000000000000..f349f944ab2e864d08258dbf1d456f2ff87f4e2a GIT binary patch literal 464 zcmV;>0WbcEP)y1)5#7R`E@qIWBvZhrcy^lnA5$Vp}zXAM6N z8_IMCK=rckC0vG55ddvddnwKul9~23!s}WJr^(!d2~~`#N?#;;JP0f4PKnBGkCsZ*fcO317UZ&(1dk_pYMezg<_~vOchu7X-;R3y_CwKf{ z;H<$}YO7V8F=lfl?998m~*5 z#lBF*ULN#&Ayx(e$3F*#A^BVFNLf?8>__i!wPRmA0`M29XkX21p#ipW}oFV~1 zW^b1kLc*A4xA}k0o;+^fKKRG10B)~q%5?>R>V!y&>cs2Q7J&1si|8I;bOQjVAzK8# ze1M&Uo#SP5&*%nJCq_3Ql9J`c(Sd=DRV3x+Zcm*E%|WRXAO__o9_$G$mni^0ItU|s0%Nptvb>l%u>YeQyhv1H z$U3@V=D^&A_D45-Ik0>-<+@tzAQXxsgk$Sja$s8u`|u@Nk>$EVbz1DeVuWwTmuT$2 zbzm+`$P-u|WByhAC+_QS4LrW|Jcez$bC`2@*x@XY<@ RQ^f!P002ovPDHLkV1m(Aq?Z5y literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/quarry.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/quarry.png new file mode 100644 index 0000000000000000000000000000000000000000..28b22b326657f584d5ff3a27a23fc4e4cb4c4a42 GIT binary patch literal 10877 zcmd6thcjH!`~Qg^geB3UL>Dc3i*6CJdM^Q=R{8pTKfmAn{)OMnojG^z+_`to`#k4)zvg*PvZ1~<^<9>`1Ox=sI?sT{1O$Y) zw}b@bq_=~uUyTa^fgn%^sAd{cbWl9?me!!zPf-EG0z+qBwzDw1D|?&6wX=4@;-OB9Mv_+&?7e;x3fQe^dAU#($bqSy z$P@X!i$cW2-Z5)2BRU`boc$(o;bnX?jRhc|B#p$9K0+QrR!*bHIC*fOXJf0=c^DaK zL!*NK+|!f1@;>;CMs}s|%Wv$9^uF+uFU#h91IoDI^mhw>@Ae{^L?rSQpCE_jM-E@* zoagrm0nA9n&AJ8Oz^hMyC}sL|%<7akV>#&0b&Ao; zKHU!{1nPTnTOv|LdQh@ElH(J>w~<&+YrpSLDAY$vSCE!Gydh#RO7IIg&KhNE{QQv` z&bo~o)i~fQfGNNKUMio0K4!x=J&mpeJ9}9;$=NIrvlC&=UHJPY7ChrYipZpc;wpb$ z&Ixl7VU=nQ`lRy_z9Gyr$zohuI=NWQA9b0g(*9}!M?9v-aL6P{?$sx@+}4RQSvOus z{@q`5ks*yz!BC~X^=>G`m&p&h_;^BxqdJN*&x)&Vxxd5SA12ixwxQHhWkCb3RT$%h z#&I6x#F0?8p}%)HJ1qzvQDZ1pl2Dfvrd=p1rd!fV?S?etW*i(P( zcsE5l-X)JtINgp~Z-ZZLSU)1Nn0Ut4U4-n0xuqH6r(J8#+!Wa?07BYk{12F8mOrsn z&3#pW7-&qmgx%nsN}M9ma>)T-CEai#I~KFlJK=Y{0fyEjPi0Mu=mNJEFk<7r1GP&;`MbyDMpyNhE}j`uQ6;Z=2Sby^-V{!+K}ds(kU z`=E`~IkxC&x*5LqtFrLE&R1TC?QSeTb1@VQiEO56*W*)(XRvwoYY-H|u6OqZ<=eN0 zwkJM5CxyMmW6*hNhkXq<0>J9lFRq^&C9Klf^(%7imj+S~%_<_=3b&Ghr1O;MdGp17 zmfuiTuU8;XQPMT?i3%4yWsCsM9nTv1fXgs%yu z>LpGi6?m7GIe%W0E_I=2S_va2Jk*rVi?{C>ZyJMs7&2qN*hrO5SF0z;RSfljdMFuW z%xM+PgGBEgJm(E<{{V)ZZbbC1PI8y>K)J{i6Nf>GiyKc3_2A+?1MTLB8sgETZSmYz z=`~rUss`LUe;Ik*lSseR+c>SZt1}MsEwU>RVb0Q`%Zc$(v^jH8WXmO+X8%+`w96#? zG|F&c-a%{75apMmiuznRj?n9|@oBSEg2n0n3G!j< z6sEt0Z2Nk*E0xgTN8S05JC5Z8jipmntl^)$3y#kr}6zbb3`t8!Xs*05eY+dgD+bHf#! zs(Z!YN0UbQ11+2inlk(4@}B;~i)Ff4FL%vl2bc{@N`nyZway?e@=NKF{%KF=9094c zwT6vNilS$W&Vhx?z!bwVU2XN70t{#KX6J)lH-oM)3FpOpcx(1>F;4-!GQ~{{tQdM7 zpu)pWhra*iJr~ow;H`TJMJPkw3J{t+HyYHdua%J~KB}ZIeg6`g@&r)#-m{ zP_42ka!kKs6yzOHUi-ZXhU@;69jF$kHeDpp=99!SjZRMrLers#?pRbCFaJCUA1=)w zd04?s!HqRxl(;eADw3k3E7Fn!zuh^{$(ak?4L8xkWfs@_YdV@ecrl?YO__7_hZRBr zcL3WU=9&u#OmFzB$2S-a$rdWE>xkdWeO_LMWE;r%mE=2RY?tXScM1B+nE^$i7=5 z27mG18?zR9GgPjSF+(ZF6L>$%S2OzP8QAyey=qIji;<`My?%Q^y&2YP7$(`WO%df| zy2;NcGP0O>OqipgGw2j`%@`H>Oxw%#p~Ka+QSg&Z`K9)?SV8ia_Ecm%-yc# z&lf5vToRMnL2}cHZJJ|iq?b2?qi8nqyJ#0{+cod)5gA+yK)JQx6@*3aC;tS=1zAWG zLfO>Fa2ZnaV~SEVAopLW@YM56aIQ|x+&dt;T)+niH&A1q$?^#ZgsmdmB;y}0DYJDy zpNs=Sz>^>Vz)1y?yHjC3aS;wqR@K;cAIXV>g2-Te=b5Ed!1s?|#vvJ3!nZY&v!}tT zW4z4jar^$dOyx2TtZ}*|eC>3usRk*Jm)=4G>C`B*s}87;FgkJyOZ>OU$MMX(rCK%C z<#c=IeciGUHj~J&1&J}j2*gL*`vJ0oG|}k=ZH|5T5vU*;i5hD zmvT&qld$e!q@cZlZ~`_jW4q{y#4d0A?M)bwH(GM7LjwztvzkHD0U8OBbp z=Ke#O3;MTeR4M-ZeDdP{N!mx?=~s7YW&ua^$Vg{Td)dA5{pT&j`8<^|IvbxI)Y=}N zQK>S<>P1hHw&6@>4c_E~gA@v4T+lP_Ds7tDD;icIbAfZDCG~$DAk@!~0T-2TV05Jl zl1Ek7i4a~p*SJ-dfXW(b)=?D*Z=6pP-wXw% z&%Q89)LthxZRBOBPB5j%_^Xk9@MT@zousCg!jSENPqC9#09D@^oK91Rzz4Z)U~V$*JMz0KgE_KJkx~WlCCc6 zSMVVDO~AHtGzj#;4M^&`Ka|4JhrhmRjQn@d=^IC>#`Y8tVKSO^eiGfHBRN9{)f%-7 zdMn=mv+R^=W+jkQI5K$OArgJQCqy` zRX+B^-qR&e5d2=6s(pF%gE zk7CX*g?xA~mIJ9?HE_6j_W6sKf8#Z&GV=6C!>c{9LtIs#nYCGzGS-#|a{LM=rNN+G z+Duu`mAMsOH?Q+vyl>7+$TeH|b^A&IC0%;ww&`n-v3ko&XQ!!#PG+sh0q1vpQcXwq zE4#CW_qkW;@r|i&V=+}wLcI5KmRN6*Y}*a4L&UY{^`VV0yTen7QP%wH>&&@~-gt_? zDqz@sQi#@{*G?+SUaIO&{g6Gq6qhm6Qu-w0@VjI)_nf=3@4m-+*QV@q<4a84q$!Vo ze6g!2=WA*WeYpSoi?@#7`i~l&a!4OyHP?4Le9wPeo?BcG9U+mRgMrcoeq*X_-o}|{ zC!I;^oXYU&C`zQOJs>G&*aCzSSSba2+i07Tq$mA*a&vOX&N_um#hI8fmcRw0#tZxu zZ?2=RvD^GJu66Q6(2&-eJ-R$oRPl}Ln$(AHujJcDtU5QRmcB4Lar~x>89vIU`CZ-N zt#78ZaG*#<$1=gfd!5Aoy28{9DUfW#Rw5NBb!U38V35_cYsK%Buc{f14w3x{==`M; zu)H{Oe5q=pUF@3k51@p!%cxUHP%a(ipvA@MJHW35*XN4d|d#*vsol%MPB(Oqs7rEWusi_!XQl z$;61D=}SThf9iO1J5rBeI*An~%%w*AOWaf2lh@aCk;H8$xNN6s+Dg57CnG)r-Zzyu zI3Qhd!cTDA9FYod)REj31>kJ>Gmktcy^$mte-yC_%J=hwwgNw))GIKH>M(T6U6I2u zn_Eruujyu9z%aFH9BGY?K}x-T#`Pn)9HZ7XuitwF62!&!cXH6M1*~{Y>?6Qcj3s4A zWa(-HvE~SzLJrDS>xP0xVkCrziN?9_}TrG4mXyN1-63>^Y}!Y5Kq?HcONQ=Gf~DmnUZcu?`-U&(+a}BAObha865Awm3Xv9Qe8&M^M66<7luF z_gD$F((4h|TM#=+9-{Xg|G=b7{*P!>Hs%R_G)S<=(vR_2vM@(F+G+Z3teEdmNQq(j z<2&~!M3Z%tE&#=HpSgV;i2HI$atLdu)K@-{`cT0ik;{e@5GopMS3T4< zqAr`Mm64A^;bFvz2Q2fKF?kT-uliCu0*`@;$wx;XO3Vh=T=Br`)YxF`R+bK25I zrVCSGeUJTX_GIXSs=6b3?37M1GETKWq|&VPs4#P-OavV-wSM?xd(6ChueWSEaD~e= z^_>)SP~E`XkEUlU2+n6)Rikw-oTzYUD1nG%IhMNBk;737oMqhRu6^Rwomx>-B8E(OAgz&_VU&SbN+iJ{O?ZMRJJTYrD;lnx!-c z0+N%Iw?6-ul?J|Z>8aC!QoO@M|1Nq*`{!hT-ix<6PIOILwC%>J=Xi5?^O02RTUCJj zL=fgY_VfZMmos|)P{W&rp~N&;f-!-Gxn0OD-O%A7)-x(tb*1sqb?V6YZRMe>yp%3( z_3jN57NJ3+bbMB2PO~nr)T?><_@I`J$2fYMv|NOrJ(w!1a9i$^r?=(Pdm=Q^_t^ax zu_<3C*X*}ULPUpxDPB-MhlmOOo$f9bnZhlJt~iBw!woCmk-FC)OLb$#BR!~phQ^-C z`R{gAgwU;jdcX*@X%1f6_z+4kY%WvVP}}ZqC>8hju}8c>k8+0E#INC{G;AWnvQxV9 zuYH?j)qo#vvl$8d*5+ufH)>Jur6{hH4>+fv0n?VO`@LF$rFm)25BIEM&o6BBJCYJ% zXRYF(780}9b)zt+!gf*{^nt;p@<b z?tDBUuJRn-H<2V6VTIDF3m6Thcm7M3bmqAwkUGQ$@c(QUYsH_h_^jE zKpevKWhAMK8pm>?l(_?jei}R>P^PxK{I_~`c(ZbIv6>b5V8%>tVr=Vd#Ogzz9;D`V zK%q@ZAFiX_4M?ZXzHDFaH0@i1d+s#v5v5yHxa9^FLdUsa-Hyky44y0(PA=svs7x%G zqx&MUN~vz$zqpr;F@d%wHYOK;Hu?+^M|*W=NAFdx*EN|8w9JU#Cb^vTMBu7BL(g#8 zEIr8wd;eAf&>DSvA?z$`i3s#XsBF~DG}Cbha(ph}N@EQv69CF6Y@uoGS;AHp*YCk3 z)-brW5JY3|MT?nJ$Q4RE9lfZY%pADLgeWX!PD?jti!)~kHzuUU2piH*E@pJCMmK#f zA^{fxzUVEh7hmmAprQh9lN~F$rm=OLk+-Ar! z-B?t+xxWSG&9`44+!~gNRUISuS24OwxZrjccG)n};6^eI$T~BkuzwbV@V%v%3%J0L+{13?E6dwd za=t1yNovm6F7Lzo8$?h`woM^3Nt$r&(ZHU#eDC97uxzSi8Jv*Xn_H$N-JCAfJM&hm z^)kk{EC$-S#Ls{4@=#(v+Se@Ky6?|kVL9|7S0e%HN54w`)Y~z;4x>)S;8d?z*yXCdWQNP{@KAZ`5_Wtoz^oz}8;)vNrrBkR@eC zSe07W#(x(FKOsx|P|MMjV=Gr%ma^|3G0hFto+gpH!ZGe7nZYQ9@nr_7U&X@kU0`n#LH;>^j+zpiuHLufgyk+PA6OGWxovqU>x$8LlgEhs*)s zN6rabRWmag3Nqpua#aRKUKxt+x3#fW$7mhf{pXA_jIyvH;5;j)HMDLQn_`N+!Zz

7W;)EYq z%}?h|@WNk65J3oR+SwVUCz4soc%9J}ZDCf6dMAuJV`1aInUDDVW3LR2v;HhgtE#%|Ca&q>px#Qm2(a%zdqOT2u&SEupBFt=xmyuDB;TkxSJ!%xwDDO zK*pdtbj?;~DqOr}tUKS75m5`&Owcm{HWq_ui&I!NCY>f~pSekpJdg+%GPAP*Q8|q( z``aQS=femEC~EVx|0a&a7qWKsSg4vt=z?;;ncT^OBYA@8j<^oKK3=#a{ABpRHYDda z{HTT~J5`nKJtU!~O8rj5Ijyuc@>`f~Q4kRT4y#;R_lrpPot=dS@t#Sig?u&~d6Y)yLvW3bQRM09uU@cYJROf8I75RUli}W=?=>1!3^e1`jLag z>tbC;X(2iN+=9POuU=C0H0bC(Q{`1{!&QfkhHdh%l)sF@Rlj*>s6Oo~o|as|3d^=$ zm0~|H260b$-gwIio>&=V*&e=R%aA5(?6Yo=W>0)A%~Y0^Llc+&*l#^Y6jEI~T1^u$ zOLwgBI%X%t6kC*DO%6bZ>k2_+Sc%L)5Ej!zlu0Ah$T4d-XOLz!?DWMf4N#LpuWDwC zlebs#NxGUI!^yGti(42Wov0nrU*icFC*xM1zqcZ9A9S0%^NAnwnKLH&xwM@>o97YJ zHPCe@;Z3`^H=j`&R0$EL=hLYws5r~3|4|`dz=0eI^PJ6M50QJeq=jqMXfZ^h9r<1H zKBj^i27j#H`OYdYxbp?tPKZdzUl=03B^FSmE>G`#ZkRaRBa&i|FZX%T`Q&9!`pr%C z&Djj~)!);Jkjp*q-n6wF++h%n>54;?O@vBSF|ze&ZbkO}*-7)kMySbC17?$UT!H&Z z(M~VBWP+Pw23s8a&Ht^{vhp@lG(9|+P&JLo9=bVydAZ5S_L;2%XIhiclB?mM$nN=6 z4CQreG0NZPC6(C^Q1)&swF^sPy*%?Xkew)2L3*)r{-cnrv=&uv-S37O8ifRcC-siZ zu4*g%e^l>}geVF=kaL_^kx6YCbm(_7G>Q(I{`?)0T&iCCy}B)W6?{S%j9F?@8N3Z3s+!P03BC+IWSCZpu@D6)P3h)tSXZSlR}FjRGsESyy1fwuGe~9ML+v4_WI;2xH~XGr}l6L&wXtn z@{o?rci=YBepnRDkgx- z9Yn36G~p8#I6W#|V}-gvQ|(0{N{&7t>TU9koGH6%m=cqEtZN+=CMYuW0Nf1y!SrhX zmMyr~C5c0-M#a3a`AhePqCL}kuOSvIh>o~TAn zeR`hYOA6*R#MZJVZmvexLPBHHS{_N7J}2@SfDRc_MX!j6T%}2>NfHFQHys7!{BGRI zMc?x%BC)0$ZzO$s?*!8HhX1>qBx{)hgGlWcO(Mk+gih(TK;s0aDTnyENx!(otzPEv z!v?9-vL5DdoBgShzjk-O^gLKJipKan@SQR%_H`}-e8y)45_*ub#?2CVd81Emg|x@; zJiEoE$QSUfGIX`E5smb|{+Gyz*Rc4 z*OsuYnC@7POCk%XNUfmsN2n`u2Z5RIPal0VtmFxwgG{g0lu@B7S~GT{CfSlBW`OdH zXyO35+kc9@4y6?0RpbQ$g@0V#9webOYD?rNo_b;D*^y5>VA>{!M7IB(&>CGdt*qbk z`N+dG#rFFtR*OOuzuwK~|IB4ryd{GD1F6xJh8oYo{T9FFM$iYRS%Ej<`k89rnbo302#sOi`7dh?cIn#=i5QS}J@_(9&dy?@q|# zcci)qorCwjktMN82ROBIlBQ{tBVIo#;#gY zJ>IguxiyMf_|TW)mo889{=x2f-tLuZxC_%u78Bk_PYE;BqLk$3h5TP&JQn?UZgES9 zLd(0OrygOYx|UN%$adQSXbM@cCuHKk^MOdz(w}vAvlQTeoahklW+3oL=scH<`FP9wcxkSpYQOut2mN^$2&(~V;14VCK62HopX{wZ6Cz(r zh!ztVDqw9iz5HuOtkhQmTzt22%E|DAAi(k9mH6bHJnj$j_WRnBg!`7_ljQYOuc%Mt z=2v1m1ka1=6+W(d}$8mf6U_ z>yW6|J(IboL!X^FXv`S47Tb%atj#Quf6f{kUOWilUkk-y@Pq-4tr3jo($StB+~p^gzu5DP9>g8 z+=@z5rnu=*?duP>2oWq{!61TCVp@`AR%h7o_zE;)cAX8fY?e>S7j&BQr;uPs$8kmIwKY7LX3SpnRYWUJ1sx^4HU^>^x34M^25P;{6pF9kBn76-4& zdM4!-w^^)HVLJD)`GX1_eOZdwp++5|=BfyJjGh-Z+$jb*3IQr>Ta0gDxPgd+c_s$4 z%c{PoY817(<5fEWSD#e!Uk$U4#f!Vz$dmRgWSxpt`-Q(gWX1A~$kQxUL3k;N^d#wV zaJ47Usi1L|@{u4Tx0;PTly|H9@ZhP5V+JVln@EMs#9H4C^V zbh=Yki{PJ%s{PAw*|67#p=;SKlK5(;z`7F5edYIvQtM~wxo7eZHj4^!BoZ!7Ho{RX ztXtatrtMPPn4{aiana@1Wa1GcWWtRZWx@O?*T;ctGg9NevQ?4t0@jR)88Szs+1IQP7J!xlZV7bS|-!8S4ZZq0K4n&zS)nse-`rx{nd;=f=u z9UH}$yHr4wn0?Yzki8C7%o*A?yxG>3>?idgHBo!IjXZ5TrOj5{-(dLAz7YN=)A8$* z>pi&>IlZ*6IRl*mAov&WwmN++UBuU%&$$+P$>*PTx^tSC3kNnjxlPtDh0D6te$5tk zIM>bD&etTLnyDv6(n?n<({f6%XXFC_Q8&8^Txf0E7s|;p>;EEj2EXl9lLd&t6mO1u zmWP^mVQVMWZvL$AQzMx94__XszHID!E93pV%a2C7;cO_PhXYZMnGyGA^?Tx}d<2qD zIK2q~l^CU&;b->RFIczK@O{v&f-`rf1c(1WF11Vk6>LXSv;4@{+he5!I-2^x26g+` F{{hGmLJj}` literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_controller.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_controller.json new file mode 100644 index 0000000..7c0e673 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_controller.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:quarry_controller" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/quarry_controller" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_landmark.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_landmark.json new file mode 100644 index 0000000..6d00bf2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/quarry_landmark.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:quarry_landmark" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/quarry_landmark" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/quarry_controller.json b/multiloader/common/src/main/resources/data/nerospace/recipe/quarry_controller.json new file mode 100644 index 0000000..b1db75d --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/quarry_controller.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "D": "#c:gems/diamond", + "F": "nerospace:frame_casing", + "I": "#c:ingots/nerosteel", + "R": "minecraft:redstone_block" + }, + "pattern": [ + "IDI", + "FRF", + "III" + ], + "result": { + "id": "nerospace:quarry_controller" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/quarry_landmark.json b/multiloader/common/src/main/resources/data/nerospace/recipe/quarry_landmark.json new file mode 100644 index 0000000..60db2fc --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/quarry_landmark.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "G": "#c:glass_blocks", + "I": "#c:ingots/nerosteel", + "R": "minecraft:redstone" + }, + "pattern": [ + "R", + "G", + "I" + ], + "result": { + "count": 3, + "id": "nerospace:quarry_landmark" + } +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index aa61650..39c325b 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -152,6 +152,17 @@ public void onInitialize() { ItemStorage.SIDED.registerForBlockEntity( (be, direction) -> ContainerStorage.of(be, direction), ModBlockEntities.FUEL_REFINERY.get()); + + // Quarry controller: grid power in, mined output + sucked fluid out, frame casings in. + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.QUARRY_CONTROLLER.get()); + FLUID.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.QUARRY_CONTROLLER.get()); + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.QUARRY_CONTROLLER.get()); } private static void addOverworldOre(String placedFeatureName) { diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index dd35183..d2f68e1 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -14,6 +14,7 @@ import za.co.neroland.nerospace.client.FuelRefineryScreen; import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; @@ -29,6 +30,7 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); MenuScreens.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); MenuScreens.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); + MenuScreens.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { @Override diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 6e7b739..73fc8eb 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -179,5 +179,21 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { (be, side) -> side != null ? new WorldlyContainerWrapper(be, side) : VanillaContainerWrapper.of(be)); + + // Quarry controller: grid power in, mined output + sucked fluid out, frame casings in. + event.registerBlockEntity( + ENERGY, + ModBlockEntities.QUARRY_CONTROLLER.get(), + (be, side) -> be.getEnergy()); + event.registerBlockEntity( + FLUID, + ModBlockEntities.QUARRY_CONTROLLER.get(), + (be, side) -> be.getTank()); + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.QUARRY_CONTROLLER.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 7582395..aa5a387 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -19,6 +19,7 @@ import za.co.neroland.nerospace.client.FuelRefineryScreen; import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.fluid.ModFluids; import za.co.neroland.nerospace.registry.ModMenuTypes; @@ -51,6 +52,7 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); event.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); event.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); + event.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); } /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ From 5ea6deac8c2c599a444176f45ba682e0d648f5c9 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:22:15 +0200 Subject: [PATCH 38/82] Add travel compasses, navigator and spawn eggs Introduce creative travel items and lazy spawn eggs: add DestinationCompassItem (teleports to specific Nerospace destinations, creates a platform on the station), GreenxertzNavigatorItem (toggles between overworld and Greenxertz), and NerospaceSpawnEggItem (lazy EntityType supplier to avoid early binding). Register navigator, four compasses and nine spawn eggs in ModItems (including a spawnEgg helper), add item assets/models/textures and language entries, and update the MULTILOADER port checklist. Assets and creative tab placements for tools/utilities and spawn eggs were also added. --- docs/MULTILOADER_PORT_CHECKLIST.md | 11 ++- .../item/DestinationCompassItem.java | 76 ++++++++++++++++++ .../item/GreenxertzNavigatorItem.java | 59 ++++++++++++++ .../nerospace/item/NerospaceSpawnEggItem.java | 53 ++++++++++++ .../neroland/nerospace/registry/ModItems.java | 41 +++++++++- .../items/alien_villager_spawn_egg.json | 6 ++ .../nerospace/items/cindara_compass.json | 6 ++ .../items/cinder_stalker_spawn_egg.json | 6 ++ .../items/ember_strutter_spawn_egg.json | 6 ++ .../items/frost_strider_spawn_egg.json | 6 ++ .../nerospace/items/glacira_compass.json | 6 ++ .../nerospace/items/greenling_spawn_egg.json | 6 ++ .../nerospace/items/greenxertz_compass.json | 6 ++ .../nerospace/items/greenxertz_navigator.json | 6 ++ .../items/meadow_loper_spawn_egg.json | 6 ++ .../items/quartz_crawler_spawn_egg.json | 6 ++ .../nerospace/items/station_compass.json | 6 ++ .../items/woolly_drift_spawn_egg.json | 6 ++ .../items/xertz_stalker_spawn_egg.json | 6 ++ .../assets/nerospace/lang/en_us.json | 19 ++++- .../models/item/alien_villager_spawn_egg.json | 6 ++ .../models/item/cindara_compass.json | 6 ++ .../models/item/cinder_stalker_spawn_egg.json | 6 ++ .../models/item/ember_strutter_spawn_egg.json | 6 ++ .../models/item/frost_strider_spawn_egg.json | 6 ++ .../models/item/glacira_compass.json | 6 ++ .../models/item/greenling_spawn_egg.json | 6 ++ .../models/item/greenxertz_compass.json | 6 ++ .../models/item/greenxertz_navigator.json | 6 ++ .../models/item/meadow_loper_spawn_egg.json | 6 ++ .../models/item/quartz_crawler_spawn_egg.json | 6 ++ .../models/item/station_compass.json | 6 ++ .../models/item/woolly_drift_spawn_egg.json | 6 ++ .../models/item/xertz_stalker_spawn_egg.json | 6 ++ .../item/alien_villager_spawn_egg.png | Bin 0 -> 270 bytes .../textures/item/cindara_compass.png | Bin 0 -> 221 bytes .../item/cinder_stalker_spawn_egg.png | Bin 0 -> 256 bytes .../item/ember_strutter_spawn_egg.png | Bin 0 -> 233 bytes .../textures/item/frost_strider_spawn_egg.png | Bin 0 -> 257 bytes .../textures/item/glacira_compass.png | Bin 0 -> 236 bytes .../textures/item/greenling_spawn_egg.png | Bin 0 -> 226 bytes .../textures/item/greenxertz_compass.png | Bin 0 -> 220 bytes .../textures/item/greenxertz_navigator.png | Bin 0 -> 249 bytes .../textures/item/meadow_loper_spawn_egg.png | Bin 0 -> 256 bytes .../item/quartz_crawler_spawn_egg.png | Bin 0 -> 241 bytes .../textures/item/station_compass.png | Bin 0 -> 221 bytes .../textures/item/woolly_drift_spawn_egg.png | Bin 0 -> 206 bytes .../textures/item/xertz_stalker_spawn_egg.png | Bin 0 -> 228 bytes 48 files changed, 422 insertions(+), 5 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/DestinationCompassItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/GreenxertzNavigatorItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/NerospaceSpawnEggItem.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/alien_villager_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/cindara_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/cinder_stalker_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/ember_strutter_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/frost_strider_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/glacira_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/greenling_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_navigator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/meadow_loper_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/quartz_crawler_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/station_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/woolly_drift_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/xertz_stalker_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/alien_villager_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/cindara_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/cinder_stalker_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/ember_strutter_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/frost_strider_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/glacira_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/greenling_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_navigator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/meadow_loper_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/quartz_crawler_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/station_compass.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/woolly_drift_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_stalker_spawn_egg.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_villager_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/cindara_compass.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/cinder_stalker_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/ember_strutter_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/frost_strider_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/glacira_compass.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/greenling_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_compass.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_navigator.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/meadow_loper_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/quartz_crawler_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/station_compass.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/woolly_drift_spawn_egg.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_stalker_spawn_egg.png diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 9338adc..30fb965 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -136,9 +136,14 @@ checked by a headless build). - [ ] `CreativeItemStore`, `CreativeFluidTank`, `CreativeGasTank` (+ `AbstractStorageBlock`) — infinite configurable sources. Marginal (creative-only). -### Utility items (`item/`) -- [ ] `ConfiguratorItem`, `DestinationCompassItem`, `GreenxertzNavigatorItem`, `PipeFilterItem`, - `PipeUpgradeItem`, `StarGuideBookItem`, `NerospaceSpawnEggItem` (+ **spawn eggs** for all mobs). +### Utility items (`item/`) — **partly DONE (4 cells green)** +- [x] `NerospaceSpawnEggItem` (+ **9 spawn eggs**: xertz stalker, quartz crawler, greenling, alien + villager, cinder stalker, frost strider, meadow loper, ember strutter, woolly drift — ruin warden is + summon-only). Lazy `EntityType` supplier (vanilla `SpawnEggItem` binds too early); SPAWN_EGGS tab. +- [x] `DestinationCompassItem` (×4: station/greenxertz/cindara/glacira) + `GreenxertzNavigatorItem` — + creative-only travel devices; TOOLS_AND_UTILITIES tab. Assets + 17 lang keys copied. +- [ ] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` (depend on **advanced pipes**), + `StarGuideBookItem` (depends on **star guide**). - [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. ### Cross-cutting registries (`registry/`) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/DestinationCompassItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/DestinationCompassItem.java new file mode 100644 index 0000000..53e111e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/DestinationCompassItem.java @@ -0,0 +1,76 @@ +package za.co.neroland.nerospace.item; + +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; + +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.rocket.Destinations; + +/** + * A creative-only travel device that teleports the holder straight to one Nerospace destination (a + * planet or the station). Intended for testing/creative building — no survival recipe. Server-authoritative. + */ +public class DestinationCompassItem extends Item { + + private final ResourceKey destination; + + public DestinationCompassItem(Properties properties, ResourceKey destination) { + super(properties); + this.destination = destination; + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player instanceof ServerPlayer serverPlayer) { + teleport(serverPlayer); + } + return InteractionResult.SUCCESS; + } + + private void teleport(ServerPlayer player) { + ServerLevel current = player.level(); + ServerLevel dest = current.getServer().getLevel(this.destination); + if (dest == null) { + return; + } + + double x = player.getX(); + double z = player.getZ(); + int blockX = Mth.floor(x); + int blockZ = Mth.floor(z); + dest.getChunk(blockX >> 4, blockZ >> 4); + + double y; + if (this.destination.equals(ModDimensions.STATION_LEVEL)) { + // The station is void; drop a small platform so the player doesn't fall. + int platformY = 64; + BlockState floor = ModBlocks.STATION_FLOOR.get().defaultBlockState(); + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + dest.setBlockAndUpdate(new BlockPos(blockX + dx, platformY, blockZ + dz), floor); + } + } + y = platformY + 1.0D; + } else { + y = dest.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, blockX, blockZ) + 1.0D; + } + + player.teleportTo(dest, x, y, z, Set.of(), player.getYRot(), player.getXRot(), true); + player.sendSystemMessage(Component.translatable( + "item.nerospace.destination_compass.travel", Destinations.name(this.destination))); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/GreenxertzNavigatorItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/GreenxertzNavigatorItem.java new file mode 100644 index 0000000..6284283 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/GreenxertzNavigatorItem.java @@ -0,0 +1,59 @@ +package za.co.neroland.nerospace.item; + +import java.util.Set; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.Heightmap; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** + * Creative travel device: right-click toggles the holder between the overworld and Greenxertz. All + * teleport logic runs server-side so it behaves correctly on a dedicated server. + */ +public class GreenxertzNavigatorItem extends Item { + + public GreenxertzNavigatorItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player instanceof ServerPlayer serverPlayer) { + teleport(serverPlayer); + } + return InteractionResult.SUCCESS; + } + + private void teleport(ServerPlayer player) { + ServerLevel current = player.level(); + MinecraftServer server = current.getServer(); + + boolean onPlanet = current.dimension().equals(ModDimensions.GREENXERTZ_LEVEL); + ServerLevel destination = server.getLevel(onPlanet ? Level.OVERWORLD : ModDimensions.GREENXERTZ_LEVEL); + if (destination == null) { + return; + } + + double x = player.getX(); + double z = player.getZ(); + int blockX = Mth.floor(x); + int blockZ = Mth.floor(z); + destination.getChunk(blockX >> 4, blockZ >> 4); + int y = destination.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, blockX, blockZ); + + player.teleportTo(destination, x, y + 1.0D, z, Set.of(), player.getYRot(), player.getXRot(), true); + player.sendSystemMessage(Component.translatable(onPlanet + ? "item.nerospace.greenxertz_navigator.return" + : "item.nerospace.greenxertz_navigator.travel")); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/NerospaceSpawnEggItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/NerospaceSpawnEggItem.java new file mode 100644 index 0000000..49d3a55 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/NerospaceSpawnEggItem.java @@ -0,0 +1,53 @@ +package za.co.neroland.nerospace.item; + +import java.util.function.Supplier; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; + +/** + * A spawn egg for a Nerospace creature. 26.1 dropped NeoForge's {@code DeferredSpawnEggItem} and vanilla + * {@code SpawnEggItem} binds its entity type too early (items register before entity types). So this + * resolves the {@link EntityType} lazily via a {@link Supplier} at use time and spawns the mob + * on right-click — mirroring vanilla spawn-egg behaviour. Its icon is a flat egg texture (no procedural + * tinting needed), which also keeps it loader-agnostic. + */ +public class NerospaceSpawnEggItem extends Item { + + private final Supplier> type; + + public NerospaceSpawnEggItem(Properties properties, Supplier> type) { + super(properties); + this.type = type; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + if (!(context.getLevel() instanceof ServerLevel level)) { + return InteractionResult.SUCCESS; + } + + ItemStack stack = context.getItemInHand(); + BlockPos clicked = context.getClickedPos(); + Direction face = context.getClickedFace(); + BlockPos spawnPos = level.getBlockState(clicked).getCollisionShape(level, clicked).isEmpty() + ? clicked : clicked.relative(face); + Player player = context.getPlayer(); + + Mob mob = this.type.get().spawn(level, stack, player, spawnPos, EntitySpawnReason.SPAWN_ITEM_USE, + true, !clicked.equals(spawnPos) && face == Direction.UP); + if (mob != null && (player == null || !player.getAbilities().instabuild)) { + stack.shrink(1); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index dee19df..4d628fb 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -16,6 +16,8 @@ import net.minecraft.world.item.CreativeModeTabs; import net.minecraft.world.item.Item; import net.minecraft.world.item.ToolMaterial; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; import net.minecraft.world.level.material.Fluid; import net.minecraft.world.item.equipment.ArmorMaterial; import net.minecraft.world.item.equipment.ArmorType; @@ -26,6 +28,9 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.item.DestinationCompassItem; +import za.co.neroland.nerospace.item.GreenxertzNavigatorItem; +import za.co.neroland.nerospace.item.NerospaceSpawnEggItem; import za.co.neroland.nerospace.rocket.RocketItem; import za.co.neroland.nerospace.rocket.RocketTier; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -113,6 +118,29 @@ public final class ModItems { public static final RegistryEntry ROCKET_TIER_4 = ITEMS.register("rocket_tier_4", key -> new RocketItem(new Item.Properties().stacksTo(1).setId(key), RocketTier.TIER_4)); + // --- Creative travel devices -------------------------------------------- + public static final RegistryEntry GREENXERTZ_NAVIGATOR = ITEMS.register("greenxertz_navigator", + key -> new GreenxertzNavigatorItem(new Item.Properties().stacksTo(1).setId(key))); + public static final RegistryEntry STATION_COMPASS = ITEMS.register("station_compass", + key -> new DestinationCompassItem(new Item.Properties().stacksTo(1).setId(key), ModDimensions.STATION_LEVEL)); + public static final RegistryEntry GREENXERTZ_COMPASS = ITEMS.register("greenxertz_compass", + key -> new DestinationCompassItem(new Item.Properties().stacksTo(1).setId(key), ModDimensions.GREENXERTZ_LEVEL)); + public static final RegistryEntry CINDARA_COMPASS = ITEMS.register("cindara_compass", + key -> new DestinationCompassItem(new Item.Properties().stacksTo(1).setId(key), ModDimensions.CINDARA_LEVEL)); + public static final RegistryEntry GLACIRA_COMPASS = ITEMS.register("glacira_compass", + key -> new DestinationCompassItem(new Item.Properties().stacksTo(1).setId(key), ModDimensions.GLACIRA_LEVEL)); + + // --- Spawn eggs (lazy entity-type supplier; ruin warden is summon-only) ---- + public static final RegistryEntry XERTZ_STALKER_SPAWN_EGG = spawnEgg("xertz_stalker_spawn_egg", ModEntities.XERTZ_STALKER); + public static final RegistryEntry QUARTZ_CRAWLER_SPAWN_EGG = spawnEgg("quartz_crawler_spawn_egg", ModEntities.QUARTZ_CRAWLER); + public static final RegistryEntry GREENLING_SPAWN_EGG = spawnEgg("greenling_spawn_egg", ModEntities.GREENLING); + public static final RegistryEntry ALIEN_VILLAGER_SPAWN_EGG = spawnEgg("alien_villager_spawn_egg", ModEntities.ALIEN_VILLAGER); + public static final RegistryEntry CINDER_STALKER_SPAWN_EGG = spawnEgg("cinder_stalker_spawn_egg", ModEntities.CINDER_STALKER); + public static final RegistryEntry FROST_STRIDER_SPAWN_EGG = spawnEgg("frost_strider_spawn_egg", ModEntities.FROST_STRIDER); + public static final RegistryEntry MEADOW_LOPER_SPAWN_EGG = spawnEgg("meadow_loper_spawn_egg", ModEntities.MEADOW_LOPER); + public static final RegistryEntry EMBER_STRUTTER_SPAWN_EGG = spawnEgg("ember_strutter_spawn_egg", ModEntities.EMBER_STRUTTER); + public static final RegistryEntry WOOLLY_DRIFT_SPAWN_EGG = spawnEgg("woolly_drift_spawn_egg", ModEntities.WOOLLY_DRIFT); + // --- Tool + armor materials -------------------------------------------- public static final ToolMaterial NEROSIUM_TOOL_MATERIAL = new ToolMaterial( BlockTags.INCORRECT_FOR_IRON_TOOL, 350, 7.0F, 2.5F, 15, cTag("ingots/nerosium")); @@ -174,6 +202,10 @@ private static RegistryEntry blockItem(String name, RegistryEntry new BlockItem(block.get(), new Item.Properties().setId(key))); } + private static RegistryEntry spawnEgg(String name, RegistryEntry> type) { + return ITEMS.register(name, key -> new NerospaceSpawnEggItem(new Item.Properties().setId(key), type)); + } + private static TagKey cTag(String path) { return TagKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath("c", path)); } @@ -203,7 +235,14 @@ public static Map, List> creativeTabItems ROCKET_FUEL_CANISTER.get(), FRAME_CASING.get(), GRAV_STRIDERS.get(), DRIFT_FLEECE.get()), CreativeModeTabs.TOOLS_AND_UTILITIES, List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get(), - ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get()), + ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get(), + GREENXERTZ_NAVIGATOR.get(), STATION_COMPASS.get(), GREENXERTZ_COMPASS.get(), + CINDARA_COMPASS.get(), GLACIRA_COMPASS.get()), + CreativeModeTabs.SPAWN_EGGS, + List.of(XERTZ_STALKER_SPAWN_EGG.get(), QUARTZ_CRAWLER_SPAWN_EGG.get(), + GREENLING_SPAWN_EGG.get(), ALIEN_VILLAGER_SPAWN_EGG.get(), CINDER_STALKER_SPAWN_EGG.get(), + FROST_STRIDER_SPAWN_EGG.get(), MEADOW_LOPER_SPAWN_EGG.get(), EMBER_STRUTTER_SPAWN_EGG.get(), + WOOLLY_DRIFT_SPAWN_EGG.get()), CreativeModeTabs.COMBAT, List.of( OXYGEN_SUIT_HELMET.get(), OXYGEN_SUIT_CHESTPLATE.get(), OXYGEN_SUIT_LEGGINGS.get(), OXYGEN_SUIT_BOOTS.get(), diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/alien_villager_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/alien_villager_spawn_egg.json new file mode 100644 index 0000000..9091e4a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/alien_villager_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/alien_villager_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/cindara_compass.json b/multiloader/common/src/main/resources/assets/nerospace/items/cindara_compass.json new file mode 100644 index 0000000..b71ab4f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/cindara_compass.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/cindara_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/cinder_stalker_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/cinder_stalker_spawn_egg.json new file mode 100644 index 0000000..f26512f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/cinder_stalker_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/cinder_stalker_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/ember_strutter_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/ember_strutter_spawn_egg.json new file mode 100644 index 0000000..5fe14ca --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/ember_strutter_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/ember_strutter_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/frost_strider_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/frost_strider_spawn_egg.json new file mode 100644 index 0000000..cbabda0 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/frost_strider_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/frost_strider_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/glacira_compass.json b/multiloader/common/src/main/resources/assets/nerospace/items/glacira_compass.json new file mode 100644 index 0000000..b53c2b1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/glacira_compass.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/glacira_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/greenling_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/greenling_spawn_egg.json new file mode 100644 index 0000000..ccedaa1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/greenling_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/greenling_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_compass.json b/multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_compass.json new file mode 100644 index 0000000..e8525c1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_compass.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/greenxertz_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_navigator.json b/multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_navigator.json new file mode 100644 index 0000000..385cd6c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/greenxertz_navigator.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/greenxertz_navigator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/meadow_loper_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/meadow_loper_spawn_egg.json new file mode 100644 index 0000000..b9e560d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/meadow_loper_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/meadow_loper_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/quartz_crawler_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/quartz_crawler_spawn_egg.json new file mode 100644 index 0000000..252b6f6 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/quartz_crawler_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/quartz_crawler_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/station_compass.json b/multiloader/common/src/main/resources/assets/nerospace/items/station_compass.json new file mode 100644 index 0000000..149084a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/station_compass.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/station_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/woolly_drift_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/woolly_drift_spawn_egg.json new file mode 100644 index 0000000..69fdfec --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/woolly_drift_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/woolly_drift_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/xertz_stalker_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_stalker_spawn_egg.json new file mode 100644 index 0000000..25cd28c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/xertz_stalker_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/xertz_stalker_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 3f76993..afc1f37 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -81,11 +81,24 @@ "item.nerospace.alien_core": "Alien Core", "item.nerospace.alien_fragment": "Alien Fragment", "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", + "item.nerospace.alien_villager_spawn_egg": "Alien Villager Spawn Egg", + "item.nerospace.cindara_compass": "Cindara Compass", + "item.nerospace.cinder_stalker_spawn_egg": "Cinder Stalker Spawn Egg", "item.nerospace.cindrite": "Cindrite", + "item.nerospace.destination_compass.travel": "Travelling to %s", "item.nerospace.drift_fleece": "Drift Fleece", + "item.nerospace.ember_strutter_spawn_egg": "Ember Strutter Spawn Egg", "item.nerospace.frame_casing": "Frame Casing", + "item.nerospace.frost_strider_spawn_egg": "Frost Strider Spawn Egg", + "item.nerospace.glacira_compass": "Glacira Compass", "item.nerospace.glacite": "Glacite", "item.nerospace.grav_striders": "Grav Striders", + "item.nerospace.greenling_spawn_egg": "Greenling Spawn Egg", + "item.nerospace.greenxertz_compass": "Greenxertz Compass", + "item.nerospace.greenxertz_navigator": "Greenxertz Navigator", + "item.nerospace.greenxertz_navigator.return": "Returned to the overworld", + "item.nerospace.greenxertz_navigator.travel": "Transported to Greenxertz", + "item.nerospace.meadow_loper_spawn_egg": "Meadow Loper Spawn Egg", "item.nerospace.nerosium_dust": "Nerosium Dust", "item.nerospace.nerosium_ingot": "Nerosium Ingot", "item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe", @@ -106,6 +119,7 @@ "item.nerospace.oxygen_suit_t2_chestplate": "Tier 2 Oxygen Suit Chestplate", "item.nerospace.oxygen_suit_t2_helmet": "Tier 2 Oxygen Suit Helmet", "item.nerospace.oxygen_suit_t2_leggings": "Tier 2 Oxygen Suit Leggings", + "item.nerospace.quartz_crawler_spawn_egg": "Quartz Crawler Spawn Egg", "item.nerospace.raw_nerosium": "Raw Nerosium", "item.nerospace.raw_nerosteel": "Raw Nerosteel", "item.nerospace.rocket.deployed": "Rocket deployed on the launch pad", @@ -119,6 +133,9 @@ "item.nerospace.rocket_tier_2": "Tier 2 Rocket", "item.nerospace.rocket_tier_3": "Tier 3 Rocket", "item.nerospace.rocket_tier_4": "Tier 4 Rocket", + "item.nerospace.station_compass": "Station Compass", + "item.nerospace.woolly_drift_spawn_egg": "Woolly Drift Spawn Egg", "item.nerospace.xertz_quartz": "Xertz Quartz", - "item.nerospace.xertz_resonator": "Xertz Resonator" + "item.nerospace.xertz_resonator": "Xertz Resonator", + "item.nerospace.xertz_stalker_spawn_egg": "Xertz Stalker Spawn Egg" } diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_villager_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_villager_spawn_egg.json new file mode 100644 index 0000000..48a55ab --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/alien_villager_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/alien_villager_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/cindara_compass.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/cindara_compass.json new file mode 100644 index 0000000..5858f3d --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/cindara_compass.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/cindara_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/cinder_stalker_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/cinder_stalker_spawn_egg.json new file mode 100644 index 0000000..2c3b575 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/cinder_stalker_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/cinder_stalker_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/ember_strutter_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/ember_strutter_spawn_egg.json new file mode 100644 index 0000000..ed81b1e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/ember_strutter_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/ember_strutter_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/frost_strider_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/frost_strider_spawn_egg.json new file mode 100644 index 0000000..46ad3aa --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/frost_strider_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/frost_strider_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/glacira_compass.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/glacira_compass.json new file mode 100644 index 0000000..10c98db --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/glacira_compass.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/glacira_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/greenling_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/greenling_spawn_egg.json new file mode 100644 index 0000000..3616a71 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/greenling_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/greenling_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_compass.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_compass.json new file mode 100644 index 0000000..c7a63f7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_compass.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/greenxertz_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_navigator.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_navigator.json new file mode 100644 index 0000000..32fa623 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/greenxertz_navigator.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/greenxertz_navigator" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/meadow_loper_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/meadow_loper_spawn_egg.json new file mode 100644 index 0000000..25d9c2f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/meadow_loper_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/meadow_loper_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/quartz_crawler_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/quartz_crawler_spawn_egg.json new file mode 100644 index 0000000..bc8ae2b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/quartz_crawler_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/quartz_crawler_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/station_compass.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/station_compass.json new file mode 100644 index 0000000..b52ab1c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/station_compass.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/station_compass" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/woolly_drift_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/woolly_drift_spawn_egg.json new file mode 100644 index 0000000..933fdd5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/woolly_drift_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/woolly_drift_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_stalker_spawn_egg.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_stalker_spawn_egg.json new file mode 100644 index 0000000..ab061d4 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/xertz_stalker_spawn_egg.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/xertz_stalker_spawn_egg" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_villager_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_villager_spawn_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..c3de1db19077768c0b8894c729cd78480e996e9b GIT binary patch literal 270 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`S3O-ELn`JZCrD%*=%(~Y3@%R1D50Ctx$FKfRS-OoU;)CR(g$oZBB%QKqlQz$L5Yl{=K~j71 zf3Y|{9x+Qt9uN=%V(y|_59lCh>|+gb zViJnl$*EuVC)_q+V{6$Qc%Z%7KOl+aFss1^h2^|Q%TmnZ^cAul%vq!lykcckl@xKF zz&rQBR9B0@gF=EjiU%`tIyqN+ZfJ1fclp9_B$MHcK#Ad`&&-Al44)bj=Zo?_s-VP*gT literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/cindara_compass.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/cindara_compass.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c1cb6ca075096c73e1abcd977a6631d27f7ee3 GIT binary patch literal 221 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`OFUg1Ln`JZCoB*!ND>a2bop1m z@1J{%vn5^`vLwtp%#;*xBs@2w>8gN8-=sO$>Sr9|t-iKULg0ZAi#f+l0k@K~FQoi` z)Fy7Yailp(V3VTHiF0o3>VNhvxNu|7DHh=aY1U`bQp9s^Z01v~nIXZ!kaG40gCp~{ zoLeq48EtH8j!16EIxJKX@{oB$go7bNG?y|z@E@ovf{QnV&N&XLVMm5M`8FuFnLDFZtM007s=8P?iT3J*Yv*5~~K?>)Tt005&K zmW9aR^(h2^cBqdtAVsUu4ceiG>y|9WX|Yo_0Z10cDccA;!nU*I7Xh~P)BLVmSS>^m zw}oFZh;yvMIPHJ{z}({QB1HzSTe6PQD#c&mjz;>2pX?1IP)yj;NaMZ$000016NVvIWX1$_c@1|GwZnfEX<`3+2n575ad5w?ZYp*KZw?sR9zz`s#> z#jb4*IWOy`zI}M)NHrqWn8#3v<#>7yh{!7gFd}qqbBv#W*Bh7;pZAIPOBB-qGlSWx zIu0azA9ed-m$fiIa=|Zp~sRB7!h_@Xq>sBS+3o#MV#_0CI jvYo@+1?}rv`|uOKRmDV=cG$1800000NkvXXu0mjf)_-3b literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/frost_strider_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/frost_strider_spawn_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..837ce0fef373c94be30a23850d7e824d8b6356a4 GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Cp=voLn`JZCrD%*=dkjNOcH*Sb1Y1i%ick(YX?x%|JKQA8+%Gm>YBL@__)s2knzq80`4~bknge z2KyasDGlo+S2Lbq2tT06Jm;l!^ddz|djVz!1-WRG&y_Q;0{z6`>FVdQ&MBb@00_it A>;M1& literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/glacira_compass.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/glacira_compass.png new file mode 100644 index 0000000000000000000000000000000000000000..3d67681bc0fb61be0a8d8583e61d2f08cecfda04 GIT binary patch literal 236 zcmVWV`HxNf$%88x>M;ahnp-9;LlLhjh{9?J3%X{Q#*V5a6a!%5H3d=svAKjn zMnUKQf(@7cYiOCE;JiD3Q1}ZrT>6jfMJ9ZHcH49Ug~sMpCS)g~JA0R(1qu!G8glqB zF%SUAny~?b25i~1i82F3L_~-)pr#;-p`vXbA@dk2+UCL21l$0u@k#)|^q^?Omylqt mt0{;=v5?}VOmSwS7ytl6|6jp$rP%WT0000STL z#4nBc;SX{a@ib&A98;}WJW--Sko!W$uk#O%F_g3R924sm+0Bq1=g_aGay?L-fkEQ8 WV~pox)0sd=GkCiCxvXa2bop1m z@1J{%vn5^`vLwtp%#;*xBs@2w>8gN8-=sO$>Sr9|t-iKULg0ZAi#f+l0k@K~5>nq? z3~~x08AATgnWps SGur`lE`z75pUXO@geCxA?oBZO literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_navigator.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_navigator.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca2720d5806564eb3829b9bed7865aa558eb84d GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`2RvOILn`JZCrD%*=1fCxcI!mLalH=4`BHllKRCE_IzBvWTjLeL tSmpC)LxDplZ_Y)JLZ?L*n*25l3~E6IJ(+x7vOvEuc)I$ztaD0e0sutrUfloy literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/meadow_loper_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/meadow_loper_spawn_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..4ec0299c8b384d04678aecb23f0518b0ddf0e7fa GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`$30yfLn`JZCrD%*=A2QEraVP93d&=kY!rsieK9(-i-G2W%b2&$_mi xQCX^R7sK73I}S0X$2s)t&3Ij~QeB#XAw|SzcKzAhSfG~}JYD@<);T3K0RV@_Uzz{_ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/quartz_crawler_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/quartz_crawler_spawn_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..ad0f6ca1eda7f0a60249530a1bfb1405e8ff2685 GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`J3L(+Ln`JZCrD%*=G5no6K}X;u1Io1Lc)*!vjS>0dKWHSNciwOqurmgOnge{mcQpu zpHAFS_?Ydm?&okGo@YEfc|3X%v5l8m3(oTJ@MNhUeUQ@>*YHzznIPL0zb?ZaJ9jQ% z(TNjR$UefpL8{?hOw76CnGCzu{)lP3%>5(Bl}+LzNAI%#7c!g;UpRhN?GfoTRy`D? pu<5gWLSVbG{sMRLNv~IMGhAqJwU(UqV0Zuk literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/station_compass.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/station_compass.png new file mode 100644 index 0000000000000000000000000000000000000000..eccae7456f143fb4500100824481f8df3870c4d0 GIT binary patch literal 221 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`OFUg1Ln`JZCoB*!ND>a2bop1m z@1J{%vn5^`vLwtp%#;*xBs@2w>8gN8-=sO$>Sr9|t-iKULg0ZAi#f+l0k@K~FQh&f zc}b}GzH~GM3aF)|FK*s5M@U3!cKFN$rW-l!H+$YRf%LTS3o|n|tdW#f$T%>~ceaLc zLwkF_1#?(#1XI6O4TBiB23vx6Vhe*0|5qWQO^Q4`DQc-D&IOjUS1~ZWe-p~qBq7oS PbT5OatDnm{r-UW|HJeN2 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/woolly_drift_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/woolly_drift_spawn_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..aab4319a53c31724d99fcd4d6ca4c397abebd697 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Q$1ZALn`JZCrD%*=7?X!H$oA)IFyetB7pnXo4Y2SmjpVhi3#A3BzNU%;ft_X0!8^G8XKO&R8QcbGBU ze0jnokik+=kzLL6A_sF=%#YmTGxu%0=~h&y0Ce5b!?paAvm~T|4rK6j^>bP0l+XkK DF8NM~ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_stalker_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_stalker_spawn_egg.png new file mode 100644 index 0000000000000000000000000000000000000000..938f511832d840d76a93d7de18629d5bb141ea9a GIT binary patch literal 228 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`t36#DLn`JZCrD%*=Nylb)|KV&`&n&2e%tN*NkT8-X?3l|bVxM1eBwqV|{^p7ht|CoO&NZL{Om@Vc^ zYLxV0we>7K>mPhM)GKWskaKTxsOuc=iYq4ne&n@3Pe;foE7=qq8 VeswB3?*Mc*gQu&X%Q~loCIGToS#1CS literal 0 HcmV?d00001 From 6dee7d41dcea8c7868a17508e1e81035d9e76826 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:31:44 +0200 Subject: [PATCH 39/82] Add dedicated Nerospace creative tab Introduce a cross-loader dedicated creative tab for the mod: add ModCreativeTab (registered via the common RegistrationProvider over CREATIVE_MODE_TAB) and wire it into ModRegistries.init(). Add ModItems.creativeContents() to produce a stable, flattened ordering for the tab. Remove per-loader creative-tab injection from Fabric and NeoForge (add explanatory comments) since the single tab now supplies contents. Add the language key (itemGroup.nerospace) and update the port checklist docs to explain the port and note the runtime bug fix (previous per-loader injections never populated tabs at runtime). --- docs/MULTILOADER_PORT_CHECKLIST.md | 9 ++++- .../nerospace/registry/ModCreativeTab.java | 40 +++++++++++++++++++ .../neroland/nerospace/registry/ModItems.java | 21 ++++++++++ .../nerospace/registry/ModRegistries.java | 1 + .../assets/nerospace/lang/en_us.json | 3 +- .../nerospace/fabric/NerospaceFabric.java | 7 +--- .../nerospace/neoforge/NerospaceNeoForge.java | 16 ++------ 7 files changed, 76 insertions(+), 21 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModCreativeTab.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 30fb965..eee9b09 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -150,8 +150,13 @@ checked by a headless build). - [ ] `ModDataComponents`, `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge attachments vs Fabric component/attachment API), `ModCriteria` (advancement triggers), `ModTags`, `ModFeatures`, `ModConfiguredFeatures`/`ModPlacedFeatures`/`ModBiomes`/`ModBiomeModifiers` (datagen - bootstraps — mostly superseded by the copied JSON), `ModCreativeModeTabs` (a dedicated mod tab; we - currently inject into vanilla tabs), `ModDimensionTypes` (space type — JSON already copied). + bootstraps — mostly superseded by the copied JSON), `ModDimensionTypes` (space type — JSON already copied). +- [x] `ModCreativeModeTabs` → ported as `ModCreativeTab`: a **dedicated "Nerospace" tab** registered via + the cross-loader `RegistrationProvider` over the vanilla `CREATIVE_MODE_TAB` registry, listing all + items (`ModItems.creativeContents()`). **Fixes a latent runtime bug**: the earlier per-loader injection + into vanilla tabs (`BuildCreativeModeTabContentsEvent` / `CreativeModeTabEvents`) never populated the + tabs in-game (items were searchable but absent when browsing) — replaced on both loaders. Note: vanilla + `CreativeModeTab.builder(Row, column)` (the no-arg overload + `withTabsBefore` are NeoForge-only). ### Networking (`network/` 5) — **needed by oxygen HUD, meteors, pipe modes** - [ ] Cross-loader packet seam: NeoForge `PayloadRegistrar` vs Fabric networking API. `ModNetwork`, diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModCreativeTab.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModCreativeTab.java new file mode 100644 index 0000000..8a1c4a9 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModCreativeTab.java @@ -0,0 +1,40 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.CreativeModeTab; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * The single dedicated Nerospace creative tab, registered cross-loader via {@link RegistrationProvider} + * over the vanilla {@code CREATIVE_MODE_TAB} registry (the root mod's approach). + * + *

The earlier multiloader scattered items into the vanilla tabs through each loader's own injection + * API ({@code BuildCreativeModeTabContentsEvent} on NeoForge, {@code CreativeModeTabEvents} on Fabric); + * that never actually populated the tabs at runtime (the multiloader had only ever been build-verified). + * A dedicated tab built via {@code CreativeModeTab.builder().displayItems(...)} is plain vanilla and so + * works identically on both loaders, mirroring the root mod, and shows a proper "Nerospace" tab.

+ */ +public final class ModCreativeTab { + + public static final RegistrationProvider TABS = + RegistrationProvider.get(Registries.CREATIVE_MODE_TAB, NerospaceCommon.MOD_ID); + + // NOTE: vanilla CreativeModeTab.builder takes (Row, column) — the no-arg overload is NeoForge-only; + // likewise withTabsBefore/After are NeoForge extensions, so neither is used here (common = raw vanilla). + public static final RegistryEntry NEROSPACE = TABS.register("nerospace", + key -> CreativeModeTab.builder(CreativeModeTab.Row.TOP, 0) + .title(Component.translatable("itemGroup.nerospace")) + .icon(() -> new ItemStack(ModItems.NEROSIUM_INGOT.get())) + .displayItems((params, output) -> ModItems.creativeContents().forEach(output::accept)) + .build()); + + private ModCreativeTab() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 4d628fb..6b7bcef 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -1,5 +1,6 @@ package za.co.neroland.nerospace.registry; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.function.UnaryOperator; @@ -253,6 +254,26 @@ public static Map, List> creativeTabItems List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get())); } + /** + * All mod items in a defined order, for the dedicated {@link ModCreativeTab}. Flattens + * {@link #creativeTabItems()} (which still groups by vanilla theme) in a stable tab order. + */ + public static List creativeContents() { + Map, List> groups = creativeTabItems(); + List all = new ArrayList<>(); + for (ResourceKey tab : List.of( + CreativeModeTabs.NATURAL_BLOCKS, CreativeModeTabs.BUILDING_BLOCKS, + CreativeModeTabs.INGREDIENTS, CreativeModeTabs.TOOLS_AND_UTILITIES, + CreativeModeTabs.COMBAT, CreativeModeTabs.FUNCTIONAL_BLOCKS, + CreativeModeTabs.SPAWN_EGGS)) { + List group = groups.get(tab); + if (group != null) { + all.addAll(group); + } + } + return all; + } + private ModItems() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index ef17c16..5d13b4c 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -19,5 +19,6 @@ public static void init() { ModBlockEntities.init(); ModMenuTypes.init(); ModEntities.init(); + ModCreativeTab.init(); } } diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index afc1f37..f651d9c 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -137,5 +137,6 @@ "item.nerospace.woolly_drift_spawn_egg": "Woolly Drift Spawn Egg", "item.nerospace.xertz_quartz": "Xertz Quartz", "item.nerospace.xertz_resonator": "Xertz Resonator", - "item.nerospace.xertz_stalker_spawn_egg": "Xertz Stalker Spawn Egg" + "item.nerospace.xertz_stalker_spawn_egg": "Xertz Stalker Spawn Egg", + "itemGroup.nerospace": "Nerospace" } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 39c325b..5e2542a 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -3,7 +3,6 @@ import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; -import net.fabricmc.fabric.api.creativetab.v1.CreativeModeTabEvents; import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry; import net.fabricmc.fabric.api.transfer.v1.item.ContainerStorage; @@ -21,7 +20,6 @@ import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModEntityAttributes; -import za.co.neroland.nerospace.registry.ModItems; /** * Fabric entry point. Shared init registers content eagerly, then Fabric-side @@ -54,9 +52,8 @@ public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); NerospaceCommon.init(); - ModItems.creativeTabItems().forEach((tab, items) -> - CreativeModeTabEvents.modifyOutputEvent(tab) - .register(output -> items.forEach(output::accept))); + // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated + // Nerospace tab), so no per-loader creative-tab injection is needed here. addOverworldOre("nerosium_ore_placed"); diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 556b0bf..22bb2fb 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -1,20 +1,15 @@ package za.co.neroland.nerospace.neoforge; -import java.util.List; - -import net.minecraft.world.level.ItemLike; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLEnvironment; -import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; import za.co.neroland.nerospace.registry.ModEntityAttributes; -import za.co.neroland.nerospace.registry.ModItems; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; /** @@ -34,17 +29,12 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { if (FMLEnvironment.getDist() == Dist.CLIENT) { NeoForgeClientSetup.init(modEventBus); } - modEventBus.addListener(this::onBuildCreativeTabs); + // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated + // Nerospace tab registered via the vanilla CREATIVE_MODE_TAB registry), so no NeoForge-specific + // BuildCreativeModeTabContentsEvent injection is needed. modEventBus.addListener(this::onCreateEntityAttributes); } - private void onBuildCreativeTabs(BuildCreativeModeTabContentsEvent event) { - List items = ModItems.creativeTabItems().get(event.getTabKey()); - if (items != null) { - items.forEach(event::accept); - } - } - private void onCreateEntityAttributes(EntityAttributeCreationEvent event) { ModEntityAttributes.forEach((type, builder) -> event.put(type, builder.build())); } From 6c775ccfac4b8f8dcd18f300b0a971be57b730cb Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sat, 20 Jun 2026 18:47:56 +0200 Subject: [PATCH 40/82] Add machine modules and creative storage Introduce a reusable MachineModules system (MachineModules, ModuleType, UpgradeModuleItem) and integrate it into the quarry: controller now exposes module slots, aggregates speed/efficiency/fortune/silk-touch effects, adjusts energy/per-block and mining rate, and persists module state. Update QuarryMenu to show module slots and enforce module-only placement, and route container access between frame/modules/output. Add creative storage endpoints (AbstractStorageBlock + CreativeFluidTank/GasTank/ItemStore and their block entities) that provide endless fluid/gas/item sources, plus their block/item registrations and assets. Register new blocks/items/block-entities and add four module items (speed/efficiency/fortune/silk-touch); copy related assets and language keys. Minor tweaks: routing helpers, enchantment lookup via Registries, serialization hooks for modules, and UI/container size plumbing. --- docs/MULTILOADER_PORT_CHECKLIST.md | 19 +- .../quarry/QuarryControllerBlockEntity.java | 91 +++- .../nerospace/machine/quarry/QuarryMenu.java | 49 +- .../nerospace/module/MachineModules.java | 94 ++++ .../neroland/nerospace/module/ModuleType.java | 27 ++ .../nerospace/module/UpgradeModuleItem.java | 35 ++ .../nerospace/registry/ModBlockEntities.java | 15 + .../nerospace/registry/ModBlocks.java | 18 + .../neroland/nerospace/registry/ModItems.java | 19 +- .../storage/AbstractStorageBlock.java | 22 + .../storage/CreativeFluidTankBlock.java | 30 ++ .../storage/CreativeFluidTankBlockEntity.java | 49 ++ .../storage/CreativeGasTankBlock.java | 30 ++ .../storage/CreativeGasTankBlockEntity.java | 48 ++ .../storage/CreativeItemStoreBlock.java | 60 +++ .../storage/CreativeItemStoreBlockEntity.java | 101 +++++ .../blockstates/creative_fluid_tank.json | 7 + .../blockstates/creative_gas_tank.json | 7 + .../blockstates/creative_item_store.json | 7 + .../nerospace/items/creative_fluid_tank.json | 6 + .../nerospace/items/creative_gas_tank.json | 6 + .../nerospace/items/creative_item_store.json | 6 + .../nerospace/items/efficiency_module.json | 6 + .../nerospace/items/fortune_module.json | 6 + .../nerospace/items/silk_touch_module.json | 6 + .../assets/nerospace/items/speed_module.json | 6 + .../assets/nerospace/lang/en_us.json | 9 + .../models/block/creative_fluid_tank.json | 425 ++++++++++++++++++ .../models/block/creative_gas_tank.json | 425 ++++++++++++++++++ .../models/block/creative_item_store.json | 6 + .../models/item/efficiency_module.json | 6 + .../nerospace/models/item/fortune_module.json | 6 + .../models/item/silk_touch_module.json | 6 + .../nerospace/models/item/speed_module.json | 6 + .../textures/block/creative_fluid_tank.png | Bin 0 -> 297 bytes .../block/creative_fluid_tank_core.png | Bin 0 -> 306 bytes .../textures/block/creative_gas_tank.png | Bin 0 -> 332 bytes .../textures/block/creative_gas_tank_core.png | Bin 0 -> 253 bytes .../textures/block/creative_item_store.png | Bin 0 -> 309 bytes .../textures/item/efficiency_module.png | Bin 0 -> 156 bytes .../textures/item/fortune_module.png | Bin 0 -> 154 bytes .../textures/item/silk_touch_module.png | Bin 0 -> 154 bytes .../nerospace/textures/item/speed_module.png | Bin 0 -> 156 bytes .../blocks/creative_fluid_tank.json | 21 + .../loot_table/blocks/creative_gas_tank.json | 21 + .../blocks/creative_item_store.json | 21 + .../nerospace/fabric/NerospaceFabric.java | 11 + .../neoforge/NeoForgeCapabilities.java | 14 + 48 files changed, 1711 insertions(+), 36 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/module/MachineModules.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/module/ModuleType.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/module/UpgradeModuleItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/AbstractStorageBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlockEntity.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_fluid_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_gas_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_item_store.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/creative_fluid_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/creative_gas_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/creative_item_store.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/efficiency_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/fortune_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/silk_touch_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/speed_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/creative_fluid_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/creative_gas_tank.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/creative_item_store.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/efficiency_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/fortune_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/silk_touch_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/speed_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_item_store.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/efficiency_module.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/fortune_module.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/silk_touch_module.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_module.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_fluid_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_gas_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_item_store.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index eee9b09..4cfe9aa 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,7 +1,7 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~121 classes ported, ~143 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~134 classes ported, ~130 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **2026-06-20 update — quarry ported.** All 4 cells green. Added 11 classes: @@ -126,15 +126,22 @@ checked by a headless build). `PipeUpgradeItem`, `SetPipeModePayload`, `UniversalPipeRenderer` (streams + travelling-item visuals, per-side I/O modes, filters). Needs networking seam. -### Machine modules / upgrades (`module/` 3) -- [ ] `MachineModules`, `ModuleType`, `UpgradeModuleItem` — speed/efficiency upgrade items for machines. +### Machine modules / upgrades (`module/` 3) — **DONE (4 cells green)** +- [x] `ModuleType`, `UpgradeModuleItem` (4 items: speed / efficiency / fortune / silk-touch) + `MachineModules` + (rebuilt on a `NonNullList` instead of the root's `MachineItemHandler`). **Re-enabled in the quarry**: + module slots restored in the controller's combined `WorldlyContainer` view + `QuarryMenu`, and the + speed / energy / Silk-Touch / Fortune multipliers now drive the dig (the quarry's earlier `×1.0` + deferral is resolved). Assets + 4 lang keys copied. ### Solar — tiers/array/BER (`solar/` 4; single-tier base **done**) - [~] `SolarTier`, `SolarArray` (multi-panel pooling), the root tiered block/BE + sun-tracking BER. -### Creative storage variants (`storage/Creative*`) -- [ ] `CreativeItemStore`, `CreativeFluidTank`, `CreativeGasTank` (+ `AbstractStorageBlock`) — infinite - configurable sources. Marginal (creative-only). +### Creative storage variants (`storage/Creative*`) — **DONE (4 cells green)** +- [x] `AbstractStorageBlock` (shared base) + `CreativeFluidTank` (endless rocket_fuel), `CreativeGasTank` + (endless oxygen), `CreativeItemStore` (right-click to set an endless item source). Fluid/gas mirror + the ported `CreativeBattery`'s infinite pattern on the cross-loader storage interfaces; the item store + exposes its endless source through a vanilla `Container` (no NeoForge `InfiniteResourceHandler`). + Fluid/Gas/Item caps wired on both loaders; assets + lang copied. ### Utility items (`item/`) — **partly DONE (4 cells green)** - [x] `NerospaceSpawnEggItem` (+ **9 spawn eggs**: xertz stalker, quartz crawler, greenling, alien diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java index a761924..6eb63fa 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java @@ -10,6 +10,7 @@ import net.minecraft.core.Direction; import net.minecraft.core.NonNullList; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; @@ -23,6 +24,7 @@ import net.minecraft.world.inventory.ContainerData; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.Items; +import net.minecraft.world.item.enchantment.Enchantments; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.LiquidBlock; @@ -39,6 +41,8 @@ import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.fluid.FluidTank; import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.module.MachineModules; +import za.co.neroland.nerospace.module.UpgradeModuleItem; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModBlocks; import za.co.neroland.nerospace.registry.ModItems; @@ -78,11 +82,14 @@ public enum State { } private final MinerTier tier; + private final int moduleSlots; private final int containerSize; private final EnergyBuffer energy = new EnergyBuffer(ENERGY_BUFFER, ENERGY_MAX_INSERT, 0, this::setChanged); private final FluidTank fluidBuffer = new FluidTank(FLUID_CAPACITY, this::setChanged); - private final NonNullList items; + /** Frame casing at index 0, mined output at indices {@code [1, OUTPUT_SLOTS]}. */ + private final NonNullList items = NonNullList.withSize(1 + OUTPUT_SLOTS, ItemStack.EMPTY); + private final MachineModules modules; private final OutputFilter filter = OutputFilter.KEEP_ALL; private State state = State.IDLE; @@ -125,12 +132,18 @@ public int getCount() { } }; + @SuppressWarnings("this-escape") // setChanged callback only invoked after construction public QuarryControllerBlockEntity(BlockPos pos, BlockState blockState) { super(ModBlockEntities.QUARRY_CONTROLLER.get(), pos, blockState); this.tier = blockState.getBlock() instanceof QuarryControllerBlock controller ? controller.tier() : MinerTier.TIER_1; - this.containerSize = 1 + OUTPUT_SLOTS; // frame + output (modules deferred) - this.items = NonNullList.withSize(this.containerSize, ItemStack.EMPTY); + this.moduleSlots = this.tier.moduleSlots(); + this.containerSize = 1 + this.moduleSlots + OUTPUT_SLOTS; // frame + modules + output + this.modules = new MachineModules(this.moduleSlots, this::setChanged); + } + + public int moduleSlots() { + return this.moduleSlots; } // --- Capability accessors --------------------------------------------------- @@ -290,8 +303,8 @@ private void mine(ServerLevel level) { return; } int floor = level.getMinY(); - int energyPerBlock = ENERGY_PER_BLOCK; - ItemStack tool = new ItemStack(Items.NETHERITE_PICKAXE); + int energyPerBlock = quarryEnergyPerBlock(); + ItemStack tool = miningTool(level); int columns = region.columns(); boolean changed = false; @@ -385,10 +398,29 @@ private void mine(ServerLevel level) { private long miningInterval(ServerLevel level) { double planet = PlanetMiningProfile.forDimension(level.dimension()).speedMultiplier(); - double rate = this.tier.baseBlocksPerCycle() * planet; // modules deferred (×1.0) + double rate = this.tier.baseBlocksPerCycle() * this.modules.speedMultiplier() * planet; return Math.max(1L, Math.round(MINE_INTERVAL / Math.max(0.01, rate))); } + private int quarryEnergyPerBlock() { + return Math.max(1, (int) Math.round(ENERGY_PER_BLOCK * this.modules.energyMultiplier())); + } + + /** Build the synthetic harvest tool reflecting the Silk-Touch / Fortune modules. */ + private ItemStack miningTool(ServerLevel level) { + ItemStack tool = new ItemStack(Items.NETHERITE_PICKAXE); + var enchantments = level.registryAccess().lookupOrThrow(Registries.ENCHANTMENT); + if (this.modules.silkTouch()) { + tool.enchant(enchantments.getOrThrow(Enchantments.SILK_TOUCH), 1); + } else { + int fortune = this.modules.fortuneLevel(); + if (fortune > 0) { + tool.enchant(enchantments.getOrThrow(Enchantments.FORTUNE), fortune); + } + } + return tool; + } + /** Try to buffer all kept drops atomically into the output slots; filtered-out drops are voided. */ private boolean acceptDrops(List drops) { List kept = new ArrayList<>(); @@ -582,6 +614,7 @@ protected void saveAdditional(ValueOutput output) { for (int i = 0; i < OUTPUT_SLOTS; i++) { output.store("Out" + i, ItemStack.OPTIONAL_CODEC, this.items.get(OUTPUT_START + i)); } + this.modules.save(output); output.putString("MinerState", this.state.name()); output.putString("PauseReason", this.pauseReason); output.putInt("FrameIndex", this.frameIndex); @@ -614,6 +647,7 @@ protected void loadAdditional(ValueInput input) { for (int i = 0; i < OUTPUT_SLOTS; i++) { this.items.set(OUTPUT_START + i, input.read("Out" + i, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); } + this.modules.load(input); this.state = parseState(input.getStringOr("MinerState", State.IDLE.name())); this.pauseReason = input.getStringOr("PauseReason", ""); this.frameIndex = input.getIntOr("FrameIndex", 0); @@ -652,10 +686,25 @@ public Component getDisplayName() { @Nullable @Override public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { - return new QuarryMenu(containerId, playerInventory, this, this.dataAccess); + return new QuarryMenu(containerId, playerInventory, this, this.dataAccess, this.moduleSlots); } - // --- WorldlyContainer (slot 0 = frame in; slots 1.. = output out) ----------- + // --- WorldlyContainer (combined view: [0]=frame, [1..M]=modules, [M+1..]=output) ---- + // Frame + output live in `items` (index 0 + 1..OUTPUT_SLOTS); modules live in `modules`. + + private NonNullList routeList(int slot) { + return (slot >= OUTPUT_START && slot <= this.moduleSlots) ? this.modules.items() : this.items; + } + + private int routeIndex(int slot) { + if (slot == FRAME_SLOT) { + return 0; + } + if (slot <= this.moduleSlots) { + return slot - 1; // modules: 0..moduleSlots-1 + } + return slot - this.moduleSlots; // output: items[1..OUTPUT_SLOTS] + } @Override public int[] getSlotsForFace(Direction side) { @@ -668,17 +717,23 @@ public int[] getSlotsForFace(Direction side) { @Override public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { - return slot == FRAME_SLOT && stack.is(ModItems.FRAME_CASING.get()); + return canPlaceItem(slot, stack); } @Override public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { - return slot >= OUTPUT_START; + return slot > this.moduleSlots; // output slots only } @Override public boolean canPlaceItem(int slot, ItemStack stack) { - return slot == FRAME_SLOT && stack.is(ModItems.FRAME_CASING.get()); + if (slot == FRAME_SLOT) { + return stack.is(ModItems.FRAME_CASING.get()); + } + if (slot <= this.moduleSlots) { + return UpgradeModuleItem.isModule(stack); + } + return false; // output slots: take only } @Override @@ -693,17 +748,22 @@ public boolean isEmpty() { return false; } } + for (ItemStack stack : this.modules.items()) { + if (!stack.isEmpty()) { + return false; + } + } return true; } @Override public ItemStack getItem(int slot) { - return this.items.get(slot); + return routeList(slot).get(routeIndex(slot)); } @Override public ItemStack removeItem(int slot, int amount) { - ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + ItemStack r = ContainerHelper.removeItem(routeList(slot), routeIndex(slot), amount); if (!r.isEmpty()) { this.setChanged(); } @@ -712,12 +772,12 @@ public ItemStack removeItem(int slot, int amount) { @Override public ItemStack removeItemNoUpdate(int slot) { - return ContainerHelper.takeItem(this.items, slot); + return ContainerHelper.takeItem(routeList(slot), routeIndex(slot)); } @Override public void setItem(int slot, ItemStack stack) { - this.items.set(slot, stack); + routeList(slot).set(routeIndex(slot), stack); this.setChanged(); } @@ -733,5 +793,6 @@ public boolean stillValid(Player player) { @Override public void clearContent() { this.items.clear(); + this.modules.items().clear(); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java index d079038..60eaf65 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java @@ -10,40 +10,46 @@ import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import za.co.neroland.nerospace.module.UpgradeModuleItem; import za.co.neroland.nerospace.registry.ModItems; import za.co.neroland.nerospace.registry.ModMenuTypes; /** - * Menu for the quarry controller. Layout: slot 0 = frame casing, then the output buffer; followed by - * the player inventory. Status (energy, state, fluid, depth) is synced through {@link ContainerData}. - * - *

Cross-loader port note: upgrade modules are deferred, so there are no module slots here.

+ * Menu for the quarry controller. Combined inventory: slot 0 = frame casing, 1..M = module cards, then + * the output buffer; followed by the player inventory. Status is synced through {@link ContainerData}. */ public class QuarryMenu extends AbstractContainerMenu { - private static final int MACHINE_SLOTS = 1 + QuarryControllerBlockEntity.OUTPUT_SLOTS; + private static final int TIER1_MODULE_SLOTS = 1; private final Container container; private final ContainerData data; + private final int machineSlots; + private final int moduleSlots; - /** Client constructor. */ + /** Client constructor (Tier-1 layout). */ public QuarryMenu(int containerId, Inventory playerInventory) { this(containerId, playerInventory, - new SimpleContainer(MACHINE_SLOTS), - new SimpleContainerData(QuarryControllerBlockEntity.DATA_COUNT)); + new SimpleContainer(1 + TIER1_MODULE_SLOTS + QuarryControllerBlockEntity.OUTPUT_SLOTS), + new SimpleContainerData(QuarryControllerBlockEntity.DATA_COUNT), + TIER1_MODULE_SLOTS); } /** Server constructor. */ @SuppressWarnings("this-escape") - public QuarryMenu(int containerId, Inventory playerInventory, Container container, ContainerData data) { + public QuarryMenu(int containerId, Inventory playerInventory, Container container, ContainerData data, int moduleSlots) { super(ModMenuTypes.QUARRY_CONTROLLER.get(), containerId); - checkContainerSize(container, MACHINE_SLOTS); this.container = container; this.data = data; + this.moduleSlots = moduleSlots; + this.machineSlots = container.getContainerSize(); this.addSlot(new FrameSlot(container, QuarryControllerBlockEntity.FRAME_SLOT, 8, 20)); + for (int i = 0; i < moduleSlots; i++) { + this.addSlot(new ModuleSlot(container, 1 + i, 26 + i * 18, 20)); + } - int outStart = 1; + int outStart = 1 + moduleSlots; for (int i = 0; i < QuarryControllerBlockEntity.OUTPUT_SLOTS; i++) { int row = i / 6; int col = i % 6; @@ -68,10 +74,10 @@ public ItemStack quickMoveStack(Player player, int index) { } ItemStack raw = slot.getItem(); moved = raw.copy(); - int playerStart = MACHINE_SLOTS; - int playerEnd = MACHINE_SLOTS + 36; + int playerStart = this.machineSlots; + int playerEnd = this.machineSlots + 36; - if (index < MACHINE_SLOTS) { + if (index < this.machineSlots) { if (!this.moveItemStackTo(raw, playerStart, playerEnd, true)) { return ItemStack.EMPTY; } @@ -81,6 +87,10 @@ public ItemStack quickMoveStack(Player player, int index) { QuarryControllerBlockEntity.FRAME_SLOT + 1, false)) { return ItemStack.EMPTY; } + } else if (UpgradeModuleItem.isModule(raw)) { + if (!this.moveItemStackTo(raw, 1, 1 + this.moduleSlots, false)) { + return ItemStack.EMPTY; + } } else { return ItemStack.EMPTY; } @@ -142,6 +152,17 @@ public boolean mayPlace(ItemStack stack) { } } + private static final class ModuleSlot extends Slot { + ModuleSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return UpgradeModuleItem.isModule(stack); + } + } + private static final class OutputSlot extends Slot { OutputSlot(Container container, int slot, int x, int y) { super(container, slot, x, y); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/module/MachineModules.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/module/MachineModules.java new file mode 100644 index 0000000..255e7a3 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/module/MachineModules.java @@ -0,0 +1,94 @@ +package za.co.neroland.nerospace.module; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +/** + * A reusable bank of upgrade-module slots that any machine can embed (the quarry is the first consumer). + * It owns the module slots and exposes the aggregated effects the host machine reads each tick. Effects + * scale with the number of matching modules across the slots, each clamped so a stack of cards can't + * trivialise balance. + * + *

Cross-loader port note: the root backed this with a NeoForge-transfer {@code MachineItemHandler}; + * the multiloader uses a plain {@link NonNullList} (the host machine exposes the slots through its own + * vanilla {@code Container}/capability view).

+ */ +public final class MachineModules { + + private static final double SPEED_PER_MODULE = 0.5D; + private static final double EFFICIENCY_PER_MODULE = 0.15D; + private static final double MIN_ENERGY_FRACTION = 0.25D; + private static final int MAX_FORTUNE = 3; + private static final double MAX_SPEED = 8.0D; + + private final NonNullList items; + private final Runnable onChange; + + public MachineModules(int slots, Runnable onChange) { + this.items = NonNullList.withSize(slots, ItemStack.EMPTY); + this.onChange = onChange; + } + + /** The backing slot list (the host machine's Container view routes to it). */ + public NonNullList items() { + return this.items; + } + + public int slots() { + return this.items.size(); + } + + public ItemStack getStack(int index) { + return this.items.get(index); + } + + public void setStack(int index, ItemStack stack) { + this.items.set(index, stack); + this.onChange.run(); + } + + private int count(ModuleType type) { + int total = 0; + for (ItemStack stack : this.items) { + if (!stack.isEmpty() && UpgradeModuleItem.typeOf(stack) == type) { + total += stack.getCount(); + } + } + return total; + } + + /** Work-cap multiplier (>= 1): more Speed modules = a higher per-cycle ceiling. */ + public double speedMultiplier() { + return Math.min(MAX_SPEED, 1.0D + count(ModuleType.SPEED) * SPEED_PER_MODULE); + } + + /** Energy-cost multiplier (<= 1): more Efficiency modules = cheaper work, floored. */ + public double energyMultiplier() { + return Math.max(MIN_ENERGY_FRACTION, 1.0D - count(ModuleType.EFFICIENCY) * EFFICIENCY_PER_MODULE); + } + + /** Effective Fortune level applied to harvested blocks (0 = none), ignored when {@link #silkTouch()}. */ + public int fortuneLevel() { + return Math.min(MAX_FORTUNE, count(ModuleType.FORTUNE)); + } + + public boolean silkTouch() { + return count(ModuleType.SILK_TOUCH) > 0; + } + + // --- Persistence (host machine calls these from save/loadAdditional) -------- + + public void save(ValueOutput output) { + for (int i = 0; i < this.items.size(); i++) { + output.store("Module" + i, ItemStack.OPTIONAL_CODEC, this.items.get(i)); + } + } + + public void load(ValueInput input) { + for (int i = 0; i < this.items.size(); i++) { + this.items.set(i, input.read("Module" + i, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/module/ModuleType.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/module/ModuleType.java new file mode 100644 index 0000000..4e16ac2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/module/ModuleType.java @@ -0,0 +1,27 @@ +package za.co.neroland.nerospace.module; + +/** + * The kinds of cross-machine upgrade module (the quarry is the first consumer, but the system is + * machine-agnostic: any machine can embed a {@link MachineModules} and read the same effects). Each + * module is its own registered item ({@link UpgradeModuleItem}); a machine counts the modules in its + * slots and aggregates their effects. + */ +public enum ModuleType { + + /** Raises the per-cycle work cap so the machine does more when fed more power. */ + SPEED, + /** Lowers the energy cost per unit of work. */ + EFFICIENCY, + /** Applies a Fortune level to harvested blocks (mutually overridden by {@link #SILK_TOUCH}). */ + FORTUNE, + /** Harvests blocks with Silk Touch (takes precedence over {@link #FORTUNE}). */ + SILK_TOUCH; + + public static ModuleType byOrdinal(int ordinal) { + ModuleType[] values = values(); + if (ordinal < 0 || ordinal >= values.length) { + return SPEED; + } + return values[ordinal]; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/module/UpgradeModuleItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/module/UpgradeModuleItem.java new file mode 100644 index 0000000..e8acdb8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/module/UpgradeModuleItem.java @@ -0,0 +1,35 @@ +package za.co.neroland.nerospace.module; + +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +/** + * A machine upgrade module card. One {@link UpgradeModuleItem} class backs all module types; each + * registered item fixes its {@link ModuleType} so the card is identified purely by its item (no data + * component needed) and is portable across every machine that embeds a {@link MachineModules}. + */ +public class UpgradeModuleItem extends Item { + + private final ModuleType type; + + public UpgradeModuleItem(Properties properties, ModuleType type) { + super(properties); + this.type = type; + } + + public ModuleType moduleType() { + return this.type; + } + + /** @return the module type of {@code stack}, or {@code null} if it is not a module card. */ + @Nullable + public static ModuleType typeOf(ItemStack stack) { + return stack.getItem() instanceof UpgradeModuleItem module ? module.moduleType() : null; + } + + public static boolean isModule(ItemStack stack) { + return stack.getItem() instanceof UpgradeModuleItem; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index fb92b87..974d42a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -16,6 +16,9 @@ import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; +import za.co.neroland.nerospace.storage.CreativeFluidTankBlockEntity; +import za.co.neroland.nerospace.storage.CreativeGasTankBlockEntity; +import za.co.neroland.nerospace.storage.CreativeItemStoreBlockEntity; import za.co.neroland.nerospace.storage.GasTankBlockEntity; import za.co.neroland.nerospace.storage.TrashCanBlockEntity; import za.co.neroland.nerospace.storage.BatteryBlockEntity; @@ -88,6 +91,18 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("fuel_refinery", key -> new BlockEntityType<>(FuelRefineryBlockEntity::new, java.util.Set.of(ModBlocks.FUEL_REFINERY.get()))); + public static final RegistryEntry> CREATIVE_FLUID_TANK = + BLOCK_ENTITIES.register("creative_fluid_tank", + key -> new BlockEntityType<>(CreativeFluidTankBlockEntity::new, java.util.Set.of(ModBlocks.CREATIVE_FLUID_TANK.get()))); + + public static final RegistryEntry> CREATIVE_GAS_TANK = + BLOCK_ENTITIES.register("creative_gas_tank", + key -> new BlockEntityType<>(CreativeGasTankBlockEntity::new, java.util.Set.of(ModBlocks.CREATIVE_GAS_TANK.get()))); + + public static final RegistryEntry> CREATIVE_ITEM_STORE = + BLOCK_ENTITIES.register("creative_item_store", + key -> new BlockEntityType<>(CreativeItemStoreBlockEntity::new, java.util.Set.of(ModBlocks.CREATIVE_ITEM_STORE.get()))); + public static final RegistryEntry> QUARRY_CONTROLLER = BLOCK_ENTITIES.register("quarry_controller", key -> new BlockEntityType<>(QuarryControllerBlockEntity::new, java.util.Set.of(ModBlocks.QUARRY_CONTROLLER.get()))); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 57a77b0..3914aad 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -28,6 +28,9 @@ import za.co.neroland.nerospace.rocket.LaunchGantryBlock; import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; import za.co.neroland.nerospace.storage.CreativeBatteryBlock; +import za.co.neroland.nerospace.storage.CreativeFluidTankBlock; +import za.co.neroland.nerospace.storage.CreativeGasTankBlock; +import za.co.neroland.nerospace.storage.CreativeItemStoreBlock; import za.co.neroland.nerospace.storage.GasTankBlock; import za.co.neroland.nerospace.storage.TrashCanBlock; import za.co.neroland.nerospace.storage.BatteryBlock; @@ -159,6 +162,21 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_PINK).strength(-1.0F, 3_600_000.0F) .sound(SoundType.METAL).noOcclusion())); + public static final RegistryEntry CREATIVE_FLUID_TANK = BLOCKS.register("creative_fluid_tank", + key -> new CreativeFluidTankBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_PINK).strength(-1.0F, 3_600_000.0F) + .sound(SoundType.METAL).noOcclusion())); + + public static final RegistryEntry CREATIVE_GAS_TANK = BLOCKS.register("creative_gas_tank", + key -> new CreativeGasTankBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_PINK).strength(-1.0F, 3_600_000.0F) + .sound(SoundType.METAL).noOcclusion())); + + public static final RegistryEntry CREATIVE_ITEM_STORE = BLOCKS.register("creative_item_store", + key -> new CreativeItemStoreBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_PINK).strength(-1.0F, 3_600_000.0F) + .sound(SoundType.METAL).noOcclusion())); + public static final RegistryEntry GAS_TANK = BLOCKS.register("gas_tank", key -> new GasTankBlock(BlockBehaviour.Properties.of() .setId(key).mapColor(MapColor.METAL).strength(3.0F, 6.0F) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 6b7bcef..cfc8e63 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -32,6 +32,8 @@ import za.co.neroland.nerospace.item.DestinationCompassItem; import za.co.neroland.nerospace.item.GreenxertzNavigatorItem; import za.co.neroland.nerospace.item.NerospaceSpawnEggItem; +import za.co.neroland.nerospace.module.ModuleType; +import za.co.neroland.nerospace.module.UpgradeModuleItem; import za.co.neroland.nerospace.rocket.RocketItem; import za.co.neroland.nerospace.rocket.RocketTier; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -77,6 +79,9 @@ public final class ModItems { public static final RegistryEntry UNIVERSAL_PIPE_ITEM = blockItem("universal_pipe", ModBlocks.UNIVERSAL_PIPE); public static final RegistryEntry TRASH_CAN_ITEM = blockItem("trash_can", ModBlocks.TRASH_CAN); public static final RegistryEntry CREATIVE_BATTERY_ITEM = blockItem("creative_battery", ModBlocks.CREATIVE_BATTERY); + public static final RegistryEntry CREATIVE_FLUID_TANK_ITEM = blockItem("creative_fluid_tank", ModBlocks.CREATIVE_FLUID_TANK); + public static final RegistryEntry CREATIVE_GAS_TANK_ITEM = blockItem("creative_gas_tank", ModBlocks.CREATIVE_GAS_TANK); + public static final RegistryEntry CREATIVE_ITEM_STORE_ITEM = blockItem("creative_item_store", ModBlocks.CREATIVE_ITEM_STORE); public static final RegistryEntry GAS_TANK_ITEM = blockItem("gas_tank", ModBlocks.GAS_TANK); public static final RegistryEntry OXYGEN_GENERATOR_ITEM = blockItem("oxygen_generator", ModBlocks.OXYGEN_GENERATOR); public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); @@ -109,6 +114,12 @@ public final class ModItems { /** Trade-only Artificer gear; ported as a plain item (its custom gear behaviour is deferred). */ public static final RegistryEntry XERTZ_RESONATOR = item("xertz_resonator"); + // --- Machine upgrade modules (the quarry is the first consumer) ---------- + public static final RegistryEntry SPEED_MODULE = module("speed_module", ModuleType.SPEED); + public static final RegistryEntry EFFICIENCY_MODULE = module("efficiency_module", ModuleType.EFFICIENCY); + public static final RegistryEntry FORTUNE_MODULE = module("fortune_module", ModuleType.FORTUNE); + public static final RegistryEntry SILK_TOUCH_MODULE = module("silk_touch_module", ModuleType.SILK_TOUCH); + // --- Rockets (one item per tier; deploys a RocketEntity onto a launch pad) ---- public static final RegistryEntry ROCKET_TIER_1 = ITEMS.register("rocket_tier_1", key -> new RocketItem(new Item.Properties().stacksTo(1).setId(key), RocketTier.TIER_1)); @@ -207,6 +218,10 @@ private static RegistryEntry spawnEgg(String name, RegistryEntry new NerospaceSpawnEggItem(new Item.Properties().setId(key), type)); } + private static RegistryEntry module(String name, ModuleType type) { + return ITEMS.register(name, key -> new UpgradeModuleItem(new Item.Properties().setId(key), type)); + } + private static TagKey cTag(String path) { return TagKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath("c", path)); } @@ -251,7 +266,9 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get())); + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), + SPEED_MODULE.get(), EFFICIENCY_MODULE.get(), FORTUNE_MODULE.get(), SILK_TOUCH_MODULE.get(), + CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get())); } /** diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/AbstractStorageBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/AbstractStorageBlock.java new file mode 100644 index 0000000..d2b5e5c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/AbstractStorageBlock.java @@ -0,0 +1,22 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Shared base for the passive storage endpoints (Battery / Fluid Tank / Gas Tank / Item Store and their + * creative variants): plain model cubes with a block entity and no ticker — pipes and machines move + * resources in and out through the mod's capabilities/lookups. + */ +public abstract class AbstractStorageBlock extends BaseEntityBlock { + + protected AbstractStorageBlock(Properties properties) { + super(properties); + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlock.java new file mode 100644 index 0000000..3da1539 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlock.java @@ -0,0 +1,30 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Creative Fluid Tank block — holds a {@link CreativeFluidTankBlockEntity}. */ +public class CreativeFluidTankBlock extends AbstractStorageBlock { + + public static final MapCodec CODEC = simpleCodec(CreativeFluidTankBlock::new); + + public CreativeFluidTankBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CreativeFluidTankBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlockEntity.java new file mode 100644 index 0000000..d4fbccc --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeFluidTankBlockEntity.java @@ -0,0 +1,49 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; + +import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Creative Fluid Tank — an endless source and sink of {@code rocket_fuel} for testing fluid logistics. */ +public class CreativeFluidTankBlockEntity extends BlockEntity { + + private static final NerospaceFluidStorage INFINITE = new NerospaceFluidStorage() { + @Override + public Fluid getFluid() { + return (Fluid) ModFluids.ROCKET_FUEL.get(); + } + + @Override + public long getAmount() { + return Integer.MAX_VALUE; + } + + @Override + public long getCapacity() { + return Integer.MAX_VALUE; + } + + @Override + public long fill(Fluid fluid, long amount, boolean simulate) { + return Math.max(0, amount); // accepts (voids) anything + } + + @Override + public long drain(long amount, boolean simulate) { + return Math.max(0, amount); // endless rocket fuel + } + }; + + public CreativeFluidTankBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.CREATIVE_FLUID_TANK.get(), pos, state); + } + + public NerospaceFluidStorage getTank() { + return INFINITE; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlock.java new file mode 100644 index 0000000..7da6c42 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlock.java @@ -0,0 +1,30 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +/** Creative Gas Tank block — holds a {@link CreativeGasTankBlockEntity}. */ +public class CreativeGasTankBlock extends AbstractStorageBlock { + + public static final MapCodec CODEC = simpleCodec(CreativeGasTankBlock::new); + + public CreativeGasTankBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CreativeGasTankBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlockEntity.java new file mode 100644 index 0000000..311adb7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeGasTankBlockEntity.java @@ -0,0 +1,48 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import za.co.neroland.nerospace.gas.GasResource; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** Creative Gas Tank — an endless source and sink of oxygen for testing gas logistics. */ +public class CreativeGasTankBlockEntity extends BlockEntity { + + private static final NerospaceGasStorage INFINITE = new NerospaceGasStorage() { + @Override + public GasResource getGas() { + return GasResource.OXYGEN; + } + + @Override + public long getAmount() { + return Integer.MAX_VALUE; + } + + @Override + public long getCapacity() { + return Integer.MAX_VALUE; + } + + @Override + public long fill(GasResource gas, long amount, boolean simulate) { + return Math.max(0, amount); // accepts (voids) anything + } + + @Override + public long drain(long amount, boolean simulate) { + return Math.max(0, amount); // endless oxygen + } + }; + + public CreativeGasTankBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.CREATIVE_GAS_TANK.get(), pos, state); + } + + public NerospaceGasStorage getTank() { + return INFINITE; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlock.java new file mode 100644 index 0000000..45efc6e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlock.java @@ -0,0 +1,60 @@ +package za.co.neroland.nerospace.storage; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +/** + * Creative Item Store block — holds a {@link CreativeItemStoreBlockEntity}. Right-click with an item to + * set the endless source; sneak-right-click (empty hand) to clear it. + */ +public class CreativeItemStoreBlock extends AbstractStorageBlock { + + public static final MapCodec CODEC = simpleCodec(CreativeItemStoreBlock::new); + + public CreativeItemStoreBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new CreativeItemStoreBlockEntity(pos, state); + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + if (!level.isClientSide() && level.getBlockEntity(pos) instanceof CreativeItemStoreBlockEntity store) { + store.setSource(stack); + player.sendSystemMessage(Component.translatable( + "block.nerospace.creative_item_store.set", stack.getHoverName())); + } + return InteractionResult.SUCCESS; + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && level.getBlockEntity(pos) instanceof CreativeItemStoreBlockEntity store) { + store.setSource(ItemStack.EMPTY); + player.sendSystemMessage(Component.translatable("block.nerospace.creative_item_store.cleared")); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlockEntity.java new file mode 100644 index 0000000..4353e0d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/storage/CreativeItemStoreBlockEntity.java @@ -0,0 +1,101 @@ +package za.co.neroland.nerospace.storage; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.Container; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Creative Item Store: an endless source of one configured item, exposed as a single-slot + * {@link Container} (so pipes/hoppers pull from it through the mod's item capability). Right-click with + * an item to choose it; sneak-right-click to clear. Extraction never depletes the source; insertion is + * rejected. + * + *

Cross-loader port note: the root used the NeoForge-transfer {@code InfiniteResourceHandler}; the + * multiloader exposes the same endless source through a vanilla Container view.

+ */ +public class CreativeItemStoreBlockEntity extends BlockEntity implements Container { + + private ItemStack source = ItemStack.EMPTY; + + public CreativeItemStoreBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.CREATIVE_ITEM_STORE.get(), pos, state); + } + + public ItemStack source() { + return this.source; + } + + public void setSource(ItemStack stack) { + this.source = stack.isEmpty() ? ItemStack.EMPTY : stack.copyWithCount(1); + setChanged(); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.store("Source", ItemStack.OPTIONAL_CODEC, this.source); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.source = input.read("Source", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY); + } + + // --- Container: a single endless-source slot -------------------------------- + + @Override + public int getContainerSize() { + return 1; + } + + @Override + public boolean isEmpty() { + return this.source.isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.source.isEmpty() ? ItemStack.EMPTY : this.source.copyWithCount(this.source.getMaxStackSize()); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + if (this.source.isEmpty()) { + return ItemStack.EMPTY; + } + return this.source.copyWithCount(Math.min(amount, this.source.getMaxStackSize())); // endless — never depletes + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return getItem(slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + // Source is configured by right-click, not by insertion; ignore. + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return false; // a source, not a sink + } + + @Override + public boolean stillValid(Player player) { + return this.level != null && this.level.getBlockEntity(this.worldPosition) == this; + } + + @Override + public void clearContent() { + this.source = ItemStack.EMPTY; + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_fluid_tank.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_fluid_tank.json new file mode 100644 index 0000000..150211e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_fluid_tank.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/creative_fluid_tank" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_gas_tank.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_gas_tank.json new file mode 100644 index 0000000..c5aec92 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_gas_tank.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/creative_gas_tank" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_item_store.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_item_store.json new file mode 100644 index 0000000..1457279 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/creative_item_store.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/creative_item_store" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/creative_fluid_tank.json b/multiloader/common/src/main/resources/assets/nerospace/items/creative_fluid_tank.json new file mode 100644 index 0000000..bc89b69 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/creative_fluid_tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/creative_fluid_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/creative_gas_tank.json b/multiloader/common/src/main/resources/assets/nerospace/items/creative_gas_tank.json new file mode 100644 index 0000000..d4b85f1 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/creative_gas_tank.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/creative_gas_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/creative_item_store.json b/multiloader/common/src/main/resources/assets/nerospace/items/creative_item_store.json new file mode 100644 index 0000000..0b054b9 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/creative_item_store.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/creative_item_store" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/efficiency_module.json b/multiloader/common/src/main/resources/assets/nerospace/items/efficiency_module.json new file mode 100644 index 0000000..5a85e8b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/efficiency_module.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/efficiency_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/fortune_module.json b/multiloader/common/src/main/resources/assets/nerospace/items/fortune_module.json new file mode 100644 index 0000000..35aa1ae --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/fortune_module.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/fortune_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/silk_touch_module.json b/multiloader/common/src/main/resources/assets/nerospace/items/silk_touch_module.json new file mode 100644 index 0000000..b65939f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/silk_touch_module.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/silk_touch_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/speed_module.json b/multiloader/common/src/main/resources/assets/nerospace/items/speed_module.json new file mode 100644 index 0000000..dd51c6f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/speed_module.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/speed_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index f651d9c..15a7b6f 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -10,6 +10,11 @@ "block.nerospace.combustion_generator": "Combustion Generator", "block.nerospace.cracked_alien_bricks": "Cracked Alien Bricks", "block.nerospace.creative_battery": "Creative Battery", + "block.nerospace.creative_fluid_tank": "Creative Fluid Tank", + "block.nerospace.creative_gas_tank": "Creative Gas Tank", + "block.nerospace.creative_item_store": "Creative Item Store", + "block.nerospace.creative_item_store.cleared": "Source cleared", + "block.nerospace.creative_item_store.set": "Source set: %s", "block.nerospace.deepslate_nerosium_ore": "Deepslate Nerosium Ore", "block.nerospace.fluid_tank": "Fluid Tank", "block.nerospace.fuel_refinery": "Fuel Refinery", @@ -87,7 +92,9 @@ "item.nerospace.cindrite": "Cindrite", "item.nerospace.destination_compass.travel": "Travelling to %s", "item.nerospace.drift_fleece": "Drift Fleece", + "item.nerospace.efficiency_module": "Efficiency Module", "item.nerospace.ember_strutter_spawn_egg": "Ember Strutter Spawn Egg", + "item.nerospace.fortune_module": "Fortune Module", "item.nerospace.frame_casing": "Frame Casing", "item.nerospace.frost_strider_spawn_egg": "Frost Strider Spawn Egg", "item.nerospace.glacira_compass": "Glacira Compass", @@ -133,6 +140,8 @@ "item.nerospace.rocket_tier_2": "Tier 2 Rocket", "item.nerospace.rocket_tier_3": "Tier 3 Rocket", "item.nerospace.rocket_tier_4": "Tier 4 Rocket", + "item.nerospace.silk_touch_module": "Silk Touch Module", + "item.nerospace.speed_module": "Speed Module", "item.nerospace.station_compass": "Station Compass", "item.nerospace.woolly_drift_spawn_egg": "Woolly Drift Spawn Egg", "item.nerospace.xertz_quartz": "Xertz Quartz", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_fluid_tank.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_fluid_tank.json new file mode 100644 index 0000000..ca36ebc --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_fluid_tank.json @@ -0,0 +1,425 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#core" + }, + "east": { + "texture": "#core" + }, + "north": { + "texture": "#core" + }, + "south": { + "texture": "#core" + }, + "up": { + "texture": "#core" + }, + "west": { + "texture": "#core" + } + }, + "from": [ + 2, + 2, + 2 + ], + "to": [ + 14, + 14, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 2, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 14 + ], + "to": [ + 2, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 0 + ], + "to": [ + 14, + 2, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 14 + ], + "to": [ + 14, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 2 + ], + "to": [ + 2, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 2 + ], + "to": [ + 16, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 0 + ], + "to": [ + 14, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 14 + ], + "to": [ + 14, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "core": "nerospace:block/creative_fluid_tank_core", + "particle": "nerospace:block/creative_fluid_tank", + "side": "nerospace:block/creative_fluid_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_gas_tank.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_gas_tank.json new file mode 100644 index 0000000..8214bbf --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_gas_tank.json @@ -0,0 +1,425 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#core" + }, + "east": { + "texture": "#core" + }, + "north": { + "texture": "#core" + }, + "south": { + "texture": "#core" + }, + "up": { + "texture": "#core" + }, + "west": { + "texture": "#core" + } + }, + "from": [ + 2, + 2, + 2 + ], + "to": [ + 14, + 14, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 2, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 0 + ], + "to": [ + 16, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 14 + ], + "to": [ + 2, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 14 + ], + "to": [ + 16, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 0 + ], + "to": [ + 14, + 2, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 0, + 14 + ], + "to": [ + 14, + 2, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 2 + ], + "to": [ + 2, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 0, + 2 + ], + "to": [ + 16, + 2, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 0 + ], + "to": [ + 14, + 16, + 2 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 2, + 14, + 14 + ], + "to": [ + 14, + 16, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 14, + 2 + ], + "to": [ + 2, + 16, + 14 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 14, + 14, + 2 + ], + "to": [ + 16, + 16, + 14 + ] + } + ], + "textures": { + "core": "nerospace:block/creative_gas_tank_core", + "particle": "nerospace:block/creative_gas_tank", + "side": "nerospace:block/creative_gas_tank" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_item_store.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_item_store.json new file mode 100644 index 0000000..c22d59a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/creative_item_store.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/creative_item_store" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/efficiency_module.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/efficiency_module.json new file mode 100644 index 0000000..dfa19d3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/efficiency_module.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/efficiency_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/fortune_module.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/fortune_module.json new file mode 100644 index 0000000..b0fd1ea --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/fortune_module.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/fortune_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/silk_touch_module.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/silk_touch_module.json new file mode 100644 index 0000000..0928d44 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/silk_touch_module.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/silk_touch_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/speed_module.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/speed_module.json new file mode 100644 index 0000000..b59484c --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/speed_module.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/speed_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ba8f9c5d2ad1c00dbd77e492ca3b2d8ff3e345 GIT binary patch literal 297 zcmV+^0oMMBP)8fWt%}5EY1h9|%Z!DMPKMYy0y0C+V8o z;rhge(E`BnaRC5`2y)I))gs6_hgn3**|ZFA0~Wgpe{-OSz|6|JtzDVIg?wJ?1A$Yl z5%`)Z&;f~W<=Jyo)k3jGli1zNN&(N^9sqE=;&R;Rk9(#We$00000NkvXXu0mjf_6>oF literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank_core.png new file mode 100644 index 0000000000000000000000000000000000000000..5052b0fbcf35ff7ba1267953855b9f989bbea939 GIT binary patch literal 306 zcmV-20nPr2P)7UG&i e2Y}nF&-n+HOV2yLO^DC{0000hePF|v^u)RZk?$*h`uj08B?9dUo>S=Kvn%0=dEZa}HN_iu zHnoP|6vfqKJ@mE;1DsN81sZT{`(GdKi?Q@b2Nexa;^8)LjJ4Rx&lOaXf_t;E zUN2FAT~Tw-dnV%% zhB=828fGh=H8q+uDDyCtwHjv3DN5dvxQ)A6ZVm&(;dPP<3Fn(afYvd1y85}Sb4q9e E0AfNmjsO4v literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/fortune_module.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/fortune_module.png new file mode 100644 index 0000000000000000000000000000000000000000..21bb95f0735cc670c3283dc7682e2bdd04e3ae17 GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`iJmTwAr*6y6C^SYbevgv4A;>~h%7#JetB^1=}F3Sg6#^CAd=d#Wzp$P!0 CtT$2s literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/silk_touch_module.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/silk_touch_module.png new file mode 100644 index 0000000000000000000000000000000000000000..d5cdac8362c16edca3e4cc744e24ae96458ddcda GIT binary patch literal 154 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`iJmTwAr*6y6C^SYbeyT&_Vj;! zl8NU}gF|08F7UPro$>hUp7oDaxaN0CNPlGH5r43WbNcBIyBsdf-oX7q>O+loE1Ljw zK^x=38w+M}v)yA@<5_uq{%M{(om~rH=HdNAnEKE)h|FP=4i=+$9!VC;4oDvFerfyjRw2r~k)z4*}Q$iB} DU*a}@ literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_fluid_tank.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_fluid_tank.json new file mode 100644 index 0000000..e53a77a --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_fluid_tank.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:creative_fluid_tank" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/creative_fluid_tank" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_gas_tank.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_gas_tank.json new file mode 100644 index 0000000..1217f55 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_gas_tank.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:creative_gas_tank" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/creative_gas_tank" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_item_store.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_item_store.json new file mode 100644 index 0000000..034a863 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/creative_item_store.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:creative_item_store" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/creative_item_store" +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 5e2542a..8d4f743 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -131,6 +131,17 @@ public void onInitialize() { (be, direction) -> be.getEnergy(), ModBlockEntities.CREATIVE_BATTERY.get()); + // Creative storage: endless sources/sinks for testing logistics. + FLUID.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.CREATIVE_FLUID_TANK.get()); + GAS.registerForBlockEntity( + (be, direction) -> be.getTank(), + ModBlockEntities.CREATIVE_GAS_TANK.get()); + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.CREATIVE_ITEM_STORE.get()); + // Fuel Tank: fluid out (pipes), canister in (hoppers/pipes). FLUID.registerForBlockEntity( (be, direction) -> be.getTank(), diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 73fc8eb..be9567a 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -152,6 +152,20 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ModBlockEntities.CREATIVE_BATTERY.get(), (be, side) -> be.getEnergy()); + // Creative storage: endless sources/sinks for testing logistics. + event.registerBlockEntity( + FLUID, + ModBlockEntities.CREATIVE_FLUID_TANK.get(), + (be, side) -> be.getTank()); + event.registerBlockEntity( + GAS, + ModBlockEntities.CREATIVE_GAS_TANK.get(), + (be, side) -> be.getTank()); + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.CREATIVE_ITEM_STORE.get(), + (be, side) -> VanillaContainerWrapper.of(be)); + // Fuel Tank: fluid out (pipes), canister in (hoppers/pipes). event.registerBlockEntity( FLUID, From 5dbbbcdb859e775b40308ee5c7c176faab48ea63 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 01:11:26 +0200 Subject: [PATCH 41/82] Add per-player oxygen system & attachments Implements a simplified cross-loader oxygen/atmosphere survival core. Adds OxygenManager (per-player O2 drain/refill, suit handling, air-supply mirroring), Fabric and NeoForge attachment registrations, and platform helper getters/setters for oxygen via IPlatformHelper. Registers ticking hooks for Fabric and NeoForge, wires ModDataComponents into ModRegistries, and adds ModTags plus a language key for the no-oxygen message. Also updates the port checklist doc to mark the oxygen core progress and inlines the cross-loader design notes; diffusion/field networking and other advanced features are deferred. --- docs/MULTILOADER_PORT_CHECKLIST.md | 28 +++- .../nerospace/platform/IPlatformHelper.java | 10 ++ .../nerospace/registry/ModDataComponents.java | 45 ++++++ .../nerospace/registry/ModRegistries.java | 1 + .../neroland/nerospace/registry/ModTags.java | 94 ++++++++++++ .../nerospace/world/OxygenManager.java | 145 ++++++++++++++++++ .../assets/nerospace/lang/en_us.json | 3 +- .../nerospace/fabric/FabricAttachments.java | 31 ++++ .../nerospace/fabric/NerospaceFabric.java | 7 + .../platform/FabricPlatformHelper.java | 13 ++ .../neoforge/NeoForgeAttachments.java | 38 +++++ .../nerospace/neoforge/NerospaceNeoForge.java | 12 ++ .../platform/NeoForgePlatformHelper.java | 13 ++ 13 files changed, 432 insertions(+), 8 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDataComponents.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 4cfe9aa..0beeea2 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,7 +1,7 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~134 classes ported, ~130 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~139 classes ported, ~125 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **2026-06-20 update — quarry ported.** All 4 cells green. Added 11 classes: @@ -102,8 +102,13 @@ checked by a headless build). Assets (textures, models, blockstates, loot, recipes) + 4 lang keys copied. ### Atmosphere / terraforming (`world/Oxygen*`, `world/Terraform*`, `machine/Terraform*`, `HydrationModule`) -- [ ] Oxygen field (airless-dimension survival): `OxygenField`, `OxygenFieldManager`, `OxygenFieldEvents`, - oxygen HUD + air-bubble suppression, suit checks. Needs the **networking seam** (sync to client). +- [~] **Oxygen survival core DONE (4 cells green)** — `OxygenManager` (per-player O2 drain/suffocate/refill, + air-supply-bar mirror, full-suit detection) on a new **data-attachment seam**: `IPlatformHelper.get/setOxygen` + backed by NeoForge `AttachmentType` (`NeoForgeAttachments`) and Fabric `AttachmentRegistry` + (`FabricAttachments`); ticked per-loader (NeoForge `PlayerTickEvent`, Fabric `ServerTickEvents.END_SERVER_TICK`). + Breathable = near a Launch Pad / Oxygen Generator. **Deferred**: the diffusion `OxygenFieldManager`/ + `OxygenField`/`OxygenFieldEvents` (sealed rooms + client overlay — needs the **networking seam**), + terraform breathability + criteria, hazard shields/feedback, and gas-tank airlock refill. Values inlined. - [ ] Terraformer + Terraform Monitor + Hydration Module (blocks/BE/menus/screens), `TerraformManager`, `TerraformConversion`, `TerraformDrift`, `TerraformFauna`, `TerraformChunkLoader`, `TerraformResources`, `GreenxertzAtmosphere`, terraformed biomes. Risk: **high** (world mutation, chunk-loading, events). @@ -154,10 +159,19 @@ checked by a headless build). - [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. ### Cross-cutting registries (`registry/`) -- [ ] `ModDataComponents`, `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge - attachments vs Fabric component/attachment API), `ModCriteria` (advancement triggers), `ModTags`, - `ModFeatures`, `ModConfiguredFeatures`/`ModPlacedFeatures`/`ModBiomes`/`ModBiomeModifiers` (datagen - bootstraps — mostly superseded by the copied JSON), `ModDimensionTypes` (space type — JSON already copied). +- [x] `ModTags` — pure `TagKey` constants (block + item; c:material + nerospace oxygen/terraform tags), + ported verbatim (no registration; tag membership is data). +- [x] `ModDataComponents` — `SELECTED_PIPE_TYPE` (int) + `FILTER_ITEM` (vanilla `ItemStack` instead of the + root's NeoForge `ItemResource`), via `RegistrationProvider` over `DATA_COMPONENT_TYPE`. Consumed by the + advanced-pipe configurator/filter (advanced pipes batch). +- [~] `ModCriteria` (`terraformed_ground`/`living_ground`/`founded_station` `PlayerTrigger`s) — **deferred**: + the criterion classes resolve to a **different package on 26.2 NeoForm than the root's 26.1 imports** + (`net.minecraft.advancements.criterion` not found), so it needs a version/loader-checked import — resolve + it alongside its first consumer (station founding / star guide / terraform). Orphan until then. +- [ ] `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge attachments vs Fabric + component/attachment API), `ModFeatures`, `ModConfiguredFeatures`/`ModPlacedFeatures`/`ModBiomes`/ + `ModBiomeModifiers` (datagen bootstraps — mostly superseded by the copied JSON), `ModDimensionTypes` + (space type — JSON already copied). - [x] `ModCreativeModeTabs` → ported as `ModCreativeTab`: a **dedicated "Nerospace" tab** registered via the cross-loader `RegistrationProvider` over the vanilla `CREATIVE_MODE_TAB` registry, listing all items (`ModItems.creativeContents()`). **Fixes a latent runtime bug**: the earlier per-loader injection diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java index e6e5762..e6b8e2a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java @@ -25,4 +25,14 @@ public interface IPlatformHelper { /** True on the physical client (renderers, screens, HUD available). */ boolean isClient(); + + // --- Per-player oxygen (data-attachment seam) --------------------------- + // NeoForge backs this with an AttachmentType registered via DeferredRegister; Fabric with the + // data-attachment API. The value defaults to {@code OxygenManager.OXYGEN_MAX} and persists. + + /** The player's stored oxygen (millibuckets-of-air units), defaulting to full if unset. */ + int getOxygen(net.minecraft.world.entity.player.Player player); + + /** Stores the player's oxygen. */ + void setOxygen(net.minecraft.world.entity.player.Player player, int value); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDataComponents.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDataComponents.java new file mode 100644 index 0000000..1a6ff23 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModDataComponents.java @@ -0,0 +1,45 @@ +package za.co.neroland.nerospace.registry; + +import com.mojang.serialization.Codec; + +import net.minecraft.core.component.DataComponentType; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Item data components, ported cross-loader through {@link RegistrationProvider} over the vanilla + * {@code DATA_COMPONENT_TYPE} registry (the root used a NeoForge {@code DeferredRegister.DataComponents}). + * + *

Cross-loader port note: the root's {@code FILTER_ITEM} stored a NeoForge-transfer {@code ItemResource}; + * the multiloader uses a vanilla {@link ItemStack} (the advanced-pipe filter that consumes it is ported on + * the cross-loader item model). These back the Configurator + Pipe Filter (advanced pipes batch).

+ */ +public final class ModDataComponents { + + public static final RegistrationProvider> COMPONENTS = + RegistrationProvider.get(Registries.DATA_COMPONENT_TYPE, NerospaceCommon.MOD_ID); + + /** Index into the pipe resource layers — which layer the Configurator is editing. */ + public static final RegistryEntry> SELECTED_PIPE_TYPE = + COMPONENTS.register("selected_pipe_type", key -> DataComponentType.builder() + .persistent(Codec.intRange(0, 3)) + .networkSynchronized(ByteBufCodecs.VAR_INT) + .build()); + + /** The item a Pipe Filter is set to (applied to pipe faces to restrict the item layer). */ + public static final RegistryEntry> FILTER_ITEM = + COMPONENTS.register("filter_item", key -> DataComponentType.builder() + .persistent(ItemStack.CODEC) + .networkSynchronized(ItemStack.STREAM_CODEC) + .build()); + + private ModDataComponents() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index 5d13b4c..e935326 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -13,6 +13,7 @@ private ModRegistries() { public static void init() { ModSounds.init(); + ModDataComponents.init(); za.co.neroland.nerospace.fluid.ModFluids.init(); ModBlocks.init(); ModItems.init(); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java new file mode 100644 index 0000000..78c55ad --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java @@ -0,0 +1,94 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.block.Block; + +/** + * Custom tag keys for Nerospace. Cross-mod material tags live in the {@code c} namespace (unified with + * Fabric); mod-specific behaviour tags (oxygen sealing, terraform conversion) live in {@code nerospace}. + * Pure {@link TagKey} constants — tag membership is data ({@code tags/**.json}). + */ +public final class ModTags { + + private ModTags() { + } + + private static TagKey blockTag(String namespace, String path) { + return TagKey.create(Registries.BLOCK, Identifier.fromNamespaceAndPath(namespace, path)); + } + + private static TagKey itemTag(String namespace, String path) { + return TagKey.create(Registries.ITEM, Identifier.fromNamespaceAndPath(namespace, path)); + } + + public static final class Blocks { + + private Blocks() { + } + + public static final TagKey ORES_NEROSIUM = blockTag("c", "ores/nerosium"); + public static final TagKey STORAGE_BLOCKS_NEROSIUM = blockTag("c", "storage_blocks/nerosium"); + public static final TagKey STORAGE_BLOCKS_RAW_NEROSIUM = blockTag("c", "storage_blocks/raw_nerosium"); + + public static final TagKey ORES_NEROSTEEL = blockTag("c", "ores/nerosteel"); + public static final TagKey ORES_XERTZ_QUARTZ = blockTag("c", "ores/xertz_quartz"); + public static final TagKey STORAGE_BLOCKS_NEROSTEEL = blockTag("c", "storage_blocks/nerosteel"); + + public static final TagKey ORES_CINDRITE = blockTag("c", "ores/cindrite"); + public static final TagKey STORAGE_BLOCKS_CINDRITE = blockTag("c", "storage_blocks/cindrite"); + + public static final TagKey ORES_GLACITE = blockTag("c", "ores/glacite"); + public static final TagKey STORAGE_BLOCKS_GLACITE = blockTag("c", "storage_blocks/glacite"); + + // --- Oxygen field (terraform design) ------------------------------- + /** Full, airtight blocks: stop ALL oxygen flow (opaque cubes, glass, station walls). */ + public static final TagKey OXYGEN_SEALING = blockTag("nerospace", "oxygen_sealing"); + /** Non-full / leaky blocks: allow PARTIAL flow (fences, slabs, torches, open trapdoors). */ + public static final TagKey OXYGEN_LEAKS = blockTag("nerospace", "oxygen_leaks"); + /** Blocks that act as oxygen sources for the field (generators; later: alien flora). */ + public static final TagKey OXYGEN_SOURCE = blockTag("nerospace", "oxygen_source"); + + // --- Terraform conversion table (data-driven) ---------------------- + /** Surface blocks a Terraformer turns into grass (deadrock, basalt, dirt, sand, …). */ + public static final TagKey TERRAFORM_TO_GRASS = blockTag("nerospace", "terraform_to_grass"); + /** Sub-surface blocks a Terraformer turns into dirt. */ + public static final TagKey TERRAFORM_TO_DIRT = blockTag("nerospace", "terraform_to_dirt"); + } + + public static final class Items { + + private Items() { + } + + public static final TagKey ORES_NEROSIUM = itemTag("c", "ores/nerosium"); + public static final TagKey INGOTS_NEROSIUM = itemTag("c", "ingots/nerosium"); + public static final TagKey DUSTS_NEROSIUM = itemTag("c", "dusts/nerosium"); + public static final TagKey RAW_MATERIALS_NEROSIUM = itemTag("c", "raw_materials/nerosium"); + public static final TagKey STORAGE_BLOCKS_NEROSIUM = itemTag("c", "storage_blocks/nerosium"); + public static final TagKey STORAGE_BLOCKS_RAW_NEROSIUM = itemTag("c", "storage_blocks/raw_nerosium"); + + public static final TagKey ORES_NEROSTEEL = itemTag("c", "ores/nerosteel"); + public static final TagKey ORES_XERTZ_QUARTZ = itemTag("c", "ores/xertz_quartz"); + public static final TagKey INGOTS_NEROSTEEL = itemTag("c", "ingots/nerosteel"); + public static final TagKey RAW_MATERIALS_NEROSTEEL = itemTag("c", "raw_materials/nerosteel"); + public static final TagKey STORAGE_BLOCKS_NEROSTEEL = itemTag("c", "storage_blocks/nerosteel"); + public static final TagKey GEMS_XERTZ_QUARTZ = itemTag("c", "gems/xertz_quartz"); + + public static final TagKey GEMS_CINDRITE = itemTag("c", "gems/cindrite"); + public static final TagKey ORES_CINDRITE = itemTag("c", "ores/cindrite"); + public static final TagKey STORAGE_BLOCKS_CINDRITE = itemTag("c", "storage_blocks/cindrite"); + + public static final TagKey GEMS_GLACITE = itemTag("c", "gems/glacite"); + public static final TagKey ORES_GLACITE = itemTag("c", "ores/glacite"); + public static final TagKey STORAGE_BLOCKS_GLACITE = itemTag("c", "storage_blocks/glacite"); + + /** Items the Hydration Module melts into hydration units for the Terraformer's water stage. */ + public static final TagKey HYDRATION_INPUT = itemTag("nerospace", "hydration_input"); + + /** Tiered "alien" meteor loot, grouped for the future scanner/upgrade system. */ + public static final TagKey ALIEN_MATERIALS = itemTag("nerospace", "alien_materials"); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java new file mode 100644 index 0000000..9218722 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java @@ -0,0 +1,145 @@ +package za.co.neroland.nerospace.world; + +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import za.co.neroland.nerospace.platform.Services; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Oxygen / atmosphere survival (Phase 8c, simplified cross-loader port). On airless Nerospace + * dimensions a survival player carries a finite oxygen supply (a per-player data attachment, accessed + * through the {@link Services#PLATFORM} seam) that drains while exposed and refills inside a breathable + * zone — near a Rocket Launch Pad (the landing site) or an Oxygen Generator. A worn Oxygen Suit grants a + * larger tank and far slower drain. At zero oxygen the player suffocates. Remaining oxygen is mirrored + * onto the vanilla air-supply bar so the bubble HUD shows it for free. + * + *

Cross-loader port note. The root drives this from a NeoForge {@code PlayerTickEvent} and a + * full diffusion {@code OxygenFieldManager} (sealed rooms + client overlay, networking-synced), plus + * terraform breathability, hazard shields, and gas-tank airlock refills. The multiloader ticks it from a + * per-loader server-tick hook and keeps the self-contained survival core; the diffusion field, terraform, + * hazard variants, advancement criteria, and gas-airlock refill are deferred to their own batches. Values + * are inlined (the config seam is deferred).

+ */ +public final class OxygenManager { + + /** Default / bare-lungs oxygen capacity (also the attachment default — keep both loaders in sync). */ + public static final int OXYGEN_MAX = 300; + /** A full Oxygen Suit's larger air tank. */ + public static final int OXYGEN_SUIT_MAX = 900; + + private static final int CHECK_INTERVAL_TICKS = 10; + private static final int DAMAGE_INTERVAL_TICKS = 40; + /** Bare-lungs drain per check (exposed) — ~a few seconds of air. */ + private static final int BARE_DRAIN_PER_CHECK = 30; + /** Suited drain per check (the suit's tank lasts far longer). */ + private static final int SUIT_DRAIN_PER_CHECK = 3; + /** Breathable-zone scan radius around the player (launch pad / oxygen generator). */ + private static final int SAFE_RADIUS = 6; + private static final float SUFFOCATION_DAMAGE = 1.0F; + + /** Every Nerospace planet (and the vacuum of the station) is airless. */ + private static final Set> PLANETS = Set.of( + ModDimensions.GREENXERTZ_LEVEL, ModDimensions.CINDARA_LEVEL, + ModDimensions.STATION_LEVEL, ModDimensions.GLACIRA_LEVEL); + + private OxygenManager() { + } + + /** Per-player server tick (called from each loader's server-tick hook). */ + public static void tick(ServerPlayer player) { + if (!(player.level() instanceof ServerLevel level)) { + return; + } + + boolean suited = isFullSuit(player); + int max = suited ? OXYGEN_SUIT_MAX : OXYGEN_MAX; + + boolean airless = PLANETS.contains(level.dimension()) + && !player.getAbilities().instabuild + && !player.isSpectator(); + if (!airless) { + Services.PLATFORM.setOxygen(player, max); + mirrorToAirSupply(player, max, max); + return; + } + + int oxygen = Math.min(Services.PLATFORM.getOxygen(player), max); + + if (player.tickCount % CHECK_INTERVAL_TICKS == 0) { + if (isBreathable(level, player.blockPosition())) { + oxygen = max; + } else { + oxygen = Math.max(0, oxygen - (suited ? SUIT_DRAIN_PER_CHECK : BARE_DRAIN_PER_CHECK)); + } + Services.PLATFORM.setOxygen(player, oxygen); + } + + mirrorToAirSupply(player, oxygen, max); + + if (oxygen <= 0 && player.tickCount % DAMAGE_INTERVAL_TICKS == 0) { + player.hurtServer(level, level.damageSources().generic(), SUFFOCATION_DAMAGE); + if (player.tickCount % (DAMAGE_INTERVAL_TICKS * 3) == 0) { + player.sendSystemMessage(Component.translatable("message.nerospace.greenxertz.no_air")); + } + } + } + + /** Maps oxygen onto the vanilla air-supply bar (full oxygen → no bubbles shown). */ + private static void mirrorToAirSupply(Player player, int oxygen, int max) { + int airMax = player.getMaxAirSupply(); + int air = (int) ((long) oxygen * airMax / Math.max(1, max)); + player.setAirSupply(Math.min(airMax, Math.max(0, air))); + } + + /** A breathable zone: within {@link #SAFE_RADIUS} of a Rocket Launch Pad or an Oxygen Generator. */ + private static boolean isBreathable(Level level, BlockPos center) { + for (BlockPos pos : BlockPos.betweenClosed( + center.offset(-SAFE_RADIUS, -SAFE_RADIUS, -SAFE_RADIUS), + center.offset(SAFE_RADIUS, SAFE_RADIUS, SAFE_RADIUS))) { + BlockState state = level.getBlockState(pos); + if (state.is(ModBlocks.ROCKET_LAUNCH_PAD.get()) || state.is(ModBlocks.OXYGEN_GENERATOR.get())) { + return true; + } + } + return false; + } + + /** Whether the player wears a full set of Oxygen Suit pieces (any tier / hazard variant counts). */ + private static boolean isFullSuit(Player player) { + return isSuitPiece(player.getItemBySlot(EquipmentSlot.HEAD), + ModItems.OXYGEN_SUIT_HELMET.get(), ModItems.OXYGEN_SUIT_T2_HELMET.get(), + ModItems.OXYGEN_SUIT_HEAT_HELMET.get(), ModItems.OXYGEN_SUIT_COLD_HELMET.get()) + && isSuitPiece(player.getItemBySlot(EquipmentSlot.CHEST), + ModItems.OXYGEN_SUIT_CHESTPLATE.get(), ModItems.OXYGEN_SUIT_T2_CHESTPLATE.get(), + ModItems.OXYGEN_SUIT_HEAT_CHESTPLATE.get(), ModItems.OXYGEN_SUIT_COLD_CHESTPLATE.get()) + && isSuitPiece(player.getItemBySlot(EquipmentSlot.LEGS), + ModItems.OXYGEN_SUIT_LEGGINGS.get(), ModItems.OXYGEN_SUIT_T2_LEGGINGS.get(), + ModItems.OXYGEN_SUIT_HEAT_LEGGINGS.get(), ModItems.OXYGEN_SUIT_COLD_LEGGINGS.get()) + && isSuitPiece(player.getItemBySlot(EquipmentSlot.FEET), + ModItems.OXYGEN_SUIT_BOOTS.get(), ModItems.OXYGEN_SUIT_T2_BOOTS.get(), + ModItems.OXYGEN_SUIT_HEAT_BOOTS.get(), ModItems.OXYGEN_SUIT_COLD_BOOTS.get()); + } + + private static boolean isSuitPiece(ItemStack worn, Item... options) { + for (Item option : options) { + if (worn.is(option)) { + return true; + } + } + return false; + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 15a7b6f..ef0f458 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -147,5 +147,6 @@ "item.nerospace.xertz_quartz": "Xertz Quartz", "item.nerospace.xertz_resonator": "Xertz Resonator", "item.nerospace.xertz_stalker_spawn_egg": "Xertz Stalker Spawn Egg", - "itemGroup.nerospace": "Nerospace" + "itemGroup.nerospace": "Nerospace", + "message.nerospace.greenxertz.no_air": "You are out of oxygen — reach a launch pad or an Oxygen Generator!" } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java new file mode 100644 index 0000000..cf8bbad --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java @@ -0,0 +1,31 @@ +package za.co.neroland.nerospace.fabric; + +import com.mojang.serialization.Codec; + +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.world.OxygenManager; + +/** + * Fabric side of the data-attachment seam (the NeoForge side uses {@code DeferredRegister} over + * {@code ATTACHMENT_TYPES}). Oxygen persists across logout and copies on death; it defaults to + * {@link OxygenManager#OXYGEN_MAX}. + */ +public final class FabricAttachments { + + public static final AttachmentType OXYGEN = AttachmentRegistry.builder() + .initializer(() -> OxygenManager.OXYGEN_MAX) + .persistent(Codec.INT) + .copyOnDeath() + .buildAndRegister(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "oxygen")); + + private FabricAttachments() { + } + + /** Touch to force class-load (and thus registration) at mod init. */ + public static void init() { + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 8d4f743..fab977b 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -1,6 +1,7 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; import net.fabricmc.fabric.api.lookup.v1.block.BlockApiLookup; @@ -20,6 +21,7 @@ import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModEntityAttributes; +import za.co.neroland.nerospace.world.OxygenManager; /** * Fabric entry point. Shared init registers content eagerly, then Fabric-side @@ -60,6 +62,11 @@ public void onInitialize() { // Default attributes for the ported mobs (counterpart to NeoForge's EntityAttributeCreationEvent). ModEntityAttributes.forEach(FabricDefaultAttributeRegistry::register); + // Oxygen survival: register the attachment + tick each player per world tick (airless-planet drain). + FabricAttachments.init(); + ServerTickEvents.END_SERVER_TICK.register(server -> + server.getPlayerList().getPlayers().forEach(OxygenManager::tick)); + // Item-storage capability (Fabric Transfer API) — counterpart to NeoForge // Capabilities.Item.BLOCK; lets mod pipes move items in/out of the item store. ItemStorage.SIDED.registerForBlockEntity( diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java index 28a6dd6..a73208d 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java @@ -2,6 +2,9 @@ import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.world.entity.player.Player; + +import za.co.neroland.nerospace.fabric.FabricAttachments; /** * Fabric implementation of {@link IPlatformHelper}. Registered via @@ -28,4 +31,14 @@ public boolean isModLoaded(String modId) { public boolean isClient() { return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; } + + @Override + public int getOxygen(Player player) { + return player.getAttachedOrCreate(FabricAttachments.OXYGEN); + } + + @Override + public void setOxygen(Player player, int value) { + player.setAttached(FabricAttachments.OXYGEN, value); + } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java new file mode 100644 index 0000000..c1804c2 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java @@ -0,0 +1,38 @@ +package za.co.neroland.nerospace.neoforge; + +import java.util.function.Supplier; + +import com.mojang.serialization.Codec; + +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.neoforge.registries.NeoForgeRegistries; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.world.OxygenManager; + +/** + * NeoForge side of the data-attachment seam. The common {@code IPlatformHelper#getOxygen/setOxygen} + * delegate here (the Fabric side uses the fabric data-attachment API). Oxygen persists across logout and + * copies on death; it defaults to {@link OxygenManager#OXYGEN_MAX}. + */ +public final class NeoForgeAttachments { + + public static final DeferredRegister> ATTACHMENT_TYPES = + DeferredRegister.create(NeoForgeRegistries.Keys.ATTACHMENT_TYPES, NerospaceCommon.MOD_ID); + + public static final Supplier> OXYGEN = ATTACHMENT_TYPES.register( + "oxygen", + () -> AttachmentType.builder(() -> OxygenManager.OXYGEN_MAX) + .serialize(Codec.INT.fieldOf("oxygen")) + .copyOnDeath() + .build()); + + private NeoForgeAttachments() { + } + + public static void register(IEventBus modEventBus) { + ATTACHMENT_TYPES.register(modEventBus); + } +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 22bb2fb..24e75c6 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -5,12 +5,16 @@ import net.neoforged.api.distmarker.Dist; import net.neoforged.fml.common.Mod; import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; +import net.neoforged.neoforge.event.tick.PlayerTickEvent; +import net.minecraft.server.level.ServerPlayer; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; +import za.co.neroland.nerospace.world.OxygenManager; /** * NeoForge entry point. Runs shared init (building the DeferredRegisters via the @@ -26,9 +30,17 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NeoForgeFluidFactory.registerFluidTypes(modEventBus); NeoForgeRegistrationFactory.registerAll(modEventBus); NeoForgeCapabilities.register(modEventBus); + NeoForgeAttachments.register(modEventBus); if (FMLEnvironment.getDist() == Dist.CLIENT) { NeoForgeClientSetup.init(modEventBus); } + + // Oxygen survival: tick each player on the game bus (airless-planet drain / suffocation). + NeoForge.EVENT_BUS.addListener((PlayerTickEvent.Post event) -> { + if (event.getEntity() instanceof ServerPlayer serverPlayer) { + OxygenManager.tick(serverPlayer); + } + }); // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated // Nerospace tab registered via the vanilla CREATIVE_MODE_TAB registry), so no NeoForge-specific // BuildCreativeModeTabContentsEvent injection is needed. diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java index fc069f5..737d976 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -3,6 +3,9 @@ import net.neoforged.fml.ModList; import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.api.distmarker.Dist; +import net.minecraft.world.entity.player.Player; + +import za.co.neroland.nerospace.neoforge.NeoForgeAttachments; /** * NeoForge implementation of {@link IPlatformHelper}. Registered via @@ -31,4 +34,14 @@ public boolean isModLoaded(String modId) { public boolean isClient() { return FMLEnvironment.getDist() == Dist.CLIENT; } + + @Override + public int getOxygen(Player player) { + return player.getData(NeoForgeAttachments.OXYGEN.get()); + } + + @Override + public void setOxygen(Player player, int value) { + player.setData(NeoForgeAttachments.OXYGEN.get(), value); + } } From 870796deeda241424575d9a5906891d6ffffc253 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 08:47:23 +0200 Subject: [PATCH 42/82] Add cross-loader networking seam Introduce a cross-loader networking abstraction and implementations. Adds common ModNetwork (payload registry, clientbound/serverbound records, send helpers and init hook) and NetworkPlatform interface, and wires Services.NETWORK. Fabric and NeoForge concrete implementations (FabricNetwork, NeoForgeNetwork) register payload types/receivers and implement send methods; service provider files added for both loaders. NerospaceFabric/NerospaceFabricClient and NerospaceNeoForge now register the networking pieces, and ModRegistries calls ModNetwork.init(). Update docs to mark the networking seam done and document payload registration/sending and client-safety contract. Payloads themselves remain unregistered here and will be added by their consuming subsystems. --- docs/MULTILOADER_PORT_CHECKLIST.md | 27 +++++-- .../nerospace/network/ModNetwork.java | 81 +++++++++++++++++++ .../nerospace/platform/NetworkPlatform.java | 19 +++++ .../neroland/nerospace/platform/Services.java | 2 + .../nerospace/registry/ModRegistries.java | 1 + .../nerospace/fabric/FabricNetwork.java | 65 +++++++++++++++ .../nerospace/fabric/NerospaceFabric.java | 1 + .../fabric/NerospaceFabricClient.java | 1 + ...eroland.nerospace.platform.NetworkPlatform | 1 + .../nerospace/neoforge/NeoForgeNetwork.java | 61 ++++++++++++++ .../nerospace/neoforge/NerospaceNeoForge.java | 1 + ...eroland.nerospace.platform.NetworkPlatform | 1 + 12 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/NetworkPlatform.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricNetwork.java create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeNetwork.java create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 0beeea2..561d010 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,7 +1,7 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~139 classes ported, ~125 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~143 classes ported, ~121 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **2026-06-20 update — quarry ported.** All 4 cells green. Added 11 classes: @@ -164,10 +164,14 @@ checked by a headless build). - [x] `ModDataComponents` — `SELECTED_PIPE_TYPE` (int) + `FILTER_ITEM` (vanilla `ItemStack` instead of the root's NeoForge `ItemResource`), via `RegistrationProvider` over `DATA_COMPONENT_TYPE`. Consumed by the advanced-pipe configurator/filter (advanced pipes batch). -- [~] `ModCriteria` (`terraformed_ground`/`living_ground`/`founded_station` `PlayerTrigger`s) — **deferred**: - the criterion classes resolve to a **different package on 26.2 NeoForm than the root's 26.1 imports** - (`net.minecraft.advancements.criterion` not found), so it needs a version/loader-checked import — resolve - it alongside its first consumer (station founding / star guide / terraform). Orphan until then. +- [~] `ModCriteria` (`terraformed_ground`/`living_ground`/`founded_station` `PlayerTrigger`s) — **deferred: + confirmed cross-version vanilla package move** (probed 2026-06-21): on **26.1.2** the classes are + `net.minecraft.advancements.CriterionTrigger` + `net.minecraft.advancements.criterion.PlayerTrigger`; on + **26.2** both are under `net.minecraft.advancements.triggers`. A single shared `import` can't satisfy both + MC versions, so this can't be a plain common class. Options when its first consumer (station founding / + star guide / terraform) lands: (a) drop the custom advancement triggers (they're cosmetic — the systems + work without firing them); (b) reflection (resolve `PlayerTrigger` by per-version FQN); or (c) add + version-split source sets. Orphan until then. - [ ] `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge attachments vs Fabric component/attachment API), `ModFeatures`, `ModConfiguredFeatures`/`ModPlacedFeatures`/`ModBiomes`/ `ModBiomeModifiers` (datagen bootstraps — mostly superseded by the copied JSON), `ModDimensionTypes` @@ -179,9 +183,16 @@ checked by a headless build). tabs in-game (items were searchable but absent when browsing) — replaced on both loaders. Note: vanilla `CreativeModeTab.builder(Row, column)` (the no-arg overload + `withTabsBefore` are NeoForge-only). -### Networking (`network/` 5) — **needed by oxygen HUD, meteors, pipe modes** -- [ ] Cross-loader packet seam: NeoForge `PayloadRegistrar` vs Fabric networking API. `ModNetwork`, - `ModPayloads`, `OxygenFieldSyncPayload`, `MeteorSyncPayload`, `SetPipeModePayload`. +### Networking (`network/` 5) — **SEAM DONE (4 cells green); payloads ship with their consumers** +- [x] Cross-loader packet seam: common `network/ModNetwork` (payload registry: `clientbound`/`serverbound` + lists + `sendToPlayer`/`sendToServer`) + `platform/NetworkPlatform` send seam. NeoForge `NeoForgeNetwork` + registers via `RegisterPayloadHandlersEvent` (`playToClient`/`playToServer`) and sends via + `PacketDistributor.sendToPlayer` / **`ClientPacketDistributor.sendToServer`** (client-only). Fabric + `FabricNetwork` registers via **`PayloadTypeRegistry.clientboundPlay()/serverboundPlay()`** + + `Server/ClientPlayNetworking` receivers and sends via `Server/ClientPlayNetworking.send`. Verified the + exact 26.2 APIs with a temporary javap probe (removed). No payloads registered yet — `OxygenFieldSyncPayload`, + `MeteorSyncPayload`, `SetPipeModePayload` ship with their subsystems (each just calls `ModNetwork.clientbound/ + serverbound(...)`). Client-safety contract documented in `ModNetwork`. ### Commands & compat - [ ] `command/NerospaceCommands` — `/nerospace` debug/admin commands (vanilla Brigadier; loader event differs). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java new file mode 100644 index 0000000..6c073f9 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java @@ -0,0 +1,81 @@ +package za.co.neroland.nerospace.network; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import za.co.neroland.nerospace.platform.Services; + +/** + * Cross-loader networking registry. Subsystems declare their payloads here once (type + stream codec + + * handler); each loader iterates these lists and wires them to its own networking API (NeoForge + * {@code PayloadRegistrar} during {@code RegisterPayloadHandlersEvent}; Fabric + * {@code PayloadTypeRegistry.clientboundPlay()/serverboundPlay()} + {@code Server|ClientPlayNetworking} + * receivers). Sending goes through the {@link Services#NETWORK} seam. + * + *

Client-safety contract. A clientbound {@link Clientbound#handler()} runs on the physical + * client; register it from client-reachable code only and do not let its method statically load a + * client-only class on a dedicated server before the handler actually runs. (No payloads are registered + * yet — the consuming subsystems — oxygen field, meteors, pipe modes — add theirs here as they are ported.)

+ */ +public final class ModNetwork { + + /** A server → client payload + the client-side handler that consumes it. */ + public record Clientbound( + CustomPacketPayload.Type type, + StreamCodec codec, + Consumer handler) { + } + + /** A client → server payload + the server-side handler (with the sending player). */ + public record Serverbound( + CustomPacketPayload.Type type, + StreamCodec codec, + BiConsumer handler) { + } + + private static final List> CLIENTBOUND = new ArrayList<>(); + private static final List> SERVERBOUND = new ArrayList<>(); + + private ModNetwork() { + } + + public static void clientbound( + CustomPacketPayload.Type type, StreamCodec codec, Consumer handler) { + CLIENTBOUND.add(new Clientbound<>(type, codec, handler)); + } + + public static void serverbound( + CustomPacketPayload.Type type, StreamCodec codec, BiConsumer handler) { + SERVERBOUND.add(new Serverbound<>(type, codec, handler)); + } + + public static List> clientbound() { + return CLIENTBOUND; + } + + public static List> serverbound() { + return SERVERBOUND; + } + + /** Server → one client. */ + public static void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + Services.NETWORK.sendToPlayer(player, payload); + } + + /** Client → server (call only on the physical client). */ + public static void sendToServer(CustomPacketPayload payload) { + Services.NETWORK.sendToServer(payload); + } + + /** Called from common init so the payload lists are populated before each loader registers them. */ + public static void init() { + // Subsystems register their payloads here as they are ported. + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/NetworkPlatform.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/NetworkPlatform.java new file mode 100644 index 0000000..1068a90 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/NetworkPlatform.java @@ -0,0 +1,19 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +/** + * Cross-loader packet send seam (the counterpart to NeoForge {@code PacketDistributor} / + * {@code ClientPacketDistributor} and Fabric {@code Server|ClientPlayNetworking.send}). Payload + * types and handlers are declared once in {@link za.co.neroland.nerospace.network.ModNetwork}; + * each loader registers them and implements this send interface, resolved via {@link Services}. + */ +public interface NetworkPlatform { + + /** Server → one client. */ + void sendToPlayer(ServerPlayer player, CustomPacketPayload payload); + + /** Client → server (call only on the physical client). */ + void sendToServer(CustomPacketPayload payload); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java index 26df523..40b885d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java @@ -16,6 +16,8 @@ public final class Services { public static final IPlatformHelper PLATFORM = load(IPlatformHelper.class); + public static final NetworkPlatform NETWORK = load(NetworkPlatform.class); + private Services() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index e935326..8363842 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -21,5 +21,6 @@ public static void init() { ModMenuTypes.init(); ModEntities.init(); ModCreativeTab.init(); + za.co.neroland.nerospace.network.ModNetwork.init(); } } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricNetwork.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricNetwork.java new file mode 100644 index 0000000..2f5bd80 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricNetwork.java @@ -0,0 +1,65 @@ +package za.co.neroland.nerospace.fabric; + +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +import za.co.neroland.nerospace.network.ModNetwork; +import za.co.neroland.nerospace.platform.NetworkPlatform; + +/** + * Fabric side of the networking seam. {@link #registerCommon()} (mod init, both sides) registers every + * payload type ({@code PayloadTypeRegistry.clientboundPlay()/serverboundPlay()}) and the + * serverbound receivers; {@link #registerClient()} (client init) registers the clientbound receivers — + * keeping {@code ClientPlayNetworking} off the dedicated server until then. Send methods implement + * {@link NetworkPlatform}. Registered via {@code META-INF/services}. + */ +public final class FabricNetwork implements NetworkPlatform { + + /** Mod-init (both sides): payload types + serverbound receivers. */ + public static void registerCommon() { + for (ModNetwork.Clientbound cb : ModNetwork.clientbound()) { + registerClientboundType(cb); + } + for (ModNetwork.Serverbound sb : ModNetwork.serverbound()) { + registerServerbound(sb); + } + } + + /** Client-init: clientbound receivers (client-only API). */ + public static void registerClient() { + for (ModNetwork.Clientbound cb : ModNetwork.clientbound()) { + registerClientReceiver(cb); + } + } + + private static void registerClientboundType(ModNetwork.Clientbound cb) { + PayloadTypeRegistry.clientboundPlay().register(cb.type(), cb.codec()); + } + + private static void registerServerbound(ModNetwork.Serverbound sb) { + PayloadTypeRegistry.serverboundPlay().register(sb.type(), sb.codec()); + ServerPlayNetworking.registerGlobalReceiver(sb.type(), (payload, context) -> { + ServerPlayer player = context.player(); + ((net.minecraft.server.level.ServerLevel) player.level()).getServer() + .execute(() -> sb.handler().accept(payload, player)); + }); + } + + private static void registerClientReceiver(ModNetwork.Clientbound cb) { + ClientPlayNetworking.registerGlobalReceiver(cb.type(), (payload, context) -> + context.client().execute(() -> cb.handler().accept(payload))); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPlayNetworking.send(payload); + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index fab977b..5d28b43 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -64,6 +64,7 @@ public void onInitialize() { // Oxygen survival: register the attachment + tick each player per world tick (airless-planet drain). FabricAttachments.init(); + FabricNetwork.registerCommon(); ServerTickEvents.END_SERVER_TICK.register(server -> server.getPlayerList().getPlayers().forEach(OxygenManager::tick)); diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index d2f68e1..3b6d38b 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -24,6 +24,7 @@ public final class NerospaceFabricClient implements ClientModInitializer { @Override public void onInitializeClient() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric client bootstrap"); + FabricNetwork.registerClient(); MenuScreens.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); MenuScreens.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); MenuScreens.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform new file mode 100644 index 0000000..347a2d8 --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform @@ -0,0 +1 @@ +za.co.neroland.nerospace.fabric.FabricNetwork diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeNetwork.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeNetwork.java new file mode 100644 index 0000000..428c259 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeNetwork.java @@ -0,0 +1,61 @@ +package za.co.neroland.nerospace.neoforge; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +import za.co.neroland.nerospace.network.ModNetwork; +import za.co.neroland.nerospace.platform.NetworkPlatform; + +/** + * NeoForge side of the networking seam: registers every {@link ModNetwork} payload during + * {@code RegisterPayloadHandlersEvent} (clientbound handlers run on the client; serverbound handlers + * receive the sending {@link ServerPlayer}) and implements the send methods. Server → client uses + * {@code PacketDistributor.sendToPlayer}; client → server uses the client-only + * {@code ClientPacketDistributor.sendToServer} (loaded lazily, only when actually sending from a client). + * Registered via {@code META-INF/services}. + */ +public final class NeoForgeNetwork implements NetworkPlatform { + + public static void register(IEventBus modEventBus) { + modEventBus.addListener(NeoForgeNetwork::onRegister); + } + + private static void onRegister(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1").optional(); + for (ModNetwork.Clientbound cb : ModNetwork.clientbound()) { + registerClientbound(registrar, cb); + } + for (ModNetwork.Serverbound sb : ModNetwork.serverbound()) { + registerServerbound(registrar, sb); + } + } + + private static void registerClientbound(PayloadRegistrar registrar, ModNetwork.Clientbound cb) { + registrar.playToClient(cb.type(), cb.codec(), + (payload, context) -> context.enqueueWork(() -> cb.handler().accept(payload))); + } + + private static void registerServerbound(PayloadRegistrar registrar, ModNetwork.Serverbound sb) { + registrar.playToServer(sb.type(), sb.codec(), + (payload, context) -> context.enqueueWork(() -> { + if (context.player() instanceof ServerPlayer serverPlayer) { + sb.handler().accept(payload, serverPlayer); + } + })); + } + + @Override + public void sendToPlayer(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } + + @Override + public void sendToServer(CustomPacketPayload payload) { + ClientPacketDistributor.sendToServer(payload); + } +} diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 24e75c6..721d678 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -31,6 +31,7 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NeoForgeRegistrationFactory.registerAll(modEventBus); NeoForgeCapabilities.register(modEventBus); NeoForgeAttachments.register(modEventBus); + NeoForgeNetwork.register(modEventBus); if (FMLEnvironment.getDist() == Dist.CLIENT) { NeoForgeClientSetup.init(modEventBus); } diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform new file mode 100644 index 0000000..9dcb835 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.NetworkPlatform @@ -0,0 +1 @@ +za.co.neroland.nerospace.neoforge.NeoForgeNetwork From cc62d198929077276ad5237e12fdb0b7f3a18231 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 09:00:10 +0200 Subject: [PATCH 43/82] Add alien structures, features, and village core Ported alien worldgen and structure code into the multiloader: adds Hamlet, Ruin and MegaCity Feature implementations, the shared AlienBuild procedural pieces, and StructureSpacing region/density logic. Registers the Feature types via a new ModFeatures provider and wires ModFeatures.init() into ModRegistries. Adds a decorative VillageCore block + item, model/texture/blockstate, loot table and language entry (interactive BE/controller deferred). Adds configured/placed feature JSON and re-adds the placed features to the Greenxertz biome so these structures generate. Also updates the docs checklist to mark structures as done. --- docs/MULTILOADER_PORT_CHECKLIST.md | 15 +- .../nerospace/registry/ModBlocks.java | 5 + .../nerospace/registry/ModFeatures.java | 38 +++++ .../neroland/nerospace/registry/ModItems.java | 4 +- .../nerospace/registry/ModRegistries.java | 1 + .../nerospace/village/VillageCoreBlock.java | 18 +++ .../neroland/nerospace/world/AlienBuild.java | 131 ++++++++++++++++++ .../nerospace/world/HamletFeature.java | 64 +++++++++ .../nerospace/world/MegaCityFeature.java | 107 ++++++++++++++ .../neroland/nerospace/world/RuinFeature.java | 58 ++++++++ .../nerospace/world/StructureSpacing.java | 61 ++++++++ .../nerospace/blockstates/village_core.json | 7 + .../assets/nerospace/items/village_core.json | 6 + .../assets/nerospace/lang/en_us.json | 1 + .../nerospace/models/block/village_core.json | 6 + .../nerospace/textures/block/village_core.png | Bin 0 -> 336 bytes .../loot_table/blocks/village_core.json | 21 +++ .../nerospace/worldgen/biome/greenxertz.json | 5 +- .../worldgen/configured_feature/hamlet.json | 4 + .../configured_feature/mega_city.json | 4 + .../worldgen/configured_feature/ruin.json | 4 + .../placed_feature/hamlet_placed.json | 19 +++ .../placed_feature/mega_city_placed.json | 19 +++ .../worldgen/placed_feature/ruin_placed.json | 19 +++ 24 files changed, 610 insertions(+), 7 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModFeatures.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/AlienBuild.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/HamletFeature.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/MegaCityFeature.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/RuinFeature.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/StructureSpacing.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/village_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/village_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/village_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/village_core.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/village_core.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/hamlet.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/mega_city.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/ruin.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/hamlet_placed.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/mega_city_placed.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/ruin_placed.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 561d010..6fafdd4 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,7 +1,7 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~143 classes ported, ~121 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~150 classes ported, ~114 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **2026-06-20 update — quarry ported.** All 4 cells green. Added 11 classes: @@ -113,10 +113,15 @@ checked by a headless build). `TerraformConversion`, `TerraformDrift`, `TerraformFauna`, `TerraformChunkLoader`, `TerraformResources`, `GreenxertzAtmosphere`, terraformed biomes. Risk: **high** (world mutation, chunk-loading, events). -### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) -- [ ] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + their - configured/placed-feature JSON (the 3 features I **stripped** from the Greenxertz biome) + structure data. -- [ ] `VillageCoreBlock`(+BE) — per-village reputation aggregation. +### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) — **DONE (4 cells green)** +- [x] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + `ModFeatures` + (registers the 3 `Feature` types via `RegistrationProvider` over `FEATURE`). Copied the + configured/placed-feature JSON and **re-added the 3 placed features to the Greenxertz biome JSON** + (`greenxertz.json` feature step 6) — since Greenxertz is our own datapack biome, no biome-modifier seam + needed. Mega-city spawns the (ported) Ruin Warden boss; ruin/mega-city fill vanilla loot chests. +- [~] `VillageCoreBlock` — ported as a **plain decorative centerpiece block** (the structures' anchor). + The interactive controller (`VillageCoreBlockEntity`: claim → teach-and-grow construction, fetch + quests, night raids) is **deferred** — it pulls in `VillageBuildings` + the config seam. ### Meteor events (`meteor/` 8 + client) - [ ] `FallingMeteorEntity` (+ model/renderer/state), `MeteorCallerItem`, `MeteorCoreBlock`(+BE), diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 3914aad..8d5447b 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -36,6 +36,7 @@ import za.co.neroland.nerospace.storage.BatteryBlock; import za.co.neroland.nerospace.storage.FluidTankBlock; import za.co.neroland.nerospace.storage.ItemStoreBlock; +import za.co.neroland.nerospace.village.VillageCoreBlock; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; /** @@ -90,6 +91,10 @@ public final class ModBlocks { p -> p.mapColor(MapColor.COLOR_GREEN).strength(1.5F, 6.0F).lightLevel(s -> 15).sound(SoundType.METAL)); public static final RegistryEntry ALIEN_CRYSTAL_BLOCK = block("alien_crystal_block", p -> p.mapColor(MapColor.EMERALD).strength(1.5F, 6.0F).lightLevel(s -> 12).sound(SoundType.AMETHYST)); + public static final RegistryEntry VILLAGE_CORE = BLOCKS.register("village_core", + key -> new VillageCoreBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.EMERALD).strength(2.0F, 6.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 10).sound(SoundType.AMETHYST))); public static final RegistryEntry METEOR_ROCK = block("meteor_rock", p -> p.mapColor(MapColor.COLOR_BLACK).strength(3.0F, 4.0F).requiresCorrectToolForDrops().lightLevel(s -> 3).sound(SoundType.STONE)); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModFeatures.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModFeatures.java new file mode 100644 index 0000000..0c30eb9 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModFeatures.java @@ -0,0 +1,38 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; +import za.co.neroland.nerospace.world.HamletFeature; +import za.co.neroland.nerospace.world.MegaCityFeature; +import za.co.neroland.nerospace.world.RuinFeature; + +/** + * Custom worldgen feature types, ported cross-loader via {@link RegistrationProvider} over the vanilla + * {@code FEATURE} registry (the root used a NeoForge {@code DeferredRegister}). The configured/placed + * feature JSON (copied from the root's datagen) reference these by id; the Greenxertz biome lists the + * placed features so they generate there. + */ +public final class ModFeatures { + + public static final RegistrationProvider> FEATURES = + RegistrationProvider.get(Registries.FEATURE, NerospaceCommon.MOD_ID); + + public static final RegistryEntry HAMLET = + FEATURES.register("hamlet", key -> new HamletFeature(NoneFeatureConfiguration.CODEC)); + + public static final RegistryEntry RUIN = + FEATURES.register("ruin", key -> new RuinFeature(NoneFeatureConfiguration.CODEC)); + + public static final RegistryEntry MEGA_CITY = + FEATURES.register("mega_city", key -> new MegaCityFeature(NoneFeatureConfiguration.CODEC)); + + private ModFeatures() { + } + + public static void init() { + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index cfc8e63..764c776 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -69,6 +69,7 @@ public final class ModItems { public static final RegistryEntry ALIEN_PILLAR_ITEM = blockItem("alien_pillar", ModBlocks.ALIEN_PILLAR); public static final RegistryEntry ALIEN_LAMP_ITEM = blockItem("alien_lamp", ModBlocks.ALIEN_LAMP); public static final RegistryEntry ALIEN_CRYSTAL_BLOCK_ITEM = blockItem("alien_crystal_block", ModBlocks.ALIEN_CRYSTAL_BLOCK); + public static final RegistryEntry VILLAGE_CORE_ITEM = blockItem("village_core", ModBlocks.VILLAGE_CORE); public static final RegistryEntry METEOR_ROCK_ITEM = blockItem("meteor_rock", ModBlocks.METEOR_ROCK); public static final RegistryEntry ITEM_STORE_ITEM = blockItem("item_store", ModBlocks.ITEM_STORE); public static final RegistryEntry BATTERY_ITEM = blockItem("battery", ModBlocks.BATTERY); @@ -242,7 +243,8 @@ public static Map, List> creativeTabItems NEROSTEEL_BLOCK_ITEM.get(), CINDRITE_BLOCK_ITEM.get(), GLACITE_BLOCK_ITEM.get(), STATION_FLOOR_ITEM.get(), STATION_WALL_ITEM.get(), ALIEN_BRICKS_ITEM.get(), CRACKED_ALIEN_BRICKS_ITEM.get(), ALIEN_TILE_ITEM.get(), - ALIEN_PILLAR_ITEM.get(), ALIEN_LAMP_ITEM.get(), ALIEN_CRYSTAL_BLOCK_ITEM.get()), + ALIEN_PILLAR_ITEM.get(), ALIEN_LAMP_ITEM.get(), ALIEN_CRYSTAL_BLOCK_ITEM.get(), + VILLAGE_CORE_ITEM.get()), CreativeModeTabs.INGREDIENTS, List.of(RAW_NEROSIUM.get(), NEROSIUM_INGOT.get(), RAW_NEROSTEEL.get(), NEROSTEEL_INGOT.get(), diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java index 8363842..fc0799a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -20,6 +20,7 @@ public static void init() { ModBlockEntities.init(); ModMenuTypes.init(); ModEntities.init(); + ModFeatures.init(); ModCreativeTab.init(); za.co.neroland.nerospace.network.ModNetwork.init(); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java new file mode 100644 index 0000000..d889545 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java @@ -0,0 +1,18 @@ +package za.co.neroland.nerospace.village; + +import net.minecraft.world.level.block.Block; + +/** + * Village Core — the glowing centerpiece of alien hamlets / ruins / mega-cities. + * + *

Cross-loader port note: ported here as a plain decorative block. The root's interactive controller + * (claim → teach-and-grow village construction, fetch quests, night raids) is a deep gameplay subsystem + * (`VillageCoreBlockEntity` + `VillageBuildings` + config gates) deferred to its own batch; the structures + * place this as their anchor centerpiece meanwhile.

+ */ +public class VillageCoreBlock extends Block { + + public VillageCoreBlock(Properties properties) { + super(properties); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/AlienBuild.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/AlienBuild.java new file mode 100644 index 0000000..eff4dc3 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/AlienBuild.java @@ -0,0 +1,131 @@ +package za.co.neroland.nerospace.world; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import za.co.neroland.nerospace.registry.ModBlocks; + +/** + * Shared procedural pieces for the alien structures — a small kit that makes the hamlet, ruin and + * mega-city read as futuristic alien architecture: tile podiums, brick walls with glowing crystal + * window bands, taller lit corner pillars, tapered roofs and crystal spires. Built entirely from the + * alien decoration block set. + */ +public final class AlienBuild { + + private AlienBuild() { + } + + public static BlockState bricks() { + return ModBlocks.ALIEN_BRICKS.get().defaultBlockState(); + } + + public static BlockState cracked() { + return ModBlocks.CRACKED_ALIEN_BRICKS.get().defaultBlockState(); + } + + public static BlockState tile() { + return ModBlocks.ALIEN_TILE.get().defaultBlockState(); + } + + public static BlockState pillar() { + return ModBlocks.ALIEN_PILLAR.get().defaultBlockState(); + } + + public static BlockState lamp() { + return ModBlocks.ALIEN_LAMP.get().defaultBlockState(); + } + + public static BlockState crystal() { + return ModBlocks.ALIEN_CRYSTAL_BLOCK.get().defaultBlockState(); + } + + public static BlockState air() { + return Blocks.AIR.defaultBlockState(); + } + + private static void set(WorldGenLevel level, BlockPos.MutableBlockPos m, int x, int y, int z, BlockState s) { + m.set(x, y, z); + level.setBlock(m, s, 2); + } + + /** + * A futuristic alien building. Tile podium, brick walls with a glowing crystal window band, taller + * lit corner pillars, a tapered roof and a crystal spire. {@code weathered} → ruined variant + * (cracked bricks, broken walls, no spire). {@code r} = half-footprint, {@code height} = wall height. + * Origin is the building centre at ground level {@code baseY}. + */ + public static void tower(WorldGenLevel level, int cx, int baseY, int cz, int r, int height, + boolean weathered, RandomSource rand, BlockPos.MutableBlockPos m) { + BlockState wall = weathered ? cracked() : bricks(); + int band = Math.max(1, height / 2); + + for (int dx = -r - 1; dx <= r + 1; dx++) { + for (int dz = -r - 1; dz <= r + 1; dz++) { + set(level, m, cx + dx, baseY - 1, cz + dz, tile()); + } + } + for (int dy = 0; dy <= height + 2; dy++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + set(level, m, cx + dx, baseY + dy, cz + dz, air()); + } + } + } + + for (int dy = 0; dy < height; dy++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + boolean perimeter = Math.abs(dx) == r || Math.abs(dz) == r; + if (!perimeter) { + continue; + } + boolean corner = Math.abs(dx) == r && Math.abs(dz) == r; + if (corner) { + continue; + } + boolean door = dz == -r && dx == 0 && dy <= 1; + if (door) { + continue; + } + if (weathered && rand.nextFloat() < 0.28F) { + continue; + } + boolean window = dy == band; + set(level, m, cx + dx, baseY + dy, cz + dz, window ? crystal() : wall); + } + } + } + + int pillarTop = weathered ? height - 1 : height + 1; + for (int sx : new int[] {-r, r}) { + for (int sz : new int[] {-r, r}) { + for (int dy = 0; dy <= pillarTop; dy++) { + set(level, m, cx + sx, baseY + dy, cz + sz, pillar()); + } + if (!weathered) { + set(level, m, cx + sx, baseY + pillarTop + 1, cz + sz, lamp()); + } + } + } + + for (int dx = -(r - 1); dx <= r - 1; dx++) { + for (int dz = -(r - 1); dz <= r - 1; dz++) { + if (weathered && rand.nextFloat() < 0.35F) { + continue; + } + set(level, m, cx + dx, baseY + height, cz + dz, dx == 0 && dz == 0 ? crystal() : tile()); + } + } + set(level, m, cx, baseY + 1, cz, lamp()); + + if (!weathered) { + for (int dy = 1; dy <= 3; dy++) { + set(level, m, cx, baseY + height + dy, cz, crystal()); + } + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/HamletFeature.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/HamletFeature.java new file mode 100644 index 0000000..dcfac42 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/HamletFeature.java @@ -0,0 +1,64 @@ +package za.co.neroland.nerospace.world; + +import com.mojang.serialization.Codec; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; + +import za.co.neroland.nerospace.registry.ModBlocks; + +/** + * Hamlet — a small alien outpost: a glowing tile plaza, a central {@code VillageCore} on a lit podium, + * and two futuristic towers. Placement is gated by {@link StructureSpacing} for spacing + density cap. + */ +public class HamletFeature extends Feature { + + private static final int PLAZA = 6; // 13x13 plaza + + public HamletFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(FeaturePlaceContext ctx) { + BlockPos o = ctx.origin(); + if (!StructureSpacing.shouldPlace(o, StructureSpacing.Roi.HAMLET)) { + return false; + } + WorldGenLevel level = ctx.level(); + RandomSource rand = ctx.random(); + int baseY = o.getY(); + BlockPos.MutableBlockPos m = new BlockPos.MutableBlockPos(); + + for (int dx = -PLAZA; dx <= PLAZA; dx++) { + for (int dz = -PLAZA; dz <= PLAZA; dz++) { + m.set(o.getX() + dx, baseY - 1, o.getZ() + dz); + boolean edge = Math.abs(dx) == PLAZA || Math.abs(dz) == PLAZA; + level.setBlock(m, edge && (dx + dz) % 2 == 0 ? AlienBuild.lamp() : AlienBuild.tile(), 2); + for (int dy = 0; dy < 4; dy++) { + m.set(o.getX() + dx, baseY + dy, o.getZ() + dz); + level.setBlock(m, AlienBuild.air(), 2); + } + } + } + + AlienBuild.tower(level, o.getX() - 4, baseY, o.getZ() - 4, 2, 4, false, rand, m); + AlienBuild.tower(level, o.getX() + 4, baseY, o.getZ() + 4, 2, 4, false, rand, m); + + BlockState core = ModBlocks.VILLAGE_CORE.get().defaultBlockState(); + m.set(o.getX(), baseY - 1, o.getZ()); + level.setBlock(m, AlienBuild.crystal(), 2); + m.set(o.getX(), baseY, o.getZ()); + level.setBlock(m, core, 2); + for (int[] off : new int[][] {{-2, 0}, {2, 0}, {0, -2}, {0, 2}}) { + m.set(o.getX() + off[0], baseY, o.getZ() + off[1]); + level.setBlock(m, AlienBuild.lamp(), 2); + } + return true; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/MegaCityFeature.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/MegaCityFeature.java new file mode 100644 index 0000000..1704649 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/MegaCityFeature.java @@ -0,0 +1,107 @@ +package za.co.neroland.nerospace.world; + +import com.mojang.serialization.Codec; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; + +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Living Mega-City — the massive end-state alien settlement: a pillared, lit, crenellated curtain wall + * with four gates around a glowing tile plaza of towers, and a central keep guarded by the Ruin Warden + * boss over a grand vault. Very rare, spaced via {@link StructureSpacing}. + */ +public class MegaCityFeature extends Feature { + + private static final int WALL_R = 20; // 41x41 footprint + private static final int WALL_H = 6; + + public MegaCityFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(FeaturePlaceContext ctx) { + BlockPos o = ctx.origin(); + if (!StructureSpacing.shouldPlace(o, StructureSpacing.Roi.MEGA_CITY)) { + return false; + } + WorldGenLevel level = ctx.level(); + RandomSource rand = ctx.random(); + int baseY = o.getY(); + BlockPos.MutableBlockPos m = new BlockPos.MutableBlockPos(); + BlockState bricks = AlienBuild.bricks(); + + for (int dx = -WALL_R; dx <= WALL_R; dx++) { + for (int dz = -WALL_R; dz <= WALL_R; dz++) { + int x = o.getX() + dx; + int z = o.getZ() + dz; + m.set(x, baseY - 1, z); + level.setBlock(m, AlienBuild.tile(), 2); + boolean perimeter = Math.abs(dx) == WALL_R || Math.abs(dz) == WALL_R; + boolean gate = (Math.abs(dx) <= 1 && Math.abs(dz) == WALL_R) + || (Math.abs(dz) <= 1 && Math.abs(dx) == WALL_R); + if (!perimeter || gate) { + continue; + } + boolean pillar = (dx % 5 == 0) || (dz % 5 == 0) || (Math.abs(dx) == WALL_R && Math.abs(dz) == WALL_R); + int top = pillar ? WALL_H + 1 : WALL_H - 1; + for (int dy = 0; dy <= top; dy++) { + m.set(x, baseY + dy, z); + level.setBlock(m, pillar ? AlienBuild.pillar() : bricks, 2); + } + if (pillar) { + m.set(x, baseY + top + 1, z); + level.setBlock(m, AlienBuild.lamp(), 2); + } else if ((dx + dz) % 2 == 0) { + m.set(x, baseY + top + 1, z); + level.setBlock(m, bricks, 2); + } + } + } + for (int[] g : new int[][] {{0, WALL_R}, {0, -WALL_R}, {WALL_R, 0}, {-WALL_R, 0}}) { + for (int s : new int[] {-2, 2}) { + int gx = o.getX() + (g[0] == 0 ? s : g[0]); + int gz = o.getZ() + (g[1] == 0 ? s : g[1]); + m.set(gx, baseY + WALL_H, gz); + level.setBlock(m, AlienBuild.lamp(), 2); + } + } + + AlienBuild.tower(level, o.getX() - 11, baseY, o.getZ() - 11, 3, 6, false, rand, m); + AlienBuild.tower(level, o.getX() + 11, baseY, o.getZ() - 11, 3, 6, false, rand, m); + AlienBuild.tower(level, o.getX() - 11, baseY, o.getZ() + 11, 3, 6, false, rand, m); + AlienBuild.tower(level, o.getX() + 11, baseY, o.getZ() + 11, 3, 5, false, rand, m); + + AlienBuild.tower(level, o.getX(), baseY, o.getZ(), 5, 8, false, rand, m); + m.set(o.getX(), baseY, o.getZ()); + level.setBlock(m, ModBlocks.VILLAGE_CORE.get().defaultBlockState(), 2); + BlockPos chestPos = new BlockPos(o.getX() + 2, baseY, o.getZ() + 2); + level.setBlock(chestPos, Blocks.CHEST.defaultBlockState(), 2); + if (level.getBlockEntity(chestPos) instanceof ChestBlockEntity chest) { + chest.setItem(4, new ItemStack(ModItems.ALIEN_CORE.get(), 2 + rand.nextInt(3))); + chest.setItem(6, new ItemStack(ModItems.GRAV_STRIDERS.get(), 1)); + chest.setItem(10, new ItemStack(ModItems.XERTZ_RESONATOR.get(), 1)); + chest.setItem(13, new ItemStack(Items.DIAMOND, 4 + rand.nextInt(6))); + chest.setItem(22, new ItemStack(Items.EMERALD, 12 + rand.nextInt(12))); + } + int by = level.getHeight(Heightmap.Types.WORLD_SURFACE, o.getX(), o.getZ()); + ModEntities.RUIN_WARDEN.get().spawn(level.getLevel(), new BlockPos(o.getX(), by, o.getZ()), + EntitySpawnReason.EVENT); + return true; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/RuinFeature.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/RuinFeature.java new file mode 100644 index 0000000..2fff992 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/RuinFeature.java @@ -0,0 +1,58 @@ +package za.co.neroland.nerospace.world; + +import com.mojang.serialization.Codec; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.FeaturePlaceContext; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; + +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Ancient Ruin — a derelict, half-buried alien hall of cracked alien brick with collapsed walls and a + * dead crystal core, holding a loot vault of rare alien goods. Spaced + capped by {@link StructureSpacing}. + */ +public class RuinFeature extends Feature { + + public RuinFeature(Codec codec) { + super(codec); + } + + @Override + public boolean place(FeaturePlaceContext ctx) { + BlockPos o = ctx.origin(); + if (!StructureSpacing.shouldPlace(o, StructureSpacing.Roi.RUIN)) { + return false; + } + WorldGenLevel level = ctx.level(); + RandomSource rand = ctx.random(); + int baseY = o.getY() - 2; // sunken + BlockPos.MutableBlockPos m = new BlockPos.MutableBlockPos(); + + AlienBuild.tower(level, o.getX(), baseY, o.getZ(), 6, 6, true, rand, m); + + BlockState core = ModBlocks.VILLAGE_CORE.get().defaultBlockState(); + m.set(o.getX(), baseY, o.getZ()); + level.setBlock(m, core, 2); + + BlockPos chestPos = new BlockPos(o.getX() + 3, baseY, o.getZ() + 3); + level.setBlock(chestPos, Blocks.CHEST.defaultBlockState(), 2); + if (level.getBlockEntity(chestPos) instanceof ChestBlockEntity chest) { + chest.setItem(4, new ItemStack(ModItems.ALIEN_CORE.get(), 1)); + chest.setItem(6, new ItemStack(ModItems.ALIEN_TECH_SCRAP.get(), 2 + rand.nextInt(4))); + chest.setItem(10, new ItemStack(ModItems.ALIEN_FRAGMENT.get(), 3 + rand.nextInt(5))); + chest.setItem(13, new ItemStack(ModItems.NEROSIUM_INGOT.get(), 2 + rand.nextInt(4))); + chest.setItem(22, new ItemStack(Items.EMERALD, 4 + rand.nextInt(8))); + } + return true; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/StructureSpacing.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/StructureSpacing.java new file mode 100644 index 0000000..b621009 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/StructureSpacing.java @@ -0,0 +1,61 @@ +package za.co.neroland.nerospace.world; + +import net.minecraft.core.BlockPos; + +/** + * Region-of-interest spacing + density cap. The world is divided into square region cells + * ({@link #CELL_CHUNKS}×{@link #CELL_CHUNKS} chunks). Each cell deterministically gets at most one + * ROI — and which kind (hamlet / ruin / mega-city, or nothing) is chosen by a hash of the cell, with + * weights that keep the rarer ones rare. The ROI sits at a deterministic "anchor" chunk inside the cell. + */ +public final class StructureSpacing { + + /** Size of a region cell in chunks (16 chunks ≈ 256 blocks). One ROI per cell, max. */ + public static final int CELL_CHUNKS = 16; + + public enum Roi { NONE, HAMLET, RUIN, MEGA_CITY } + + private StructureSpacing() { + } + + /** + * @return true only when {@code origin}'s chunk is the anchor of a region cell whose assigned ROI + * is {@code mine}. Call this first in a feature's {@code place} and bail out if false. + */ + public static boolean shouldPlace(BlockPos origin, Roi mine) { + int cx = origin.getX() >> 4; + int cz = origin.getZ() >> 4; + int rx = Math.floorDiv(cx, CELL_CHUNKS); + int rz = Math.floorDiv(cz, CELL_CHUNKS); + long h = mix(rx, rz); + + int roll = (int) Math.floorMod(h, 100L); + Roi type; + if (roll < 26) { + type = Roi.HAMLET; // 26% + } else if (roll < 34) { + type = Roi.RUIN; // 8% + } else if (roll < 36) { + type = Roi.MEGA_CITY; // 2% + } else { + type = Roi.NONE; // 64% empty + } + if (type != mine) { + return false; + } + int span = Math.max(1, CELL_CHUNKS - 4); + int ax = 2 + (int) Math.floorMod(h >>> 8, span); + int az = 2 + (int) Math.floorMod(h >>> 24, span); + return Math.floorMod(cx, CELL_CHUNKS) == ax && Math.floorMod(cz, CELL_CHUNKS) == az; + } + + private static long mix(int x, int z) { + long h = x * 341873128712L + z * 132897987541L + 0x9E3779B97F4A7C15L; + h ^= (h >>> 33); + h *= 0xff51afd7ed558ccdL; + h ^= (h >>> 33); + h *= 0xc4ceb9fe1a85ec53L; + h ^= (h >>> 33); + return h; + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/village_core.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/village_core.json new file mode 100644 index 0000000..6d2b273 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/village_core.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/village_core" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/village_core.json b/multiloader/common/src/main/resources/assets/nerospace/items/village_core.json new file mode 100644 index 0000000..8d4c932 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/village_core.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/village_core" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index ef0f458..7a10fab 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -52,6 +52,7 @@ "block.nerospace.station_wall": "Station Wall", "block.nerospace.trash_can": "Trash Can", "block.nerospace.universal_pipe": "Universal Pipe", + "block.nerospace.village_core": "Village Core", "block.nerospace.xertz_quartz_ore": "Xertz Quartz Ore", "container.nerospace.combustion_generator": "Combustion Generator", "container.nerospace.fuel_refinery": "Fuel Refinery", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/village_core.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/village_core.json new file mode 100644 index 0000000..d02431a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/village_core.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/village_core" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/village_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/village_core.png new file mode 100644 index 0000000000000000000000000000000000000000..ced3aa08d462855c1f532ce0d51c1baba7c23f1e GIT binary patch literal 336 zcmV-W0k8gvP)%wc^KX>o*y!Sr2&UiNcYV8QP~RG5PNG zt+77vJb-Tioun#Zgk=AfVLTN`=u+u0qMR^KQxfs|q@nFMEsmPZjEb>SAjLi`CM+ it;o<+*S>S Date: Sun, 21 Jun 2026 10:23:44 +0200 Subject: [PATCH 44/82] Add meteor feature: falling meteor & caller Port the creative meteor slice and spawn-placement rules. Adds FallingMeteorEntity, MeteorCoreBlock (+BE), MeteorCallerItem, MeteorLoot, model/renderer/state, textures and block/item models, and language keys; registers the new entity, block, block-entity and item in the mod registries and wires the client renderer. Introduces ModSpawnPlacements with a Sink interface and registers placement rules in the Fabric initializer. Updates the multiloader port checklist (docs) to note the meteor and spawn rules work; natural-shower manager, tracker and networking are deferred for a follow-up. --- docs/MULTILOADER_PORT_CHECKLIST.md | 33 ++- .../client/ClientEntityRenderers.java | 1 + .../nerospace/client/FallingMeteorModel.java | 44 ++++ .../client/FallingMeteorRenderState.java | 10 + .../client/FallingMeteorRenderer.java | 67 +++++ .../nerospace/meteor/FallingMeteorEntity.java | 236 ++++++++++++++++++ .../nerospace/meteor/MeteorCallerItem.java | 39 +++ .../nerospace/meteor/MeteorCoreBlock.java | 39 +++ .../meteor/MeteorCoreBlockEntity.java | 84 +++++++ .../neroland/nerospace/meteor/MeteorLoot.java | 70 ++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 6 + .../nerospace/registry/ModEntities.java | 11 +- .../neroland/nerospace/registry/ModItems.java | 7 +- .../registry/ModSpawnPlacements.java | 64 +++++ .../nerospace/blockstates/meteor_core.json | 7 + .../assets/nerospace/items/meteor_caller.json | 6 + .../assets/nerospace/lang/en_us.json | 4 + .../nerospace/models/block/meteor_core.json | 6 + .../nerospace/models/item/meteor_caller.json | 6 + .../nerospace/textures/block/meteor_core.png | Bin 0 -> 509 bytes .../textures/entity/falling_meteor.png | Bin 0 -> 4078 bytes .../nerospace/textures/item/meteor_caller.png | Bin 0 -> 142 bytes .../nerospace/fabric/NerospaceFabric.java | 15 ++ .../nerospace/neoforge/NerospaceNeoForge.java | 19 ++ 25 files changed, 770 insertions(+), 9 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSpawnPlacements.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/meteor_caller.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_caller.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_caller.png diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 6fafdd4..2176db2 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,21 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~150 classes ported, ~114 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~159 classes ported, ~105 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — meteor creative slice ported.** All 4 cells green. Added `meteor/{FallingMeteorEntity, +> MeteorCoreBlock, MeteorCoreBlockEntity, MeteorCallerItem, MeteorLoot}` + client `{FallingMeteorModel, +> FallingMeteorRenderer, FallingMeteorRenderState}` (bake-direct). Creative Meteor Caller → falling meteor → +> crater + break-to-loot Meteor Core. Config meteor keys inlined; natural-shower scheduler + client tracker + +> sync payload deferred (a clean networking-consumer follow-up). Lang validated via the built jar (mount was +> serving a stale truncated copy — jar check is the reliable validator). + +> **2026-06-21 update — spawn rules ported.** All 4 cells green. Added `registry/ModSpawnPlacements` +> (9 placement rules: 6× ground light-independent, 3× livestock on grass) behind a `Sink` seam — +> NeoForge `RegisterSpawnPlacementsEvent` (`Operation.REPLACE`), Fabric vanilla `SpawnPlacements.register`; +> both stable on 26.1.2 + 26.2. Mobs previously relied on biome lists + vanilla defaults only. + > **2026-06-20 update — quarry ported.** All 4 cells green. Added 11 classes: > `machine/quarry/{MinerTier, QuarryRegion, OutputFilter, PlanetMiningProfile, QuarryFrameBlock, > QuarryLandmarkBlock, QuarryLandmarkBlockEntity, QuarryControllerBlock, QuarryControllerBlockEntity, @@ -124,8 +136,16 @@ checked by a headless build). quests, night raids) is **deferred** — it pulls in `VillageBuildings` + the config seam. ### Meteor events (`meteor/` 8 + client) -- [ ] `FallingMeteorEntity` (+ model/renderer/state), `MeteorCallerItem`, `MeteorCoreBlock`(+BE), - `MeteorEventManager`, `MeteorEvents`, `MeteorSite`, `MeteorLoot`. Needs networking seam (impact sync). +- [x] **Creative slice** — `FallingMeteorEntity` (+ `FallingMeteorModel`/`FallingMeteorRenderer`/ + `FallingMeteorRenderState`, bake-direct), `MeteorCallerItem` (creative-only), `MeteorCoreBlock`(+BE, + break-to-loot), `MeteorLoot`. Meteor Caller → falling meteor → crater of `meteor_rock` around a + loot-bearing `meteor_core`. `METEOR_ROCK` + loot items (`alien_*`, raw ores) already existed; added + `FALLING_METEOR` entity, `METEOR_CORE` block+BE (no block item — world-gen only), `METEOR_CALLER` + item (TOOLS tab) + renderer; copied 3 textures + 4 asset JSON + 4 lang keys. Config meteor keys + inlined (crater radius 3, bonus rolls 3). All 4 cells green. +- [ ] **Natural showers + tracker** (deferred) — `MeteorEventManager` (SavedData scheduler), + `MeteorEvents` (per-loader tick hook), `MeteorSite`, + client `ClientMeteorTracker` HUD and a + `MeteorSyncPayload` (uses the networking seam). Needs the meteor config keys (spawn pacing) too. ### Star Guide / progression (`progression/` 5 + client + item) - [ ] `StarGuide`, `StarGuideProgress`, `StarGuideBlock`(+BE), `StarGuideMenu` + screen, hologram BER, @@ -208,8 +228,11 @@ checked by a headless build). shared config). Many ported machines currently use inlined constants where the root reads `Tuning`. ### Spawn rules -- [ ] `entity/ModEntityEvents` — natural-spawn placement rules (ground/light) + a cross-loader spawn-placement - seam (NeoForge `RegisterSpawnPlacementsEvent` vs Fabric). Mobs currently spawn via biome lists + vanilla defaults. +- [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures + (6× `ON_GROUND` light-independent; 3× terraform livestock gated on `GRASS_BLOCK`). Cross-loader + spawn-placement seam (`ModSpawnPlacements.Sink`): NeoForge `RegisterSpawnPlacementsEvent` + (`Operation.REPLACE`) vs Fabric vanilla `SpawnPlacements.register`. Both stable on 26.1.2 + 26.2. + Ruin Warden has no rule (structure/event boss only). --- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java index 8fe5e74..a38743b 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java @@ -53,6 +53,7 @@ public static void registerAll(Sink sink) { sink.register(ModEntities.ALIEN_VILLAGER.get(), AlienVillagerRenderer::new); sink.register(ModEntities.ROCKET.get(), RocketRenderer::new); + sink.register(ModEntities.FALLING_METEOR.get(), FallingMeteorRenderer::new); } private static Identifier tex(String name) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java new file mode 100644 index 0000000..fef38d7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorModel.java @@ -0,0 +1,44 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.model.geom.ModelLayerLocation; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.model.geom.PartPose; +import net.minecraft.client.model.geom.builders.CubeListBuilder; +import net.minecraft.client.model.geom.builders.LayerDefinition; +import net.minecraft.client.model.geom.builders.MeshDefinition; +import net.minecraft.client.model.geom.builders.PartDefinition; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * A lumpy meteor: a chunky charred core with a couple of bumps for an irregular silhouette. Built + * with the 26.1 {@code LayerDefinition} mesh API; the renderer tumbles it and the entity trails fire. + * Authored purely in Java, and (per the cross-loader convention) baked directly from + * {@code createBodyLayer().bakeRoot()} by the renderer, so no model-layer registry is required. + */ +public class FallingMeteorModel extends EntityModel { + + public static final ModelLayerLocation LAYER = new ModelLayerLocation( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "falling_meteor"), "main"); + + public FallingMeteorModel(ModelPart root) { + super(root); + } + + public static LayerDefinition createBodyLayer() { + MeshDefinition mesh = new MeshDefinition(); + PartDefinition root = mesh.getRoot(); + + root.addOrReplaceChild("core", + CubeListBuilder.create().texOffs(0, 0).addBox(-6F, -6F, -6F, 12F, 12F, 12F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + root.addOrReplaceChild("bump", + CubeListBuilder.create().texOffs(0, 28).addBox(3F, -8F, -2F, 6F, 6F, 6F), + PartPose.offset(0.0F, 0.0F, 0.0F)); + + return LayerDefinition.create(mesh, 64, 64); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java new file mode 100644 index 0000000..41cc3f5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderState.java @@ -0,0 +1,10 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.entity.state.EntityRenderState; + +/** Render state for the falling meteor: an age value used to spin the rock. */ +public class FallingMeteorRenderState extends EntityRenderState { + + /** Entity age (ticks + partial) — drives the tumble rotation. */ + public float ticks; +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java new file mode 100644 index 0000000..be39df2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/FallingMeteorRenderer.java @@ -0,0 +1,67 @@ +package za.co.neroland.nerospace.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; + +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.EntityRendererProvider; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.meteor.FallingMeteorEntity; + +/** + * Entity renderer for the falling meteor (meteor-events design §4). Draws {@link FallingMeteorModel} + * via the 26.1 submit pipeline, tumbling it on its age, at full brightness so the molten rock glows + * against the sky. The flame/smoke trail is spawned by the entity itself. + * + *

Cross-loader port note: the model geometry is baked directly from {@code createBodyLayer()} + * (the same approach the rocket + mob renderers use), so no model-layer registry is required.

+ */ +public class FallingMeteorRenderer extends EntityRenderer { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/entity/falling_meteor.png"); + private static final int FULL_BRIGHT = 0x00F000F0; + + private final FallingMeteorModel model; + + public FallingMeteorRenderer(EntityRendererProvider.Context context) { + super(context); + this.model = new FallingMeteorModel(FallingMeteorModel.createBodyLayer().bakeRoot()); + } + + @Override + public FallingMeteorRenderState createRenderState() { + return new FallingMeteorRenderState(); + } + + @Override + public void extractRenderState(FallingMeteorEntity meteor, FallingMeteorRenderState state, float partialTick) { + super.extractRenderState(meteor, state, partialTick); + state.ticks = meteor.tickCount + partialTick; + } + + @Override + public void submit(FallingMeteorRenderState state, PoseStack poseStack, SubmitNodeCollector collector, + CameraRenderState cameraState) { + poseStack.pushPose(); + // Standard entity-model orientation (flip into model space), then tumble on the age so the + // rock spins as it falls. + poseStack.scale(-1.0F, -1.0F, 1.0F); + poseStack.translate(0.0F, -0.7F, 0.0F); + poseStack.mulPose(Axis.YP.rotationDegrees(state.ticks * 7.0F)); + poseStack.mulPose(Axis.XP.rotationDegrees(state.ticks * 5.0F)); + + RenderType renderType = this.model.renderType(TEXTURE); + collector.order(0).submitModel(this.model, state, poseStack, renderType, + FULL_BRIGHT, OverlayTexture.NO_OVERLAY, -1, null, 0, null); + + poseStack.popPose(); + super.submit(state, poseStack, collector, cameraState); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java new file mode 100644 index 0000000..f48a788 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java @@ -0,0 +1,236 @@ +package za.co.neroland.nerospace.meteor; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.InterpolationHandler; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.Vec3; + +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModEntities; + +/** + * A meteor falling from the sky (meteor-events design §4). A non-living, AI-less {@link Entity} + * (like the rocket) that descends on a diagonal arc toward a fixed crater centre, trailing flame and + * smoke, and on contact carves a small crater of {@code meteor_rock} around a loot-bearing + * {@code meteor_core}. All gameplay is server-authoritative; the client only renders the synced + * position + spins the rock and draws the trail. + * + *

Cross-loader port note: this is the creative-slice meteor (spawned on demand by the Meteor + * Caller). The natural-shower scheduler ({@code MeteorEventManager}) and the incoming-tracker HUD are + * deferred to a later batch (they need the meteor config keys + a sync payload), so the crater radius + * is inlined to the root's shipped default (3) and there is no manager call on impact.

+ */ +public class FallingMeteorEntity extends Entity { + + /** Blocks above the target the meteor spawns at. */ + public static final int FALL_HEIGHT = 150; + /** Blocks travelled per tick (fast + dramatic). */ + private static final double SPEED = 1.7D; + /** Inlined from {@code Config.METEOR_CRATER_RADIUS} (root default) until the config seam lands. */ + private static final int CRATER_RADIUS = 3; + + private int targetX; + private int targetY; + private int targetZ; + private long lootSeed; + /** Gallery/showcase only: hover in place (spin + trail) instead of falling. Not persisted. */ + private boolean frozen; + + private final InterpolationHandler interpolation = new InterpolationHandler(this); + + @SuppressWarnings("this-escape") + public FallingMeteorEntity(EntityType type, Level level) { + super(type, level); + this.setNoGravity(true); + this.noPhysics = true; // we step the position manually; no vanilla collision pushback + } + + /** + * Spawns a meteor aimed at {@code target} (a surface block position) with RNG loot from + * {@code seed}. The spawn point is high above the target with a random horizontal offset so the + * descent reads as a diagonal arc. Server-side. + */ + public static FallingMeteorEntity spawn(ServerLevel level, BlockPos target, long seed) { + FallingMeteorEntity meteor = new FallingMeteorEntity(ModEntities.FALLING_METEOR.get(), level); + meteor.targetX = target.getX(); + meteor.targetY = target.getY(); + meteor.targetZ = target.getZ(); + meteor.lootSeed = seed; + + double angle = level.getRandom().nextDouble() * Math.PI * 2.0D; + double offset = FALL_HEIGHT * 0.45D; + double sx = target.getX() + 0.5D + Math.cos(angle) * offset; + double sz = target.getZ() + 0.5D + Math.sin(angle) * offset; + meteor.setPos(sx, target.getY() + FALL_HEIGHT, sz); + level.addFreshEntity(meteor); + level.playSound(null, target, SoundEvents.FIREWORK_ROCKET_LARGE_BLAST_FAR, SoundSource.AMBIENT, 4.0F, 0.6F); + return meteor; + } + + /** Spawns a non-falling meteor that hovers + spins + trails — for the gallery/showcase only. */ + public static FallingMeteorEntity spawnFrozen(ServerLevel level, double x, double y, double z) { + FallingMeteorEntity meteor = new FallingMeteorEntity(ModEntities.FALLING_METEOR.get(), level); + meteor.frozen = true; + meteor.setPos(x, y, z); + level.addFreshEntity(meteor); + return meteor; + } + + @Override + protected void defineSynchedData(net.minecraft.network.syncher.SynchedEntityData.Builder builder) { + // No synced data: the client renders from the tracked position + spins on tickCount. + } + + @Override + public InterpolationHandler getInterpolation() { + return this.interpolation; + } + + private Vec3 targetVec() { + return new Vec3(this.targetX + 0.5D, this.targetY + 0.5D, this.targetZ + 0.5D); + } + + @Override + public void tick() { + super.tick(); + + if (level().isClientSide()) { + if (this.interpolation.hasActiveInterpolation()) { + this.interpolation.interpolate(); + } + spawnTrail(); + return; + } + + if (this.frozen) { + return; // gallery/showcase: hover in place + } + + Vec3 pos = position(); + Vec3 target = targetVec(); + Vec3 delta = target.subtract(pos); + double dist = delta.length(); + + // Impact when we reach the target column or drop to/below the crater surface. + if (dist <= SPEED || pos.y <= this.targetY + 0.5D) { + resolveImpact((ServerLevel) level()); + return; + } + + Vec3 step = delta.scale(SPEED / dist); + this.setDeltaMovement(step); // for client interpolation / rotation cues + this.setPos(pos.x + step.x, pos.y + step.y, pos.z + step.z); + } + + /** Flame + smoke trail, denser as the meteor nears the ground (client-side, per the design §4). */ + private void spawnTrail() { + double proximity = 1.0D - Math.min(1.0D, (getY() - this.targetY) / FALL_HEIGHT); + int puffs = 2 + (int) (proximity * 4); + for (int i = 0; i < puffs; i++) { + double ox = (this.random.nextDouble() - 0.5D) * 0.8D; + double oy = (this.random.nextDouble() - 0.5D) * 0.8D; + double oz = (this.random.nextDouble() - 0.5D) * 0.8D; + level().addParticle(ParticleTypes.FLAME, getX() + ox, getY() + oy, getZ() + oz, 0.0D, 0.02D, 0.0D); + level().addParticle(ParticleTypes.LARGE_SMOKE, getX() + ox, getY() + 0.4D + oy, getZ() + oz, + 0.0D, 0.04D, 0.0D); + } + } + + /** + * Carve a small bowl crater (meteor-events design §4): clears the bowl interior to air, lines the + * floor with {@code meteor_rock}, and seats a loot-bearing {@code meteor_core} at the deepest + * point. Non-destructive beyond the radius, never touches bedrock, no fire or wide explosion. + */ + private void resolveImpact(ServerLevel level) { + int radius = CRATER_RADIUS; + BlockPos center = new BlockPos(this.targetX, this.targetY, this.targetZ); + BlockState rock = ModBlocks.METEOR_ROCK.get().defaultBlockState(); + + for (int dx = -radius - 1; dx <= radius + 1; dx++) { + for (int dz = -radius - 1; dz <= radius + 1; dz++) { + double horiz = Math.sqrt(dx * dx + dz * dz); + if (horiz > radius + 0.6D) { + continue; + } + int depth = Math.max(1, Math.round((float) (radius - horiz) * 0.7F)); + // Clear the open bowl above the floor. + for (int dy = -depth + 1; dy <= radius; dy++) { + if (Math.sqrt(dx * dx + dy * dy + dz * dz) > radius + 0.6D) { + continue; + } + BlockPos p = center.offset(dx, dy, dz); + if (!isProtected(level, p)) { + level.setBlock(p, Blocks.AIR.defaultBlockState(), 3); + } + } + // Line the bowl floor with meteor rock. + BlockPos floor = center.offset(dx, -depth, dz); + if (!isProtected(level, floor)) { + level.setBlock(floor, rock, 3); + } + } + } + + // The loot core sits at the deepest centre point. + int centerDepth = Math.max(1, Math.round(radius * 0.7F)); + BlockPos corePos = center.offset(0, -centerDepth, 0); + if (!isProtected(level, corePos)) { + level.setBlock(corePos, ModBlocks.METEOR_CORE.get().defaultBlockState(), 3); + if (level.getBlockEntity(corePos) instanceof MeteorCoreBlockEntity core) { + core.generateLoot(this.lootSeed); + } + } + + // Impact feedback (no terrain-damaging explosion). + level.sendParticles(ParticleTypes.EXPLOSION_EMITTER, center.getX() + 0.5D, center.getY() + 1.0D, + center.getZ() + 0.5D, 1, 0.0D, 0.0D, 0.0D, 0.0D); + level.sendParticles(ParticleTypes.LARGE_SMOKE, center.getX() + 0.5D, center.getY() + 1.0D, + center.getZ() + 0.5D, 40, radius, 1.0D, radius, 0.02D); + level.playSound(null, center.getX() + 0.5D, center.getY() + 0.5D, center.getZ() + 0.5D, + SoundEvents.GENERIC_EXPLODE, SoundSource.BLOCKS, 6.0F, 0.7F); + + discard(); + } + + /** Bedrock and other unbreakable blocks (destroy speed < 0) are left untouched. */ + private static boolean isProtected(ServerLevel level, BlockPos pos) { + BlockState state = level.getBlockState(pos); + return state.is(Blocks.BEDROCK) || state.getDestroySpeed(level, pos) < 0.0F; + } + + @Override + public boolean isPickable() { + return false; + } + + @Override + public boolean hurtServer(ServerLevel level, net.minecraft.world.damagesource.DamageSource source, float amount) { + return false; // indestructible in flight + } + + @Override + protected void readAdditionalSaveData(ValueInput input) { + this.targetX = input.getIntOr("TargetX", 0); + this.targetY = input.getIntOr("TargetY", 0); + this.targetZ = input.getIntOr("TargetZ", 0); + this.lootSeed = input.getLongOr("LootSeed", 0L); + } + + @Override + protected void addAdditionalSaveData(ValueOutput output) { + output.putInt("TargetX", this.targetX); + output.putInt("TargetY", this.targetY); + output.putInt("TargetZ", this.targetZ); + output.putLong("LootSeed", this.lootSeed); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java new file mode 100644 index 0000000..bd3c3a5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCallerItem.java @@ -0,0 +1,39 @@ +package za.co.neroland.nerospace.meteor; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.context.UseOnContext; + +/** + * Creative-only Meteor Caller (meteor-events design §7): right-click a block to call a meteor down + * onto that spot with freshly rolled RNG loot — the same path natural spawning uses, on demand. + * Functions only for creative-mode players; in survival it does nothing (and says so). + */ +public class MeteorCallerItem extends Item { + + public MeteorCallerItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Player player = context.getPlayer(); + if (player == null || !player.getAbilities().instabuild) { + if (player != null && !context.getLevel().isClientSide()) { + player.sendSystemMessage(Component.translatable("item.nerospace.meteor_caller.creative_only")); + } + return InteractionResult.PASS; + } + + if (context.getLevel() instanceof ServerLevel level) { + BlockPos target = context.getClickedPos(); + FallingMeteorEntity.spawn(level, target, level.getRandom().nextLong()); + player.sendSystemMessage(Component.translatable("item.nerospace.meteor_caller.called")); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java new file mode 100644 index 0000000..323ef47 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlock.java @@ -0,0 +1,39 @@ +package za.co.neroland.nerospace.meteor; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** + * The Meteor Core (meteor-events design §5): the glowing block at the centre of a crater that holds + * the meteor's RNG loot. Break-to-loot — the stored stacks spill when the core is removed, driven by + * {@link MeteorCoreBlockEntity#preRemoveSideEffects} (the block has no loot table, so the rolled + * contents survive rather than a fresh roll). + */ +public class MeteorCoreBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(MeteorCoreBlock::new); + + public MeteorCoreBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new MeteorCoreBlockEntity(pos, state); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java new file mode 100644 index 0000000..cfa080e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorCoreBlockEntity.java @@ -0,0 +1,84 @@ +package za.co.neroland.nerospace.meteor; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.Containers; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The "box in the middle" of a meteor (meteor-events design §5): stores the loot rolled once when + * the meteor lands, so contents are fixed per meteor (no re-roll exploit) and identical for every + * player who reaches it. v1 is break-to-loot — {@link MeteorCoreBlock} spills these stacks when the + * core is broken; a clickable container GUI is a noted follow-up. + * + *

Cross-loader port note: the meteor config keys are not yet ported, so the bonus-roll count is + * inlined to the root's shipped default (3). The full config seam is a deferred batch.

+ */ +public class MeteorCoreBlockEntity extends BlockEntity { + + /** Inlined from {@code Config.METEOR_LOOT_BONUS_ROLLS} (root default) until the config seam lands. */ + private static final int METEOR_LOOT_BONUS_ROLLS = 3; + + private final List loot = new ArrayList<>(); + + public MeteorCoreBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.METEOR_CORE.get(), pos, state); + } + + /** Rolls and stores loot from {@code seed} (idempotent — only the first non-empty roll sticks). */ + public void generateLoot(long seed) { + if (!this.loot.isEmpty()) { + return; + } + this.loot.addAll(MeteorLoot.roll(RandomSource.create(seed), METEOR_LOOT_BONUS_ROLLS)); + setChanged(); + } + + /** + * Break-to-loot: spill the stored stacks the moment the core is removed (mirrors the Station + * Core's charter pop). The block has no loot table, so this is the only drop path — the rolled + * contents survive rather than a fresh roll. + */ + @Override + public void preRemoveSideEffects(BlockPos pos, BlockState state) { + super.preRemoveSideEffects(pos, state); + if (!(this.level instanceof ServerLevel serverLevel)) { + return; + } + for (ItemStack stack : this.loot) { + if (!stack.isEmpty()) { + Containers.dropItemStack(serverLevel, + pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, stack.copy()); + } + } + if (!this.loot.isEmpty()) { + serverLevel.playSound(null, pos, SoundEvents.AMETHYST_BLOCK_BREAK, SoundSource.BLOCKS, 1.0F, 0.7F); + } + this.loot.clear(); + } + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.store("Loot", ItemStack.OPTIONAL_CODEC.listOf(), this.loot); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.loot.clear(); + this.loot.addAll(input.read("Loot", ItemStack.OPTIONAL_CODEC.listOf()).orElse(List.of())); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java new file mode 100644 index 0000000..5e5f3d5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorLoot.java @@ -0,0 +1,70 @@ +package za.co.neroland.nerospace.meteor; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.util.RandomSource; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; + +import za.co.neroland.nerospace.registry.ModItems; + +/** + * RNG contents of a meteor core (meteor-events design §5). Rolling is deterministic for a given + * seed, so the {@link MeteorCoreBlockEntity} can roll once on placement and store the result — all + * players who reach the meteor see identical loot and there is no re-roll exploit. + * + *

v1 keeps the loot table in code (sensible defaults). Every meteor guarantees a handful of + * {@code alien_fragment} (the future scanner feedstock) plus a number of weighted bonus rolls drawn + * from existing raw ores and the rarer alien items.

+ */ +public final class MeteorLoot { + + /** A single weighted entry: an item, how many to give, and its selection weight. */ + private record Entry(ItemLike item, int min, int max, int weight) { + int roll(RandomSource rng) { + return this.min >= this.max ? this.min : this.min + rng.nextInt(this.max - this.min + 1); + } + } + + private MeteorLoot() { + } + + /** The weighted bonus pool (existing ores are common; alien tech/core are the rare prizes). */ + private static List pool() { + List pool = new ArrayList<>(); + pool.add(new Entry(ModItems.RAW_NEROSIUM.get(), 2, 5, 30)); + pool.add(new Entry(ModItems.RAW_NEROSTEEL.get(), 2, 5, 24)); + pool.add(new Entry(ModItems.XERTZ_QUARTZ.get(), 1, 4, 18)); + pool.add(new Entry(ModItems.ALIEN_FRAGMENT.get(), 2, 4, 16)); + pool.add(new Entry(ModItems.ALIEN_TECH_SCRAP.get(), 1, 2, 9)); + pool.add(new Entry(ModItems.ALIEN_CORE.get(), 1, 1, 3)); + return pool; + } + + /** + * Rolls a fresh set of stacks for a meteor core. + * + * @param rng seeded source (use {@code RandomSource.create(seed)} for reproducibility) + * @param bonusRolls number of weighted bonus rolls on top of the guaranteed fragments + */ + public static List roll(RandomSource rng, int bonusRolls) { + List out = new ArrayList<>(); + // Guaranteed: a handful of alien fragments — every meteor seeds the scanner economy. + out.add(new ItemStack(ModItems.ALIEN_FRAGMENT.get(), 3 + rng.nextInt(4))); + + List pool = pool(); + int totalWeight = pool.stream().mapToInt(Entry::weight).sum(); + for (int i = 0; i < bonusRolls; i++) { + int pick = rng.nextInt(totalWeight); + for (Entry e : pool) { + pick -= e.weight(); + if (pick < 0) { + out.add(new ItemStack(e.item(), e.roll(rng))); + break; + } + } + } + return out; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 974d42a..a963b21 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -14,6 +14,7 @@ import za.co.neroland.nerospace.machine.OxygenGeneratorBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; +import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; import za.co.neroland.nerospace.storage.CreativeFluidTankBlockEntity; @@ -111,6 +112,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("quarry_landmark", key -> new BlockEntityType<>(QuarryLandmarkBlockEntity::new, java.util.Set.of(ModBlocks.QUARRY_LANDMARK.get()))); + public static final RegistryEntry> METEOR_CORE = + BLOCK_ENTITIES.register("meteor_core", + key -> new BlockEntityType<>(MeteorCoreBlockEntity::new, java.util.Set.of(ModBlocks.METEOR_CORE.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 8d5447b..00186e0 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -24,6 +24,7 @@ import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlock; import za.co.neroland.nerospace.machine.quarry.QuarryFrameBlock; import za.co.neroland.nerospace.machine.quarry.QuarryLandmarkBlock; +import za.co.neroland.nerospace.meteor.MeteorCoreBlock; import za.co.neroland.nerospace.pipe.UniversalPipeBlock; import za.co.neroland.nerospace.rocket.LaunchGantryBlock; import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; @@ -97,6 +98,11 @@ public final class ModBlocks { .requiresCorrectToolForDrops().lightLevel(s -> 10).sound(SoundType.AMETHYST))); public static final RegistryEntry METEOR_ROCK = block("meteor_rock", p -> p.mapColor(MapColor.COLOR_BLACK).strength(3.0F, 4.0F).requiresCorrectToolForDrops().lightLevel(s -> 3).sound(SoundType.STONE)); + /** The glowing, loot-bearing core at a crater's centre. World-generated only (no block item); breaking it spills the rolled loot. */ + public static final RegistryEntry METEOR_CORE = BLOCKS.register("meteor_core", + key -> new MeteorCoreBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_BLACK).strength(4.0F, 6.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 9).sound(SoundType.AMETHYST))); // Block entity — item storage (pilot for the block-entity + capability seam). public static final RegistryEntry ITEM_STORE = BLOCKS.register("item_store", diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java index bdd9c59..59a00ac 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java @@ -15,6 +15,7 @@ import za.co.neroland.nerospace.entity.RuinWarden; import za.co.neroland.nerospace.entity.WoollyDrift; import za.co.neroland.nerospace.entity.XertzStalker; +import za.co.neroland.nerospace.meteor.FallingMeteorEntity; import za.co.neroland.nerospace.rocket.RocketEntity; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -22,9 +23,8 @@ * Entity types, ported cross-loader through {@link RegistrationProvider} over the vanilla * {@code ENTITY_TYPE} registry (the root used NeoForge's {@code DeferredRegister.Entities}). The * builder's {@code build(ResourceKey)} consumes the key the provider hands the factory. Attributes - * are applied per-loader from {@link ModEntityAttributes}; renderers from - * {@code client/ClientEntityRenderers}. Natural-spawn placement rules are deferred until the planet - * dimensions land (the creatures are summonable meanwhile). + * are applied per-loader from {@link ModEntityAttributes}; natural-spawn placement rules from + * {@link ModSpawnPlacements}; renderers from {@code client/ClientEntityRenderers}. */ public final class ModEntities { @@ -86,6 +86,11 @@ public final class ModEntities { key -> EntityType.Builder.of(RocketEntity::new, MobCategory.MISC) .sized(1.0F, 3.0F).clientTrackingRange(10).build(key)); + public static final RegistryEntry> FALLING_METEOR = ENTITY_TYPES.register( + "falling_meteor", + key -> EntityType.Builder.of(FallingMeteorEntity::new, MobCategory.MISC) + .sized(1.6F, 1.6F).clientTrackingRange(12).build(key)); + private ModEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 764c776..cfbd7af 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -32,6 +32,7 @@ import za.co.neroland.nerospace.item.DestinationCompassItem; import za.co.neroland.nerospace.item.GreenxertzNavigatorItem; import za.co.neroland.nerospace.item.NerospaceSpawnEggItem; +import za.co.neroland.nerospace.meteor.MeteorCallerItem; import za.co.neroland.nerospace.module.ModuleType; import za.co.neroland.nerospace.module.UpgradeModuleItem; import za.co.neroland.nerospace.rocket.RocketItem; @@ -143,6 +144,10 @@ public final class ModItems { public static final RegistryEntry GLACIRA_COMPASS = ITEMS.register("glacira_compass", key -> new DestinationCompassItem(new Item.Properties().stacksTo(1).setId(key), ModDimensions.GLACIRA_LEVEL)); + /** Creative-only Meteor Caller: right-click the ground to call a loot-bearing meteor down on that spot. */ + public static final RegistryEntry METEOR_CALLER = ITEMS.register("meteor_caller", + key -> new MeteorCallerItem(new Item.Properties().stacksTo(1).setId(key))); + // --- Spawn eggs (lazy entity-type supplier; ruin warden is summon-only) ---- public static final RegistryEntry XERTZ_STALKER_SPAWN_EGG = spawnEgg("xertz_stalker_spawn_egg", ModEntities.XERTZ_STALKER); public static final RegistryEntry QUARTZ_CRAWLER_SPAWN_EGG = spawnEgg("quartz_crawler_spawn_egg", ModEntities.QUARTZ_CRAWLER); @@ -255,7 +260,7 @@ public static Map, List> creativeTabItems List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get(), ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get(), GREENXERTZ_NAVIGATOR.get(), STATION_COMPASS.get(), GREENXERTZ_COMPASS.get(), - CINDARA_COMPASS.get(), GLACIRA_COMPASS.get()), + CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get()), CreativeModeTabs.SPAWN_EGGS, List.of(XERTZ_STALKER_SPAWN_EGG.get(), QUARTZ_CRAWLER_SPAWN_EGG.get(), GREENLING_SPAWN_EGG.get(), ALIEN_VILLAGER_SPAWN_EGG.get(), CINDER_STALKER_SPAWN_EGG.get(), diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSpawnPlacements.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSpawnPlacements.java new file mode 100644 index 0000000..589ed57 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSpawnPlacements.java @@ -0,0 +1,64 @@ +package za.co.neroland.nerospace.registry; + +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.SpawnPlacementType; +import net.minecraft.world.entity.SpawnPlacementTypes; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.Heightmap; + +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Cross-loader natural-spawn placement rules for the ported creatures. The loaders apply them + * differently — NeoForge through {@code RegisterSpawnPlacementsEvent} (which adds a REPLACE/AND/OR + * {@code Operation}), Fabric through the vanilla {@code SpawnPlacements#register} static — so this + * exposes each rule (entity type + placement type + heightmap + predicate) through a {@link Sink} + * and lets each loader register it its own way. + * + *

Every creature spawns on solid ground with open space above. The Xertz Stalker (hostile) + * deliberately keeps a light-independent rule so Greenxertz is dangerous day and night; the + * terraform livestock graze only on grassed (matured) ground. The Ruin Warden has no natural rule + * — it is a structure/event boss only. + */ +public final class ModSpawnPlacements { + + /** Receives one placement rule for loader-specific registration. */ + public interface Sink { + void register(EntityType type, SpawnPlacementType placementType, + Heightmap.Types heightmap, SpawnPlacements.SpawnPredicate predicate); + } + + public static void registerAll(Sink sink) { + ground(sink, ModEntities.XERTZ_STALKER); + ground(sink, ModEntities.QUARTZ_CRAWLER); + ground(sink, ModEntities.GREENLING); + ground(sink, ModEntities.CINDER_STALKER); + ground(sink, ModEntities.FROST_STRIDER); + ground(sink, ModEntities.ALIEN_VILLAGER); + grass(sink, ModEntities.MEADOW_LOPER); + grass(sink, ModEntities.EMBER_STRUTTER); + grass(sink, ModEntities.WOOLLY_DRIFT); + } + + /** Solid ground below, open air at the spawn position; light-independent. */ + private static void ground(Sink sink, RegistryEntry> entry) { + sink.register(entry.get(), SpawnPlacementTypes.ON_GROUND, + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, + (type, level, reason, pos, random) -> + !level.getBlockState(pos.below()).isAir() && level.getBlockState(pos).isAir()); + } + + /** Grassed (matured/terraformed) ground only — the reliable runtime signal for mature biomes. */ + private static void grass(Sink sink, RegistryEntry> entry) { + sink.register(entry.get(), SpawnPlacementTypes.ON_GROUND, + Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, + (type, level, reason, pos, random) -> + level.getBlockState(pos.below()).is(Blocks.GRASS_BLOCK) + && level.getBlockState(pos).isAir()); + } + + private ModSpawnPlacements() { + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_core.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_core.json new file mode 100644 index 0000000..9b79e51 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/meteor_core.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/meteor_core" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/meteor_caller.json b/multiloader/common/src/main/resources/assets/nerospace/items/meteor_caller.json new file mode 100644 index 0000000..c6c6f79 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/meteor_caller.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/meteor_caller" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 7a10fab..e12d01d 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -26,6 +26,7 @@ "block.nerospace.launch_gantry": "Launch Gantry", "block.nerospace.launch_gantry.boarded": "Boarded the rocket — strap in", "block.nerospace.launch_gantry.no_rocket": "No rocket on the pad to board", + "block.nerospace.meteor_core": "Meteor Core", "block.nerospace.meteor_rock": "Meteor Rock", "block.nerospace.nerosium_block": "Block of Nerosium", "block.nerospace.nerosium_grinder": "Nerosium Grinder", @@ -107,6 +108,9 @@ "item.nerospace.greenxertz_navigator.return": "Returned to the overworld", "item.nerospace.greenxertz_navigator.travel": "Transported to Greenxertz", "item.nerospace.meadow_loper_spawn_egg": "Meadow Loper Spawn Egg", + "item.nerospace.meteor_caller": "Meteor Caller", + "item.nerospace.meteor_caller.called": "A meteor streaks down from the sky…", + "item.nerospace.meteor_caller.creative_only": "The Meteor Caller only works in creative mode", "item.nerospace.nerosium_dust": "Nerosium Dust", "item.nerospace.nerosium_ingot": "Nerosium Ingot", "item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_core.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_core.json new file mode 100644 index 0000000..83da311 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/meteor_core.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/meteor_core" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_caller.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_caller.json new file mode 100644 index 0000000..2989ddc --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_caller.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/meteor_caller" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_core.png new file mode 100644 index 0000000000000000000000000000000000000000..19d2727f5fef6cdb238b84e264651ba8ef718a32 GIT binary patch literal 509 zcmVsuW?XJi@Y%u=o-70n((&6S&d?s1R6% z2&ufn3}RBIath}T(vnkMq{N&1zFP^=(;u$h3tKuNOPjr z1zHz4R{>l*7{?PKoRLw0BpTxcfcG`ycv^@8tN}te<6K48wRByJa}~$ElG*^g-=7jm z#6mc;?z)y3r!;?z6W_o5Vrw23m5%ohZ0{p4k!f9EZHX}(Vw`w>x=rL=S+OgxBW=wi z$GyV)nx;9VVwo`;hM`Y03wCA2=hv@NERaq^IAd)|HjXEh%8^dy`1box5)p_uB+iDR zPn$MolM;XQZ4!|fuFoms0<27NbCIoz$bXkOKJe!2Pk@CxSN`3Hi&%5s+Vj)xLSTCz z7giSQuS{`s;RWWq2;q#@MG{sjPh@LLGVg1=uTd&bM6t9~8iszcpSkrjd72oflq3mK zNvV8cH0P>xwQEtDdtB=xowWD$V!ZjWIal!)+=urAUCIVP00000NkvXXu0mjfr{Uwl literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png new file mode 100644 index 0000000000000000000000000000000000000000..52a76a0b6aae753b9bafe0cc4ea1974c558ae367 GIT binary patch literal 4078 zcmVMc}m;p(BnwMh|z%eVa?nq8O+w=PHUp?4c~@>UOtdJ)WuSyVy@LJ&E;~%NzB2 zJ4(;U?>gmVtg5<=O>;0NWZ~KDLW{*^OqOL^8V>hjv0iUSlJxD()A)I@xQxY$VxVq! zJ2rr@y4`KfW*3^xF4XOAtFG_V>+R^vG}CfoNlP1f-dDG~tvv6m*V_rrvvCB(n9VL!Rktc9V->|f z&!>-6RkyJ?qb%nkq;7XR)~Kpm4TpQ704(M4>#wmV*EkmDRyZ8)#WRb=Wh2|E|q0ldhz*Fyw^z&)9dYo_GYt-mI)!{s=AE@5lp>*sO9n|%rZ4M<}b^69E@k# zxSMqPZkTws)jIsy=sNa|a1bJgMi6}eZg)54?YPwPydRp&^S*||z1C(iSvDgn%#_K| zHvcb|H_G$Aiek{x6!R^YH{oW?Ti17Sz+P`h)$x&}l^f-6PeK_7r%zfjhk+u8!@Uq% zQ4GReBq@J;q6h!}^W_gV%P$c|Ro&{v=TDmbv9DflM`bzJi_f3*;c~8e|1hM=ducFZ z(^!$4th*9!_HpuE&!>;#0JGUeXp#M3Aqa|f(=~YhXI6yUchI6gg!y&5+giM8g5+#= zp`sY*-R^Egeb*?FnmOwFE;h^iS+=D-?}v#KNTtRK4^E$`s&2#m_+FBR|C~!&#UNjo z!8wM*y@)qorkU#cPG6>3%SxG}|oxhI^g0OH0Ec*HMk+N(nEDMVv&OPYw4t}_4AQ=wk7Ln#)*F5jK zMtFyL7?|}YS4hp?&EMC1d3B}PANw&6c){1?c~67H9?tY&_}lM4B&`7+mnKo)H{}}k z6!h)QQ$6ebC(;myB32-1K9kZM%>3+`v(dF4zy7M1S6BLQIae_~Sp!e$XMQG=xgg7O z-Ut8}aL|*cAqOEIb@5Txs;5J&#)apY!mZ8`Tb}ou=sz0Af!qZ=NT!EH%Lvk~f&H+* z&SG(?ax!lDcN#3nvmCanZevjbna2YE%%(ZK)HYG6agVCHjet2E?lq}-|1kU&0aev) zSd7#JM^B%N#ifd35NX`gHZg`#mh&bJx*54o@h}=gJLz{HC*O5&`eaRL3@j#fGY3Lg z7}~?fQ|ok#Vjo_koQnHqV4glsYf2aR%A5bJ>&7~OJ!Nz zq(Q#y^>)H+um%uR>i0!4h+<>0xNK?9&HlsXJm4>>v99mJe3#1`6~#c`-aHLdh!EKm z^Lo1L^>&n(*3aKRyF98@`C%jM^Mn>a(NR%Lr@eVEP~0iaZK>> z>#wc#Bty7|5M{7LV2cSE9HVmip+AZW#d z%_et1;=~N%jhiNM!Ux6IyWQP2MChbQYT7_+N3JNt(|JH)2L^8Xo^MkK?C6ihToVY$ z&HeG~uWN4daq?ZydjF9|;}Dv(s^(IY_j!5FV}MtuYx5l+wdlK|7&JjJJ%r~>$NHNB zn+whnnCIV3g^~Ncm$V3e6Hcg(k5um;wt{3?&ZW~671NVexsBG-+Qef~l*1|PWy3E% ze~L}P=Q;c0h%eqff3GatS|PQB=#V6xu1HId*dR==P#INqt9t*i;UOJ7ot^|6MK$Yt zB7G7|TJzO+>X2&qCAD*&_k#;?!W3a~u%Z~m#=G5Z)%%BQtP%6V;y|PlKq)5Xkj2d@^p-R(ee4zL0^ z9Up~oorLvh&jCC&r^Qqnm-ybPHaCWLeH*<4DgDUYdgPyssCZKZTSrsa|hq zt(Zb&y zzr(*g%BHcvQ6ZZ4xrr00pnt|HrDGH3kfi6+)p-y&`*3qG2eR`$IDq2$;Xmhkx4YW{cX{4lHAdWL z+?U*x9aTA>$4H+ArD-&7`Mx(f992pUG=BX0>l&8F8eD^Cqia2z)bX<;LAMGor&EE4 z0Fb_k$$fty6%xY4EmEGaC?2JyxmT14{w3h`Hhy; z@BJLO56bQEMcS`-N#TR}+mkS%-~#p!*UDFX5n*DwUb0rlM{5~?!@9(@4 zw7{qZ7>mVauu8baY<8jHKj#sw&qmjh?tzeZem#e$<*5%(oo-qP;B0iQgVQH##g6y< zrFQGx`FyxPhIfclllN5UJ<7B*?dhF&9=7}b*qEZ!CKBm zIBBoJNtM~`LPJTiP89!>eGrxhIc|eg)kAJ&as^gFh|lV-jF`CY^yo@llK zxuI3xa0*n|7{$-xJ}!6$==F9~j>Zx3u^i0P17kKCOSZn7zw?f%Tl%dM_T&mBmbs!j zY1&H(Nw3wbUn=k?Ic<@Z?giTx`!sTZ#RJ@2uF+obYI22DCD_;FE(uPdO zP4MI7dn~-EsY&|t`+sSk^{SFogyz!*2mvs?bOr8k0r}knaDkh$Lt)nNpj{M$7Hr2# zU=bc~I#N@- z(9=U}l${^`^=p6+4)<~Ly#-QIsY=Zi8irqfic+yl0g$BP&vnW+l7MM)I;%Gjlz5Sk%^Nj@FmseLUW$-@? z5Bu}R4n)k^=sG%FDE2{b`A}|5NZBKY z!`=@_!5nLc0Mgv)BxK5}`hi^PDZ}%>qH<9T8h+tE!)#)0TnXugi!5vY_-XNK-12|V z5d5`^q}Ii2=C13zh7{%C9LTYNlN}!IUtV2>S$YJgI$JC*TYo~wduOBT`0N>GHoFL- z!x0@O?g#g4GR%*r>-tXB@lj(<_lK9HVtTU1FAj%$4G==R1RoY~Sj<3B{+Cw-EWh3| z9Pa7aq;A1fsT~U#Ve+GK%ui-wZtpNrV14-4udQY)v4T582yzqE$9F-9czK$ZnVYo2 zJ8tr+nR?*?jqPi=TQNrpb-ns@CHS+>VDUY52riYn5f=W?C-%z^9>6eD1i^xi3e#X1 z@!6U;XyxX)D6EniA*lBQT;Z zo^|u`GBy^z|MNQLM5|~Ejq(55+V@dCMVjB=V;#bDvL|Ar*6y6C~z2xH&%m-^4rp z-{Q+I(hQfs)Hfgg|6f|>LED5kOfwi$b}o2yn4c#f$dTYR?okmxq4DC+`X3*fi^LVA poi=f=5Mq;3_v2~cZWCi*ILp^J`?lEJ`#=jAJYD@<);T3K0RW^kF@*pC literal 0 HcmV?d00001 diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 5d28b43..1a7b365 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -12,7 +12,12 @@ import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.SpawnPlacementType; +import net.minecraft.world.entity.SpawnPlacements; import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; import net.minecraft.world.level.levelgen.placement.PlacedFeature; import za.co.neroland.nerospace.NerospaceCommon; @@ -21,6 +26,7 @@ import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModEntityAttributes; +import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.world.OxygenManager; /** @@ -62,6 +68,15 @@ public void onInitialize() { // Default attributes for the ported mobs (counterpart to NeoForge's EntityAttributeCreationEvent). ModEntityAttributes.forEach(FabricDefaultAttributeRegistry::register); + // Natural-spawn placement rules (counterpart to NeoForge's RegisterSpawnPlacementsEvent). + ModSpawnPlacements.registerAll(new ModSpawnPlacements.Sink() { + @Override + public void register(EntityType type, SpawnPlacementType placementType, + Heightmap.Types heightmap, SpawnPlacements.SpawnPredicate predicate) { + SpawnPlacements.register(type, placementType, heightmap, predicate); + } + }); + // Oxygen survival: register the attachment + tick each player per world tick (airless-planet drain). FabricAttachments.init(); FabricNetwork.registerCommon(); diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 721d678..f53f90d 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -7,12 +7,19 @@ import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; +import net.neoforged.neoforge.event.entity.RegisterSpawnPlacementsEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.SpawnPlacementType; +import net.minecraft.world.entity.SpawnPlacements; +import net.minecraft.world.level.levelgen.Heightmap; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; import za.co.neroland.nerospace.registry.ModEntityAttributes; +import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; import za.co.neroland.nerospace.world.OxygenManager; @@ -46,9 +53,21 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { // Nerospace tab registered via the vanilla CREATIVE_MODE_TAB registry), so no NeoForge-specific // BuildCreativeModeTabContentsEvent injection is needed. modEventBus.addListener(this::onCreateEntityAttributes); + modEventBus.addListener(this::onRegisterSpawnPlacements); } private void onCreateEntityAttributes(EntityAttributeCreationEvent event) { ModEntityAttributes.forEach((type, builder) -> event.put(type, builder.build())); } + + private void onRegisterSpawnPlacements(RegisterSpawnPlacementsEvent event) { + ModSpawnPlacements.registerAll(new ModSpawnPlacements.Sink() { + @Override + public void register(EntityType type, SpawnPlacementType placementType, + Heightmap.Types heightmap, SpawnPlacements.SpawnPredicate predicate) { + event.register(type, placementType, heightmap, predicate, + RegisterSpawnPlacementsEvent.Operation.REPLACE); + } + }); + } } From 3c73aa1acde71c93b974ff0040fd307d3adf2215 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:34:31 +0200 Subject: [PATCH 45/82] Add meteor scheduler and site tracking Port natural meteor shower scheduler and tracking into the multiloader: add MeteorEventManager (SavedData), MeteorSite, and MeteorEvents driver to tick per-level schedulers on eligible surface dimensions. Wire MeteorEvents.tick into Fabric and NeoForge server-tick hooks and have FallingMeteorEntity report impacts via MeteorEventManager.onImpact so scheduled sites flip to LANDED (or a transient landed site is added for creative spawns). Update docs checklist and remove a temporary javap probe task from common build.gradle. Notes: pacing defaults are inlined until the config seam lands, and SavedDataType uses the 4-arg ctor with null DataFixTypes for new mod data. --- docs/MULTILOADER_PORT_CHECKLIST.md | 22 +- multiloader/common/build.gradle | 12 - .../nerospace/meteor/FallingMeteorEntity.java | 3 + .../nerospace/meteor/MeteorEventManager.java | 219 ++++++++++++++++++ .../nerospace/meteor/MeteorEvents.java | 44 ++++ .../neroland/nerospace/meteor/MeteorSite.java | 41 ++++ .../nerospace/fabric/NerospaceFabric.java | 7 +- .../nerospace/neoforge/NerospaceNeoForge.java | 4 + 8 files changed, 334 insertions(+), 18 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 2176db2..9606ebf 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~159 classes ported, ~105 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~162 classes ported, ~102 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — meteor natural-shower scheduler ported.** All 4 cells green. Added `meteor/{MeteorSite, +> MeteorEventManager (multiloader's first SavedData), MeteorEvents}` + per-loader server-tick wiring +> (NeoForge `ServerTickEvent.Post`, Fabric `END_SERVER_TICK`). Meteors now fall naturally on the 4 surface +> dims. **26.x `SavedDataType` is 4-arg only on NeoForm** (Identifier, Supplier, Codec, DataFixTypes=null) — +> the 3-arg the standalone mod uses is a NeoForge convenience (found via the javap probe). Tracker HUD +> (item + sync payload + client readout) is the deferred networking-consumer follow-up. + > **2026-06-21 update — meteor creative slice ported.** All 4 cells green. Added `meteor/{FallingMeteorEntity, > MeteorCoreBlock, MeteorCoreBlockEntity, MeteorCallerItem, MeteorLoot}` + client `{FallingMeteorModel, > FallingMeteorRenderer, FallingMeteorRenderState}` (bake-direct). Creative Meteor Caller → falling meteor → @@ -143,9 +150,16 @@ checked by a headless build). `FALLING_METEOR` entity, `METEOR_CORE` block+BE (no block item — world-gen only), `METEOR_CALLER` item (TOOLS tab) + renderer; copied 3 textures + 4 asset JSON + 4 lang keys. Config meteor keys inlined (crater radius 3, bonus rolls 3). All 4 cells green. -- [ ] **Natural showers + tracker** (deferred) — `MeteorEventManager` (SavedData scheduler), - `MeteorEvents` (per-loader tick hook), `MeteorSite`, + client `ClientMeteorTracker` HUD and a - `MeteorSyncPayload` (uses the networking seam). Needs the meteor config keys (spawn pacing) too. +- [x] **Natural showers (scheduler)** — `MeteorSite` + `MeteorEventManager` (the multiloader's first + `SavedData`) + cross-loader `MeteorEvents.tick(MinecraftServer)` driving the per-level scheduler on the + 4 surface dims (overworld + Greenxertz + Cindara + Glacira); wired into NeoForge `ServerTickEvent.Post` + and Fabric `END_SERVER_TICK`; `FallingMeteorEntity` re-wired to call `onImpact`. Meteor pacing inlined + (avg 9000s, warn 30s, 200–500 blocks, ≤4 active). **26.x gotcha: `SavedDataType` on pure-vanilla NeoForm + has only the 4-arg ctor `(Identifier, Supplier, Codec, DataFixTypes)`** — the standalone mod's 3-arg call + is a NeoForge convenience; pass `null` DataFixTypes (new mod data, no datafixer schema). All 4 cells green. +- [ ] **Tracker HUD** (deferred) — `ModItems.METEOR_TRACKER` item + `MeteorSyncPayload` (clientbound, via + the networking seam) pushed to tracker holders + `ClientMeteorTracker` + a client-tick readout. The + first real networking-seam consumer; `MeteorEventManager.nearestSite` is already in place for it. ### Star Guide / progression (`progression/` 5 + client + item) - [ ] `StarGuide`, `StarGuideProgress`, `StarGuideBlock`(+BE), `StarGuideMenu` + screen, hologram BER, diff --git a/multiloader/common/build.gradle b/multiloader/common/build.gradle index be8c0bd..46c4075 100644 --- a/multiloader/common/build.gradle +++ b/multiloader/common/build.gradle @@ -18,15 +18,3 @@ neoForge { accessTransformers.from(at.absolutePath) } } - -// TEMP probe (remove after): EntityType.Builder.build signature in 26.x. -def probeCp = configurations.compileClasspath -tasks.register('probeEntity') { - doLast { - def cp = probeCp.asPath - def p = ['javap', '-p', '-classpath', cp, 'net.minecraft.world.entity.EntityType$Builder'].execute() - def out = new StringBuilder(); def err = new StringBuilder() - p.consumeProcessOutput(out, err); p.waitFor() - out.toString().readLines().findAll { it.contains('build') || it.contains(' of(') }.each { println it } - } -} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java index f48a788..0c01cd0 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java @@ -199,6 +199,9 @@ private void resolveImpact(ServerLevel level) { level.playSound(null, center.getX() + 0.5D, center.getY() + 0.5D, center.getZ() + 0.5D, SoundEvents.GENERIC_EXPLODE, SoundSource.BLOCKS, 6.0F, 0.7F); + // Flip the matching scheduled site to LANDED (or add a transient one for a creative-called meteor). + MeteorEventManager.get(level).onImpact(center); + discard(); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java new file mode 100644 index 0000000..f9b0b30 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEventManager.java @@ -0,0 +1,219 @@ +package za.co.neroland.nerospace.meteor; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Per-{@link ServerLevel} driver + persistent state for natural meteor events (meteor-events design + * §3). Holds the live impact sites and a cooldown, schedules a rare meteor near a random online + * player when the cooldown elapses, advances each site (SCHEDULED → spawns the falling entity → + * LANDED), and answers "nearest site" for the tracker. + * + *

Cross-loader port note: the first {@link SavedData} in the multiloader (vanilla + * {@code SavedDataType} codec — only the sites + cooldown persist; everything reconverges from them on + * load). The meteor pacing config keys are not yet ported, so they are inlined to the root's shipped + * defaults; the config seam is a deferred incremental batch. {@link #nearestSite} is consumed by the + * deferred tracker batch (item + sync payload + client HUD).

+ */ +public final class MeteorEventManager extends SavedData { + + public static final Identifier ID = Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "meteor_events"); + + // 26.x NeoForm (pure vanilla) exposes only the 4-arg ctor (Identifier, Supplier, Codec, + // DataFixTypes); the 3-arg form the standalone NeoForge mod uses is a loader convenience. New mod + // data has no datafixer schema, so DataFixTypes is null (no fixes to apply). + public static final SavedDataType TYPE = + new SavedDataType<>(ID, MeteorEventManager::new, codec(), null); + + // --- Inlined from Config (root shipped defaults) until the config seam lands --- + /** Whether meteors fall naturally near players. */ + private static final boolean NATURAL_SPAWN = true; + /** Max simultaneous scheduled/falling meteors tracked per dimension. */ + private static final int MAX_ACTIVE_SITES = 4; + /** Warning window (seconds) a meteor is tracked as 'incoming' before it falls. */ + private static final int WARNING_SECONDS = 30; + /** Average seconds between natural impacts on an eligible dimension with players online (~2.5h). */ + private static final int AVG_INTERVAL_SECONDS = 9000; + /** Minimum horizontal distance (blocks) from the anchor player a meteor targets. */ + private static final int MIN_DISTANCE = 200; + /** Maximum horizontal distance (blocks) from the anchor player a meteor targets. */ + private static final int MAX_DISTANCE = 500; + + /** Ticks a landed site lingers so the tracker can still lead players to a fresh crater (5 min). */ + private static final int LANDED_EXPIRY_TICKS = 6000; + /** Failsafe: drop a FALLING site if its entity never reports impact (e.g. unloaded). */ + private static final int FALLING_TIMEOUT_TICKS = 600; + + private final List sites; + private int cooldown; + + public MeteorEventManager() { + this(new ArrayList<>(), 0); + } + + private MeteorEventManager(List sites, int cooldown) { + this.sites = new ArrayList<>(sites); + this.cooldown = cooldown; + } + + private static Codec codec() { + return RecordCodecBuilder.create(inst -> inst.group( + MeteorSite.CODEC.listOf().fieldOf("sites").forGetter(m -> m.sites), + Codec.INT.fieldOf("cooldown").forGetter(m -> m.cooldown) + ).apply(inst, MeteorEventManager::new)); + } + + public static MeteorEventManager get(ServerLevel level) { + return level.getDataStorage().computeIfAbsent(TYPE); + } + + // --- Tick driver -------------------------------------------------------- + + /** One server tick on an eligible dimension (called from {@link MeteorEvents}). */ + public void tick(ServerLevel level) { + boolean dirty = scheduleIfDue(level); + dirty |= advanceSites(level); + if (dirty) { + setDirty(); + } + } + + private boolean scheduleIfDue(ServerLevel level) { + if (!NATURAL_SPAWN || level.players().isEmpty()) { + return false; + } + if (this.cooldown > 0) { + this.cooldown--; + return this.cooldown % 200 == 0; // persist roughly once every 10s of countdown + } + // Cooldown elapsed: schedule one meteor near a random online player (rarity is global per level). + this.cooldown = nextInterval(level); + if (countByState(MeteorSite.SCHEDULED, MeteorSite.FALLING) >= MAX_ACTIVE_SITES) { + return true; + } + ServerPlayer anchor = level.players().get(level.getRandom().nextInt(level.players().size())); + BlockPos target = pickTarget(level, anchor); + if (target != null) { + this.sites.add(new MeteorSite(target.asLong(), MeteorSite.SCHEDULED, + Math.max(1, WARNING_SECONDS * 20))); + } + return true; + } + + private boolean advanceSites(ServerLevel level) { + boolean dirty = false; + Iterator it = this.sites.iterator(); + while (it.hasNext()) { + MeteorSite site = it.next(); + switch (site.state) { + case MeteorSite.SCHEDULED -> { + if (--site.timer <= 0) { + FallingMeteorEntity.spawn(level, site.blockPos(), level.getRandom().nextLong()); + site.state = MeteorSite.FALLING; + site.timer = FALLING_TIMEOUT_TICKS; + dirty = true; + } + } + case MeteorSite.FALLING -> { + if (--site.timer <= 0) { + it.remove(); // failsafe: entity never impacted + dirty = true; + } + } + case MeteorSite.LANDED -> { + if (--site.timer <= 0) { + it.remove(); + dirty = true; + } + } + default -> it.remove(); + } + } + return dirty; + } + + /** Called by {@link FallingMeteorEntity} on impact: flip the matching site to LANDED (or add one). */ + public void onImpact(BlockPos pos) { + for (MeteorSite site : this.sites) { + if (site.state != MeteorSite.LANDED && site.blockPos().closerThan(pos, 8.0D)) { + site.state = MeteorSite.LANDED; + site.timer = LANDED_EXPIRY_TICKS; + site.pos = pos.asLong(); + setDirty(); + return; + } + } + // Creative-spawned (or unscheduled) meteor: add a transient landed site for the tracker. + this.sites.add(new MeteorSite(pos.asLong(), MeteorSite.LANDED, LANDED_EXPIRY_TICKS)); + setDirty(); + } + + /** Nearest tracked site to {@code from} (any state), or {@code null} if none. For the tracker. */ + @Nullable + public MeteorSite nearestSite(BlockPos from) { + MeteorSite best = null; + double bestSq = Double.MAX_VALUE; + for (MeteorSite site : this.sites) { + double sq = site.blockPos().distSqr(from); + if (sq < bestSq) { + bestSq = sq; + best = site; + } + } + return best; + } + + // --- Helpers ------------------------------------------------------------ + + private int countByState(int... states) { + int n = 0; + for (MeteorSite site : this.sites) { + for (int s : states) { + if (site.state == s) { + n++; + break; + } + } + } + return n; + } + + private static int nextInterval(ServerLevel level) { + int avg = Math.max(1, AVG_INTERVAL_SECONDS) * 20; + // Spread 0.66x .. 1.33x of the average so impacts feel irregular. + return (int) (avg * 0.66D) + level.getRandom().nextInt(Math.max(1, (int) (avg * 0.67D))); + } + + @Nullable + private static BlockPos pickTarget(ServerLevel level, ServerPlayer anchor) { + int min = Math.max(0, MIN_DISTANCE); + int max = Math.max(min + 1, MAX_DISTANCE); + double angle = level.getRandom().nextDouble() * Math.PI * 2.0D; + double d = min + level.getRandom().nextDouble() * (max - min); + int x = (int) Math.floor(anchor.getX() + Math.cos(angle) * d); + int z = (int) Math.floor(anchor.getZ() + Math.sin(angle) * d); + // getHeight loads/generates the target chunk — acceptable for a rare event. + int surfaceAir = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z); + int groundY = surfaceAir - 1; + if (groundY <= level.getMinY() + 1) { + return null; // void / no terrain + } + return new BlockPos(x, groundY, z); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java new file mode 100644 index 0000000..137032c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java @@ -0,0 +1,44 @@ +package za.co.neroland.nerospace.meteor; + +import java.util.Set; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** + * Cross-loader server-side driver for natural meteor events (meteor-events design §3/§6). Each loader + * calls {@link #tick(MinecraftServer)} once per server tick from its own hook (NeoForge + * {@code ServerTickEvent.Post}, Fabric {@code ServerTickEvents.END_SERVER_TICK}); this ticks the + * per-level {@link MeteorEventManager} on the eligible surface dimensions. Cheap when idle — the + * manager short-circuits with no players online. + * + *

Cross-loader port note: the tracker push (nearest-site sync to Meteor Tracker holders) is a + * deferred follow-up — it needs the Meteor Tracker item + a clientbound sync payload + a client HUD. + * This batch is the server-side scheduler only; the {@link MeteorEventManager#nearestSite} hook is + * already in place for it.

+ */ +public final class MeteorEvents { + + /** Surface worlds meteors fall on (the void station is excluded — nothing to crater). */ + public static final Set> METEOR_DIMENSIONS = Set.of( + Level.OVERWORLD, + ModDimensions.GREENXERTZ_LEVEL, + ModDimensions.CINDARA_LEVEL, + ModDimensions.GLACIRA_LEVEL); + + private MeteorEvents() { + } + + /** Ticks the meteor scheduler on every eligible loaded dimension. */ + public static void tick(MinecraftServer server) { + for (ServerLevel level : server.getAllLevels()) { + if (METEOR_DIMENSIONS.contains(level.dimension())) { + MeteorEventManager.get(level).tick(level); + } + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java new file mode 100644 index 0000000..3526533 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorSite.java @@ -0,0 +1,41 @@ +package za.co.neroland.nerospace.meteor; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.core.BlockPos; + +/** + * One tracked meteor impact site (meteor-events design §3). Mutable so the {@link MeteorEventManager} + * can advance its state/timer in place each tick. {@code timer} means "ticks until the fall" while + * {@link #SCHEDULED}, and "ticks until this record expires" once {@link #LANDED}. + */ +public final class MeteorSite { + + /** Scheduled near a player; the tracker shows it as incoming during the warning window. */ + public static final int SCHEDULED = 0; + /** The meteor entity is descending. */ + public static final int FALLING = 1; + /** Landed — the crater + loot core exist; kept briefly so the tracker leads players in. */ + public static final int LANDED = 2; + + public static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( + Codec.LONG.fieldOf("pos").forGetter(s -> s.pos), + Codec.INT.fieldOf("state").forGetter(s -> s.state), + Codec.INT.fieldOf("timer").forGetter(s -> s.timer) + ).apply(inst, MeteorSite::new)); + + public long pos; + public int state; + public int timer; + + public MeteorSite(long pos, int state, int timer) { + this.pos = pos; + this.state = state; + this.timer = timer; + } + + public BlockPos blockPos() { + return BlockPos.of(this.pos); + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 1a7b365..45a91db 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -24,6 +24,7 @@ import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; @@ -80,8 +81,10 @@ public void register(EntityType type, SpawnPlacementType plac // Oxygen survival: register the attachment + tick each player per world tick (airless-planet drain). FabricAttachments.init(); FabricNetwork.registerCommon(); - ServerTickEvents.END_SERVER_TICK.register(server -> - server.getPlayerList().getPlayers().forEach(OxygenManager::tick)); + ServerTickEvents.END_SERVER_TICK.register(server -> { + server.getPlayerList().getPlayers().forEach(OxygenManager::tick); + MeteorEvents.tick(server); + }); // Item-storage capability (Fabric Transfer API) — counterpart to NeoForge // Capabilities.Item.BLOCK; lets mod pipes move items in/out of the item store. diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index f53f90d..6e840e8 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -9,6 +9,7 @@ import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import net.neoforged.neoforge.event.entity.RegisterSpawnPlacementsEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; +import net.neoforged.neoforge.event.tick.ServerTickEvent; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.Mob; @@ -17,6 +18,7 @@ import net.minecraft.world.level.levelgen.Heightmap; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; @@ -49,6 +51,8 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { OxygenManager.tick(serverPlayer); } }); + // Natural meteor showers: tick the per-level scheduler once per server tick. + NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> MeteorEvents.tick(event.getServer())); // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated // Nerospace tab registered via the vanilla CREATIVE_MODE_TAB registry), so no NeoForge-specific // BuildCreativeModeTabContentsEvent injection is needed. From 9a7cf6d5f885da32d44ea142298c11e1fe301530 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:43:59 +0200 Subject: [PATCH 46/82] Add Meteor Tracker HUD, networking, and item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the Meteor Tracker subsystem: add server→client MeteorSyncPayload (first multiloader networking payload) and register its client handler in ModNetwork.init(); add ClientMeteorTracker (client-side data holder) and MeteorTrackerHud (action-bar readout using Player.sendOverlayMessage). Send nearest-site snapshots from MeteorEvents to players holding the tracker every 10 ticks. Register the METEOR_TRACKER item and place it into the creative tab; include item model, texture and localization entries. Wire client tick hooks for Fabric and NeoForge to drive the HUD. Also update the multiloader port checklist docs to mark the Tracker HUD ported. --- docs/MULTILOADER_PORT_CHECKLIST.md | 19 ++++-- .../nerospace/client/ClientMeteorTracker.java | 43 +++++++++++++ .../nerospace/client/MeteorTrackerHud.java | 60 ++++++++++++++++++ .../nerospace/meteor/MeteorEvents.java | 36 ++++++++++- .../nerospace/network/MeteorSyncPayload.java | 38 +++++++++++ .../nerospace/network/ModNetwork.java | 6 +- .../neroland/nerospace/registry/ModItems.java | 5 +- .../nerospace/items/meteor_tracker.json | 6 ++ .../assets/nerospace/lang/en_us.json | 5 ++ .../nerospace/models/item/meteor_tracker.json | 6 ++ .../textures/item/meteor_tracker.png | Bin 0 -> 208 bytes .../fabric/NerospaceFabricClient.java | 5 ++ .../neoforge/NeoForgeClientSetup.java | 5 ++ 13 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeteorTrackerHud.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/meteor_tracker.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_tracker.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 9606ebf..8a7f5d1 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~162 classes ported, ~102 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~165 classes ported, ~99 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — meteor Tracker HUD ported (networking seam proven end-to-end).** All 4 cells green. +> Added `network/MeteorSyncPayload` (multiloader's FIRST payload), `client/{ClientMeteorTracker, +> MeteorTrackerHud}`, `ModItems.METEOR_TRACKER`; registered clientbound in `ModNetwork.init()` (both loader +> seams auto-wire it), pushed from `MeteorEvents` to tracker holders every 10t, readout on the action bar via +> per-loader client-tick hooks. **26.x gotcha: `Gui.setOverlayMessage(Component, boolean)` is gone from vanilla +> Gui → use `Player.sendOverlayMessage(Component)`** (probed). The meteor subsystem is now fully ported. + > **2026-06-21 update — meteor natural-shower scheduler ported.** All 4 cells green. Added `meteor/{MeteorSite, > MeteorEventManager (multiloader's first SavedData), MeteorEvents}` + per-loader server-tick wiring > (NeoForge `ServerTickEvent.Post`, Fabric `END_SERVER_TICK`). Meteors now fall naturally on the 4 surface @@ -157,9 +164,13 @@ checked by a headless build). (avg 9000s, warn 30s, 200–500 blocks, ≤4 active). **26.x gotcha: `SavedDataType` on pure-vanilla NeoForm has only the 4-arg ctor `(Identifier, Supplier, Codec, DataFixTypes)`** — the standalone mod's 3-arg call is a NeoForge convenience; pass `null` DataFixTypes (new mod data, no datafixer schema). All 4 cells green. -- [ ] **Tracker HUD** (deferred) — `ModItems.METEOR_TRACKER` item + `MeteorSyncPayload` (clientbound, via - the networking seam) pushed to tracker holders + `ClientMeteorTracker` + a client-tick readout. The - first real networking-seam consumer; `MeteorEventManager.nearestSite` is already in place for it. +- [x] **Tracker HUD** — `ModItems.METEOR_TRACKER` item + `network/MeteorSyncPayload` (the multiloader's + **first networking payload** — registered clientbound in `ModNetwork.init()`, auto-wired by both loader + seams) pushed to tracker holders every 10t from `MeteorEvents` + `client/ClientMeteorTracker` (data + holder) + `client/MeteorTrackerHud` (action-bar readout via `Player.sendOverlayMessage`) driven by + per-loader client-tick hooks (NeoForge `ClientTickEvent.Post`, Fabric `END_CLIENT_TICK`). **26.x gotcha: + `Gui.setOverlayMessage(Component, boolean)` (the standalone mod's call) is gone from vanilla `Gui` — + use `Player.sendOverlayMessage(Component)`** (probed). Proves the networking seam end-to-end. All 4 cells green. ### Star Guide / progression (`progression/` 5 + client + item) - [ ] `StarGuide`, `StarGuideProgress`, `StarGuideBlock`(+BE), `StarGuideMenu` + screen, hologram BER, diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java new file mode 100644 index 0000000..6c13129 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientMeteorTracker.java @@ -0,0 +1,43 @@ +package za.co.neroland.nerospace.client; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.core.BlockPos; + +import za.co.neroland.nerospace.network.MeteorSyncPayload; + +/** + * Client-side holder for the latest nearest-meteor snapshot (meteor-events design §6). Fed by + * {@link MeteorSyncPayload} (the clientbound handler registered in {@code ModNetwork.init()}); read by + * {@link MeteorTrackerHud} each client tick. Pure data — no client-only imports — so it loads safely + * even where the handler is registered from common code. + */ +public final class ClientMeteorTracker { + + private static boolean present; + @Nullable + private static BlockPos pos; + private static int state; + + private ClientMeteorTracker() { + } + + public static void accept(MeteorSyncPayload payload) { + present = payload.present(); + pos = payload.present() ? BlockPos.of(payload.pos()) : null; + state = payload.state(); + } + + public static boolean isPresent() { + return present && pos != null; + } + + @Nullable + public static BlockPos pos() { + return pos; + } + + public static int state() { + return state; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeteorTrackerHud.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeteorTrackerHud.java new file mode 100644 index 0000000..2e4d13e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/MeteorTrackerHud.java @@ -0,0 +1,60 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.phys.Vec3; + +import za.co.neroland.nerospace.meteor.MeteorSite; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Meteor Tracker readout (meteor-events design §6): while the player holds a Meteor Tracker, show the + * nearest meteor's state (incoming / landed), compass heading and distance in the action bar. Purely + * presentational — the data arrives server-authoritatively via {@link ClientMeteorTracker}; this just + * draws it. + * + *

Cross-loader port note: {@link #tick()} is called once per client tick from each loader's own + * client-tick hook (NeoForge {@code ClientTickEvent.Post} on the game bus, Fabric + * {@code ClientTickEvents.END_CLIENT_TICK}). Client-only — never loaded on a dedicated server.

+ */ +public final class MeteorTrackerHud { + + private static final String[] COMPASS_8 = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"}; + + private MeteorTrackerHud() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + if (mc.player == null || mc.level == null || mc.isPaused()) { + return; + } + boolean holding = mc.player.getMainHandItem().is(ModItems.METEOR_TRACKER.get()) + || mc.player.getOffhandItem().is(ModItems.METEOR_TRACKER.get()); + if (!holding) { + return; + } + if (!ClientMeteorTracker.isPresent()) { + mc.player.sendOverlayMessage(Component.translatable("item.nerospace.meteor_tracker.none")); + return; + } + BlockPos target = ClientMeteorTracker.pos(); + if (target == null) { + // isPresent() was true above, but pos() is @Nullable — guard the deref explicitly. + return; + } + Vec3 p = mc.player.position(); + double dx = target.getX() + 0.5D - p.x; + double dz = target.getZ() + 0.5D - p.z; + int dist = (int) Math.round(Math.sqrt(dx * dx + dz * dz)); + // Bearing where North = -Z, East = +X (Minecraft convention). + double deg = (Math.toDegrees(Math.atan2(dx, -dz)) + 360.0D) % 360.0D; + String heading = COMPASS_8[(int) Math.round(deg / 45.0D) & 7]; + Component state = Component.translatable(ClientMeteorTracker.state() == MeteorSite.LANDED + ? "item.nerospace.meteor_tracker.landed" + : "item.nerospace.meteor_tracker.incoming"); + mc.player.sendOverlayMessage( + Component.translatable("item.nerospace.meteor_tracker.readout", state, heading, dist)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java index 137032c..d75c233 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java @@ -5,9 +5,14 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import za.co.neroland.nerospace.network.MeteorSyncPayload; +import za.co.neroland.nerospace.network.ModNetwork; import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.registry.ModItems; /** * Cross-loader server-side driver for natural meteor events (meteor-events design §3/§6). Each loader @@ -30,15 +35,40 @@ public final class MeteorEvents { ModDimensions.CINDARA_LEVEL, ModDimensions.GLACIRA_LEVEL); + /** How often the nearest-site snapshot is pushed to tracker holders. */ + private static final int SYNC_INTERVAL_TICKS = 10; + private MeteorEvents() { } - /** Ticks the meteor scheduler on every eligible loaded dimension. */ + /** Ticks the meteor scheduler on every eligible loaded dimension, and syncs the tracker readout. */ public static void tick(MinecraftServer server) { for (ServerLevel level : server.getAllLevels()) { - if (METEOR_DIMENSIONS.contains(level.dimension())) { - MeteorEventManager.get(level).tick(level); + if (!METEOR_DIMENSIONS.contains(level.dimension())) { + continue; + } + MeteorEventManager manager = MeteorEventManager.get(level); + manager.tick(level); + + if (level.getGameTime() % SYNC_INTERVAL_TICKS == 0) { + for (ServerPlayer player : level.players()) { + if (!holdsTracker(player)) { + continue; + } + MeteorSite nearest = manager.nearestSite(player.blockPosition()); + ModNetwork.sendToPlayer(player, nearest == null + ? MeteorSyncPayload.ABSENT + : new MeteorSyncPayload(true, nearest.pos, nearest.state)); + } } } } + + private static boolean holdsTracker(ServerPlayer player) { + return isTracker(player.getMainHandItem()) || isTracker(player.getOffhandItem()); + } + + private static boolean isTracker(ItemStack stack) { + return stack.is(ModItems.METEOR_TRACKER.get()); + } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java new file mode 100644 index 0000000..f7f0413 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/MeteorSyncPayload.java @@ -0,0 +1,38 @@ +package za.co.neroland.nerospace.network; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Server → client nearest-meteor snapshot for the Meteor Tracker (meteor-events design §6). Pushed + * only to players holding a tracker. {@code present} false means "no tracked meteor" (the readout + * idles); otherwise the packed position + {@link za.co.neroland.nerospace.meteor.MeteorSite} state + * let the client draw direction, distance and incoming/landed status. + * + *

Cross-loader port note: the multiloader's first networking payload — registered (with its + * client handler {@code ClientMeteorTracker::accept}) in {@code ModNetwork.init()}; both loaders' + * networking seams pick it up from the {@code ModNetwork.clientbound()} list automatically.

+ */ +public record MeteorSyncPayload(boolean present, long pos, int state) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "meteor_sync")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, MeteorSyncPayload::present, + ByteBufCodecs.VAR_LONG, MeteorSyncPayload::pos, + ByteBufCodecs.VAR_INT, MeteorSyncPayload::state, + MeteorSyncPayload::new); + + public static final MeteorSyncPayload ABSENT = new MeteorSyncPayload(false, 0L, 0); + + @Override + public Type type() { + return TYPE; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java index 6c073f9..f3f1581 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java @@ -76,6 +76,10 @@ public static void sendToServer(CustomPacketPayload payload) { /** Called from common init so the payload lists are populated before each loader registers them. */ public static void init() { - // Subsystems register their payloads here as they are ported. + // Meteor Tracker: server → tracker-holders nearest-site snapshot. The handler runs only on the + // physical client; ClientMeteorTracker is a pure data holder (no client-only imports), so the + // method reference is safe to register from common code. + clientbound(MeteorSyncPayload.TYPE, MeteorSyncPayload.STREAM_CODEC, + za.co.neroland.nerospace.client.ClientMeteorTracker::accept); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index cfbd7af..a66a841 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -148,6 +148,9 @@ public final class ModItems { public static final RegistryEntry METEOR_CALLER = ITEMS.register("meteor_caller", key -> new MeteorCallerItem(new Item.Properties().stacksTo(1).setId(key))); + /** Meteor Tracker: while held, shows the nearest tracked meteor's heading/distance/state (server-synced). */ + public static final RegistryEntry METEOR_TRACKER = item("meteor_tracker", p -> p.stacksTo(1)); + // --- Spawn eggs (lazy entity-type supplier; ruin warden is summon-only) ---- public static final RegistryEntry XERTZ_STALKER_SPAWN_EGG = spawnEgg("xertz_stalker_spawn_egg", ModEntities.XERTZ_STALKER); public static final RegistryEntry QUARTZ_CRAWLER_SPAWN_EGG = spawnEgg("quartz_crawler_spawn_egg", ModEntities.QUARTZ_CRAWLER); @@ -260,7 +263,7 @@ public static Map, List> creativeTabItems List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get(), ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get(), GREENXERTZ_NAVIGATOR.get(), STATION_COMPASS.get(), GREENXERTZ_COMPASS.get(), - CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get()), + CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get(), METEOR_TRACKER.get()), CreativeModeTabs.SPAWN_EGGS, List.of(XERTZ_STALKER_SPAWN_EGG.get(), QUARTZ_CRAWLER_SPAWN_EGG.get(), GREENLING_SPAWN_EGG.get(), ALIEN_VILLAGER_SPAWN_EGG.get(), CINDER_STALKER_SPAWN_EGG.get(), diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/meteor_tracker.json b/multiloader/common/src/main/resources/assets/nerospace/items/meteor_tracker.json new file mode 100644 index 0000000..b5f4945 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/meteor_tracker.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/meteor_tracker" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index e12d01d..86afec1 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -111,6 +111,11 @@ "item.nerospace.meteor_caller": "Meteor Caller", "item.nerospace.meteor_caller.called": "A meteor streaks down from the sky…", "item.nerospace.meteor_caller.creative_only": "The Meteor Caller only works in creative mode", + "item.nerospace.meteor_tracker": "Meteor Tracker", + "item.nerospace.meteor_tracker.incoming": "Incoming", + "item.nerospace.meteor_tracker.landed": "Landed", + "item.nerospace.meteor_tracker.none": "Meteor Tracker: no meteors detected", + "item.nerospace.meteor_tracker.readout": "☄ Meteor %s — %s, %sm", "item.nerospace.nerosium_dust": "Nerosium Dust", "item.nerospace.nerosium_ingot": "Nerosium Ingot", "item.nerospace.nerosium_pickaxe": "Nerosium Pickaxe", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_tracker.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_tracker.json new file mode 100644 index 0000000..cf6d7d3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/meteor_tracker.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/meteor_tracker" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png new file mode 100644 index 0000000000000000000000000000000000000000..59371e1ac60d9fbf99c09fdbbf3ac739b87ee48c GIT binary patch literal 208 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`(>+}rLn`JZCoB*!ND`hBc;RQi z@1K*++7hn}SrTSNtLZokuGj21l*!Tcj8$6iZ~TUbm+Cn!MMZdok3Y5Akyg?xDqoi% z(UQX void register(EntityType type, EntityRend EntityRendererRegistry.register(type, provider); } }); + + // Meteor Tracker readout (action bar) — counterpart to NeoForge's ClientTickEvent.Post. + ClientTickEvents.END_CLIENT_TICK.register(mc -> MeteorTrackerHud.tick()); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index aa5a387..39af98f 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -7,13 +7,16 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.EntityRenderersEvent; import net.neoforged.neoforge.client.event.RegisterFluidModelsEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.client.fluid.FluidTintSources; +import net.neoforged.neoforge.common.NeoForge; import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.client.ClientEntityRenderers; +import za.co.neroland.nerospace.client.MeteorTrackerHud; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; import za.co.neroland.nerospace.client.FuelRefineryScreen; @@ -34,6 +37,8 @@ public static void init(IEventBus modEventBus) { modEventBus.addListener(NeoForgeClientSetup::onRegisterScreens); modEventBus.addListener(NeoForgeClientSetup::onRegisterFluidModels); modEventBus.addListener(NeoForgeClientSetup::onRegisterEntityRenderers); + // Meteor Tracker readout (action bar) — game-bus client tick (counterpart to Fabric's END_CLIENT_TICK). + NeoForge.EVENT_BUS.addListener((ClientTickEvent.Post event) -> MeteorTrackerHud.tick()); } private static void onRegisterEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { From 503c703166626fbe45985c448e75096348bed8f1 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 10:53:48 +0200 Subject: [PATCH 47/82] Add oxygen diffusion field and integrate sources Implement a server-side oxygen diffusion system and wire it into generators and tick drivers. Adds OxygenField (block sealing rules), OxygenFieldManager (fastutil sparse concentration field, saved source set, flood-fill diffusion/evaporation, snapshot API), and OxygenFieldEvents (per-server tick driver). Updates OxygenGenerator to emit/drain from its tank as a field source and remove itself on removal; OxygenManager now queries the field for breathability and falls back to launch-pad radius. Registers the field tick in both Fabric and NeoForge entry points and updates docs to reflect the port and deferred client-visual sync work. --- docs/MULTILOADER_PORT_CHECKLIST.md | 24 +- .../machine/OxygenGeneratorBlockEntity.java | 39 ++- .../neroland/nerospace/world/OxygenField.java | 59 ++++ .../nerospace/world/OxygenFieldEvents.java | 47 +++ .../nerospace/world/OxygenFieldManager.java | 306 ++++++++++++++++++ .../nerospace/world/OxygenManager.java | 16 +- .../nerospace/fabric/NerospaceFabric.java | 2 + .../nerospace/neoforge/NerospaceNeoForge.java | 8 +- 8 files changed, 483 insertions(+), 18 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenField.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldManager.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 8a7f5d1..75b3824 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~165 classes ported, ~99 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~168 classes ported, ~96 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — oxygen diffusion field (server half) ported.** All 4 cells green. Added +> `world/{OxygenField, OxygenFieldManager (SavedData, fastutil flood-fill sim), OxygenFieldEvents}`; the +> Oxygen Generator now feeds the field from its tank and `OxygenManager.isBreathable` reads it, so **sealed +> rooms are genuinely breathable** (open space only gets a bubble). Field config inlined; the cosmetic client +> visual layer (sync payload + particle/haze/boundary overlay) is the deferred follow-up. fastutil resolves on +> common NeoForm. + > **2026-06-21 update — meteor Tracker HUD ported (networking seam proven end-to-end).** All 4 cells green. > Added `network/MeteorSyncPayload` (multiloader's FIRST payload), `client/{ClientMeteorTracker, > MeteorTrackerHud}`, `ModItems.METEOR_TRACKER`; registered clientbound in `ModNetwork.init()` (both loader @@ -132,9 +139,18 @@ checked by a headless build). air-supply-bar mirror, full-suit detection) on a new **data-attachment seam**: `IPlatformHelper.get/setOxygen` backed by NeoForge `AttachmentType` (`NeoForgeAttachments`) and Fabric `AttachmentRegistry` (`FabricAttachments`); ticked per-loader (NeoForge `PlayerTickEvent`, Fabric `ServerTickEvents.END_SERVER_TICK`). - Breathable = near a Launch Pad / Oxygen Generator. **Deferred**: the diffusion `OxygenFieldManager`/ - `OxygenField`/`OxygenFieldEvents` (sealed rooms + client overlay — needs the **networking seam**), - terraform breathability + criteria, hazard shields/feedback, and gas-tank airlock refill. Values inlined. + Breathable = the diffusion field **or** near a Launch Pad (safe-zone radius). +- [x] **Oxygen diffusion field — server half DONE (4 cells green).** `world/{OxygenField (tag-based + sealing classifier — `OXYGEN_SEALING`/`OXYGEN_LEAKS`, doors/trapdoors, full-cube fallback), + OxygenFieldManager (SavedData; sparse fastutil concentration field + source set; per-pass flood-fill + detects sealed-vs-leaky/open volumes → sealed rooms fill to MAX, open space pressurises only a bubble; + slow evaporation), OxygenFieldEvents (cross-loader `tick(MinecraftServer)`, throttled sim pass)}`. + Wired into both server-tick hooks alongside the meteor driver; `OxygenManager.isBreathable` now reads the + field; the **Oxygen Generator registers itself as a field source**, draining `EMIT_MB_PER_TICK` from its + tank while sourcing (and clears on `setRemoved`). Sealed bases are now genuinely breathable. ~9 field + config keys inlined. **Deferred (cosmetic): the client visual layer** — `OxygenFieldSyncPayload` + + `ClientOxygenField` + the particle/haze/boundary overlay (gated on a visual-quality config). +- [ ] **Deferred**: terraform breathability + criteria, hazard shields/feedback, gas-tank airlock refill. - [ ] Terraformer + Terraform Monitor + Hydration Module (blocks/BE/menus/screens), `TerraformManager`, `TerraformConversion`, `TerraformDrift`, `TerraformFauna`, `TerraformChunkLoader`, `TerraformResources`, `GreenxertzAtmosphere`, terraformed biomes. Risk: **high** (world mutation, chunk-loading, events). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java index fb85aeb..7372e06 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java @@ -1,6 +1,7 @@ package za.co.neroland.nerospace.machine; import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; @@ -13,12 +14,14 @@ import za.co.neroland.nerospace.gas.GasTank; import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.world.OxygenFieldManager; /** * Oxygen Generator — a grid-powered electrolyser: each tick it spends energy from its internal buffer * to synthesise {@link GasResource#OXYGEN} into its gas tank. Exposes the energy capability (insert * only, fed by the pipe network) and the gas capability (extract only, tapped by the pipe network / - * adjacent gas tanks). GUI-less for now; the world oxygen-field effect is a deferred atmosphere system. + * adjacent gas tanks). It also feeds the world {@link OxygenFieldManager}: while its tank holds oxygen + * this position is a field source (pressurising sealed rooms / a bubble) and the tank drains slowly. */ public class OxygenGeneratorBlockEntity extends BlockEntity { @@ -27,6 +30,8 @@ public class OxygenGeneratorBlockEntity extends BlockEntity { public static final int MAX_INSERT = 1_000; public static final int MB_PER_TICK = 4; public static final int FE_PER_MB = 20; + /** Oxygen drawn from the tank per tick to keep the breathable field alive (Config emit rate, inlined). */ + public static final int EMIT_MB_PER_TICK = 2; private final EnergyBuffer energy = new EnergyBuffer(ENERGY_CAPACITY, MAX_INSERT, 0, this::setChanged); private final GasTank gas = new GasTank(GAS_CAPACITY, this::setChanged); @@ -73,16 +78,36 @@ public void tick(Level level, BlockPos pos, BlockState state) { if (level.isClientSide()) { return; } + // Electrolyse energy into oxygen gas while powered and there's room in the tank. long room = this.gas.getCapacity() - this.gas.getAmount(); int produce = (int) Math.min(MB_PER_TICK, room); - if (produce <= 0) { - return; + if (produce > 0) { + int cost = produce * FE_PER_MB; + if (this.energy.getAmount() >= cost) { + this.energy.consume(cost); + this.gas.fill(GasResource.OXYGEN, produce, false); + } + } + // Feed the world oxygen field from the tank: while it holds oxygen this position is a field + // source (the diffusion in OxygenFieldManager decides the breathable volume) and the tank drains + // slowly; out of gas the source is dropped and the bubble collapses. + if (level instanceof ServerLevel serverLevel) { + OxygenFieldManager fieldManager = OxygenFieldManager.get(serverLevel); + if (this.gas.getGas() == GasResource.OXYGEN && this.gas.getAmount() >= EMIT_MB_PER_TICK) { + this.gas.drain(EMIT_MB_PER_TICK, false); + fieldManager.addSource(pos); + } else { + fieldManager.removeSource(pos); + } } - int cost = produce * FE_PER_MB; - if (this.energy.getAmount() >= cost) { - this.energy.consume(cost); - this.gas.fill(GasResource.OXYGEN, produce, false); + } + + @Override + public void setRemoved() { + if (this.level instanceof ServerLevel serverLevel) { + OxygenFieldManager.get(serverLevel).removeSource(this.worldPosition); } + super.setRemoved(); } @Override diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenField.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenField.java new file mode 100644 index 0000000..6d6c332 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenField.java @@ -0,0 +1,59 @@ +package za.co.neroland.nerospace.world; + +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.DoorBlock; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; + +import net.minecraft.core.BlockPos; + +import za.co.neroland.nerospace.registry.ModTags; + +/** + * Block sealing classification for the oxygen field (terraform design §1.4). + * + *

Data-driven via block tags so it stays moddable: {@code nerospace:oxygen_sealing} blocks all + * flow (opaque cubes, glass, station walls — players can build airtight rooms with windows), + * {@code nerospace:oxygen_leaks} members are non-full blocks that hold oxygen but bleed faster, and + * air flows freely. Doors and trapdoors flow only while {@code open}, so opening a door leaks the + * room. There are no hardcoded {@code Block} checks here beyond the generic door/trapdoor + full-cube + * fallbacks.

+ */ +public final class OxygenField { + + private OxygenField() { + } + + /** @return true if a cell can hold oxygen (air, a leaky block, or an open door); false if it seals. */ + public static boolean canHold(BlockGetter level, BlockPos pos, BlockState state) { + if (state.isAir()) { + return true; + } + if (state.is(ModTags.Blocks.OXYGEN_SEALING)) { + return false; + } + if (state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) { + return state.hasProperty(BlockStateProperties.OPEN) && state.getValue(BlockStateProperties.OPEN); + } + if (state.is(ModTags.Blocks.OXYGEN_LEAKS)) { + return true; + } + // Fallback: anything that isn't a full solid cube is treated as leaky (holds + bleeds); + // a full solid cube seals even without an explicit tag. + return !state.isCollisionShapeFullBlock(level, pos); + } + + /** @return true for non-full / leaky cells that bleed oxygen to the void faster (openings). */ + public static boolean isLeaky(BlockGetter level, BlockPos pos, BlockState state) { + if (state.isAir()) { + return false; + } + // Doors/trapdoors BEFORE the leaks tag: vanilla TRAPDOORS are in OXYGEN_LEAKS, but a + // closed one is a wall (canHold false) and must never read as leaky. + if (state.getBlock() instanceof DoorBlock || state.getBlock() instanceof TrapDoorBlock) { + return state.hasProperty(BlockStateProperties.OPEN) && state.getValue(BlockStateProperties.OPEN); + } + return state.is(ModTags.Blocks.OXYGEN_LEAKS); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java new file mode 100644 index 0000000..d5fbd68 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java @@ -0,0 +1,47 @@ +package za.co.neroland.nerospace.world; + +import java.util.Set; + +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModDimensions; + +/** + * Cross-loader server-side driver for the oxygen field (terraform design §1.3). Each loader calls + * {@link #tick(MinecraftServer)} once per server tick from its own hook (alongside the meteor and + * oxygen-survival drivers); this runs the throttled relaxation pass on each airless Nerospace + * dimension. Cheap when idle: the manager pauses simulation for sources with no nearby player and + * short-circuits when there are no sources and no live cells. + * + *

Cross-loader port note: the client field sync (range-limited snapshot → {@code ClientOxygenField} + * for the particle / haze / boundary visual layers) is the deferred follow-up; this batch is the + * server field + breathability only. The sim interval is inlined (config seam deferred).

+ */ +public final class OxygenFieldEvents { + + /** Dimensions whose atmosphere is driven by the oxygen field. */ + public static final Set> FIELD_DIMENSIONS = Set.of( + ModDimensions.GREENXERTZ_LEVEL, ModDimensions.CINDARA_LEVEL, ModDimensions.STATION_LEVEL, + ModDimensions.GLACIRA_LEVEL); + + /** Server ticks between field relaxation passes (inlined from Config.OXYGEN_SIM_INTERVAL_TICKS). */ + private static final int SIM_INTERVAL_TICKS = 5; + + private OxygenFieldEvents() { + } + + /** Runs one throttled field pass per eligible dimension. */ + public static void tick(MinecraftServer server) { + for (ServerLevel level : server.getAllLevels()) { + if (!FIELD_DIMENSIONS.contains(level.dimension())) { + continue; + } + if (level.getGameTime() % SIM_INTERVAL_TICKS == 0) { + OxygenFieldManager.get(level).simulate(level); + } + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldManager.java new file mode 100644 index 0000000..958ab5a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldManager.java @@ -0,0 +1,306 @@ +package za.co.neroland.nerospace.world; + +import java.util.ArrayList; +import java.util.List; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Per-{@link ServerLevel} oxygen field (terraform design §1). Holds a sparse per-block concentration + * field (world-packed pos → {@code 0..MAX}) and the set of active oxygen sources, and runs a throttled + * diffusion-with-decay relaxation that produces dissipation in open space, fill in sealed volumes, and + * leakage through openings from one rule. Breathability is then an O(1) hash lookup. + * + *

The live field is held in memory (always loaded with the level) and re-converges from its sources + * within a few seconds of load, so only the source set is persisted (via the {@link SavedDataType} + * codec). Cross-loader port notes: the oxygen-field config keys are inlined to the root's shipped + * defaults (config seam deferred); {@code SavedDataType} on NeoForm exposes only the 4-arg ctor + * (DataFixTypes is null for new mod data). {@link #snapshotAround} feeds the deferred client visual + * layer (sync payload + overlay).

+ */ +public final class OxygenFieldManager extends SavedData { + + public static final Identifier ID = Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "oxygen_field"); + + public static final SavedDataType TYPE = new SavedDataType<>( + ID, OxygenFieldManager::new, codec(), null); + + // --- Inlined from Config (root shipped defaults) until the config seam lands --- + private static final int MAX_CONCENTRATION = 15; + private static final int BREATHABLE_THRESHOLD = 6; + private static final int MAX_ACTIVE_CELLS_PER_SOURCE = 4096; + private static final int BUBBLE_RADIUS = 14; + private static final int LEAK_RANGE = 16; + private static final int SIM_INTERVAL_TICKS = 5; + private static final int EVAPORATE_SECONDS = 10; + private static final int SYNC_RADIUS = 32; + + /** Live concentration field: world-packed BlockPos → concentration byte (absent = 0 = vacuum). */ + private final Long2ByteOpenHashMap field = new Long2ByteOpenHashMap(); + /** Active oxygen-source cells (world-packed), forced to MAX each step. Persisted. */ + private final LongOpenHashSet sources = new LongOpenHashSet(); + + public OxygenFieldManager() { + this.field.defaultReturnValue((byte) 0); + } + + private static Codec codec() { + return RecordCodecBuilder.create(inst -> inst.group( + Codec.LONG.listOf().fieldOf("sources").forGetter(m -> new ArrayList<>(m.sources)) + ).apply(inst, OxygenFieldManager::fromSources)); + } + + private static OxygenFieldManager fromSources(List sources) { + OxygenFieldManager m = new OxygenFieldManager(); + for (long s : sources) { + m.sources.add(s); + m.field.put(s, (byte) MAX_CONCENTRATION); + } + return m; + } + + public static OxygenFieldManager get(ServerLevel level) { + return level.getDataStorage().computeIfAbsent(TYPE); + } + + // --- Source registry ---------------------------------------------------- + + public void addSource(BlockPos pos) { + long key = pos.asLong(); + if (this.sources.add(key)) { + setDirty(); // injection happens at the source's air neighbours during simulate() + } + } + + public void removeSource(BlockPos pos) { + if (this.sources.remove(pos.asLong())) { + setDirty(); + } + } + + public boolean isSource(BlockPos pos) { + return this.sources.contains(pos.asLong()); + } + + // --- Lookup (O(1)) ------------------------------------------------------ + + /** @return concentration {@code 0..MAX} at {@code pos}. */ + public int concentrationAt(BlockPos pos) { + return this.field.get(pos.asLong()) & 0xFF; + } + + public boolean isBreathable(BlockPos pos) { + return concentrationAt(pos) >= BREATHABLE_THRESHOLD; + } + + public LongSet sourceCells() { + return this.sources; + } + + public int activeCellCount() { + return this.field.size(); + } + + // --- Simulation --------------------------------------------------------- + + /** Sim-pass counter, used to pace the slow evaporation drain (not persisted). */ + private transient int simCounter; + + /** + * One simulation pass (terraform design §1, reworked for gas-like behaviour). Each pass: + * + *
    + *
  1. Flood from each active source through the connected air space (BFS), detecting + * whether that volume is sealed (BFS never reaches a sky-exposed cell and stays under + * the cap) or leaky/open (it does). A sealed volume is the breathable target at full + * strength everywhere (the room fills); a leaky/open volume only pressurises a small bubble + * around the generator and bleeds to 0 toward the opening — oxygen finds the leak and escapes. + *
  2. Ease the live field toward that target: rise to it immediately (fill), but drain + * toward it slowly so that losing supply (out of fuel / broken generator) or springing a leak + * makes the oxygen evaporate over {@code EVAPORATE_SECONDS} rather than vanishing.
  3. + *
+ * + *

Because the target is recomputed every pass from the current blocks, the field re-paths + * automatically when the surroundings change — seal a wall and the room fills; break one and it + * leaks out.

+ */ + public void simulate(ServerLevel level) { + if (this.sources.isEmpty() && this.field.isEmpty()) { + return; + } + // Run while a player is near a source, or while leftover oxygen still needs to evaporate. + boolean run = anyPlayerNearSource(level) || (!this.field.isEmpty() && !level.players().isEmpty()); + if (!run) { + return; // paused; persisted state resumes when a player returns + } + + this.simCounter++; + final int max = MAX_CONCENTRATION; + final int cap = MAX_ACTIVE_CELLS_PER_SOURCE; + final int bubbleR = Math.max(1, BUBBLE_RADIUS); + final int leakRange = LEAK_RANGE; + final int interval = SIM_INTERVAL_TICKS; + // Drain one concentration level every N passes so a full cell reaches 0 in ~EVAPORATE_SECONDS. + int passesToEmpty = Math.max(1, Math.round(EVAPORATE_SECONDS * 20.0F / interval)); + int drainEvery = Math.max(1, Math.round((float) passesToEmpty / max)); + boolean drainTick = this.simCounter % drainEvery == 0; + + // 1. Target field: flood-fill from every active source. + Long2ByteOpenHashMap target = new Long2ByteOpenHashMap(); + target.defaultReturnValue((byte) 0); + BlockPos.MutableBlockPos m = new BlockPos.MutableBlockPos(); + LongIterator si = this.sources.iterator(); + while (si.hasNext()) { + floodFromSource(level, BlockPos.of(si.nextLong()), max, cap, bubbleR, leakRange, target, m); + } + + // 2. Ease the live field toward the target: snap up to fill, drain slowly to evaporate/leak. + LongOpenHashSet cells = new LongOpenHashSet(this.field.keySet()); + cells.addAll(target.keySet()); + Long2ByteOpenHashMap next = new Long2ByteOpenHashMap(); + next.defaultReturnValue((byte) 0); + LongIterator ci = cells.iterator(); + while (ci.hasNext()) { + long c = ci.nextLong(); + int old = this.field.get(c) & 0xFF; + int tgt = target.get(c) & 0xFF; + int val; + if (tgt >= old) { + val = tgt; // fill: rise to target immediately + } else if (drainTick) { + val = Math.max(tgt, old - 1); // evaporate/leak: drain one level per drain tick + } else { + val = old; + } + if (val > 0) { + next.put(c, (byte) val); + } + } + this.field.clear(); + this.field.putAll(next); + } + + /** + * BFS the connected air space from a source block and write its breathable target into {@code out}. + * Sealed volumes (no sky-exposed cell, under the cap) fill to MAX everywhere; leaky/open volumes + * pressurise only a {@code bubbleR} falloff around the source and drop to 0 toward the opening. + */ + private void floodFromSource(ServerLevel level, BlockPos source, int max, int cap, int bubbleR, + int leakRange, Long2ByteOpenHashMap out, BlockPos.MutableBlockPos m) { + Long2IntOpenHashMap dist = new Long2IntOpenHashMap(); + dist.defaultReturnValue(-1); + LongArrayFIFOQueue queue = new LongArrayFIFOQueue(); + + // Seed from the source's holdable air neighbours (the generator block itself is solid). + for (Direction dir : Direction.values()) { + m.setWithOffset(source, dir); + if (level.hasChunk(m.getX() >> 4, m.getZ() >> 4) && OxygenField.canHold(level, m, level.getBlockState(m))) { + long k = m.asLong(); + if (dist.get(k) < 0) { + dist.put(k, 0); + queue.enqueue(k); + } + } + } + if (queue.isEmpty()) { + return; + } + + boolean leaked = false; + boolean capped = false; + BlockPos.MutableBlockPos mm = new BlockPos.MutableBlockPos(); + while (!queue.isEmpty()) { + if (dist.size() > cap) { + capped = true; // too big to confirm sealed → treat as open + break; + } + long c = queue.dequeueLong(); + int d = dist.get(c); + BlockPos cp = BlockPos.of(c); + if (level.canSeeSky(cp)) { + leaked = true; // an opening to the sky/vacuum — the volume is not sealed + } + if (d >= leakRange) { + continue; // a generator only searches/pressurises out to LEAK_RANGE blocks + } + for (Direction dir : Direction.values()) { + mm.setWithOffset(cp, dir); + if (!level.hasChunk(mm.getX() >> 4, mm.getZ() >> 4)) { + continue; + } + long nk = mm.asLong(); + if (dist.get(nk) >= 0) { + continue; + } + if (OxygenField.canHold(level, mm, level.getBlockState(mm))) { + dist.put(nk, d + 1); + queue.enqueue(nk); + } + } + } + + boolean sealed = !leaked && !capped; + for (Long2IntMap.Entry e : dist.long2IntEntrySet()) { + int d = e.getIntValue(); + int val = sealed ? max : Math.max(0, Math.round(max * (1.0F - (float) d / bubbleR))); + if (val <= 0) { + continue; + } + long k = e.getLongKey(); + if ((out.get(k) & 0xFF) < val) { + out.put(k, (byte) val); + } + } + } + + private boolean anyPlayerNearSource(ServerLevel level) { + List players = level.players(); + if (players.isEmpty()) { + return false; + } + double rSq = (double) SYNC_RADIUS * SYNC_RADIUS; + LongIterator si = this.sources.iterator(); + while (si.hasNext()) { + BlockPos sp = BlockPos.of(si.nextLong()); + for (ServerPlayer p : players) { + if (p.distanceToSqr(sp.getX() + 0.5, sp.getY() + 0.5, sp.getZ() + 0.5) <= rSq) { + return true; + } + } + } + return false; + } + + /** Snapshot of cells within {@code radius} of {@code center} for the (deferred) client sync packet. */ + public Long2ByteMap snapshotAround(BlockPos center, int radius) { + Long2ByteOpenHashMap out = new Long2ByteOpenHashMap(); + long rSq = (long) radius * radius; + for (Long2ByteMap.Entry e : this.field.long2ByteEntrySet()) { + BlockPos p = BlockPos.of(e.getLongKey()); + if (center.distSqr(p) <= rSq) { + out.put(e.getLongKey(), e.getByteValue()); + } + } + return out; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java index 9218722..a1b1be2 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java @@ -12,7 +12,6 @@ import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; import za.co.neroland.nerospace.platform.Services; import za.co.neroland.nerospace.registry.ModBlocks; @@ -105,13 +104,20 @@ private static void mirrorToAirSupply(Player player, int oxygen, int max) { player.setAirSupply(Math.min(airMax, Math.max(0, air))); } - /** A breathable zone: within {@link #SAFE_RADIUS} of a Rocket Launch Pad or an Oxygen Generator. */ - private static boolean isBreathable(Level level, BlockPos center) { + /** + * A breathable zone: the diffusion {@link OxygenFieldManager} reads breathable at {@code center} + * (sealed rooms fill completely; an Oxygen Generator pressurises a bubble / its sealed room), or the + * player is within {@link #SAFE_RADIUS} of a Rocket Launch Pad — a permanent pressurised safe zone at + * the landing site (the pad is not a field source, so it stays a simple radius check). + */ + private static boolean isBreathable(ServerLevel level, BlockPos center) { + if (OxygenFieldManager.get(level).isBreathable(center)) { + return true; + } for (BlockPos pos : BlockPos.betweenClosed( center.offset(-SAFE_RADIUS, -SAFE_RADIUS, -SAFE_RADIUS), center.offset(SAFE_RADIUS, SAFE_RADIUS, SAFE_RADIUS))) { - BlockState state = level.getBlockState(pos); - if (state.is(ModBlocks.ROCKET_LAUNCH_PAD.get()) || state.is(ModBlocks.OXYGEN_GENERATOR.get())) { + if (level.getBlockState(pos).is(ModBlocks.ROCKET_LAUNCH_PAD.get())) { return true; } } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 45a91db..745d9c4 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -26,6 +26,7 @@ import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.world.OxygenFieldEvents; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.world.OxygenManager; @@ -84,6 +85,7 @@ public void register(EntityType type, SpawnPlacementType plac ServerTickEvents.END_SERVER_TICK.register(server -> { server.getPlayerList().getPlayers().forEach(OxygenManager::tick); MeteorEvents.tick(server); + OxygenFieldEvents.tick(server); }); // Item-storage capability (Fabric Transfer API) — counterpart to NeoForge diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 6e840e8..da243de 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -20,6 +20,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; +import za.co.neroland.nerospace.world.OxygenFieldEvents; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; @@ -51,8 +52,11 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { OxygenManager.tick(serverPlayer); } }); - // Natural meteor showers: tick the per-level scheduler once per server tick. - NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> MeteorEvents.tick(event.getServer())); + // Natural meteor showers + oxygen-field diffusion: tick the per-level drivers once per server tick. + NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> { + MeteorEvents.tick(event.getServer()); + OxygenFieldEvents.tick(event.getServer()); + }); // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated // Nerospace tab registered via the vanilla CREATIVE_MODE_TAB registry), so no NeoForge-specific // BuildCreativeModeTabContentsEvent injection is needed. From 5391b08f88fc69ceeeeb5962eb6a9f922fcc5bbe Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:07:34 +0200 Subject: [PATCH 48/82] Add per-chunk terraforming data and biomes Introduce per-chunk terraform state (TERRAFORMED, TERRAFORM_STAGE): add attachment registrations for Fabric and NeoForge, new IPlatformHelper APIs (is/setTerraformed, get/setTerraformStage), and platform implementations that read/write chunk attachments. Add ModBiomes ResourceKey constants and ship four terraformed biome datapack JSONs plus two terraform block-tag JSONs. Wire terraformed chunks into OxygenManager.isBreathable so converted chunks are treated as breathable. Update the multiloader port checklist docs to record the terraforming slice progress. --- docs/MULTILOADER_PORT_CHECKLIST.md | 34 +++++++++++++-- .../nerospace/platform/IPlatformHelper.java | 17 ++++++++ .../neroland/nerospace/world/ModBiomes.java | 41 +++++++++++++++++++ .../nerospace/world/OxygenManager.java | 4 ++ .../tags/block/terraform_to_dirt.json | 21 ++++++++++ .../tags/block/terraform_to_grass.json | 27 ++++++++++++ .../nerospace/worldgen/biome/terraformed.json | 24 +++++++++++ .../worldgen/biome/terraformed_meadow.json | 37 +++++++++++++++++ .../worldgen/biome/terraformed_savanna.json | 31 ++++++++++++++ .../worldgen/biome/terraformed_tundra.json | 31 ++++++++++++++ .../nerospace/fabric/FabricAttachments.java | 12 ++++++ .../platform/FabricPlatformHelper.java | 21 ++++++++++ .../neoforge/NeoForgeAttachments.java | 14 +++++++ .../platform/NeoForgePlatformHelper.java | 21 ++++++++++ 14 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/ModBiomes.java create mode 100644 multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_dirt.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_grass.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_meadow.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_savanna.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_tundra.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 75b3824..36e6a6e 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,20 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~168 classes ported, ~96 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~169 classes ported, ~95 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 2 (biomes + tags data).** All 4 cells green. Added `world/ModBiomes` +> (4 terraformed biome ResourceKey constants) + copied the 4 terraformed biome JSON + 2 terraform tag JSON. +> Data foundation for the conversion engine (slice 3). + +> **2026-06-21 update — terraforming started (slice 1: chunk-attachment seam).** All 4 cells green. +> Extended the data-attachment seam for per-chunk terraform data (`TERRAFORMED` + `TERRAFORM_STAGE`) — +> NeoForge `chunk.getData/setData`, Fabric `chunk.getAttachedOrCreate/setAttached` (same registries as the +> player oxygen attachment, `LevelChunk` target); wired terraformed-ground into `OxygenManager.isBreathable`. +> The signature terraform subsystem is sliced into 6 (see Atmosphere section); this is the critical-path +> foundation. No new class count yet (seam extension); slices 2–6 add the ~18 terraform classes. + > **2026-06-21 update — oxygen diffusion field (server half) ported.** All 4 cells green. Added > `world/{OxygenField, OxygenFieldManager (SavedData, fastutil flood-fill sim), OxygenFieldEvents}`; the > Oxygen Generator now feeds the field from its tank and `OxygenManager.isBreathable` reads it, so **sealed @@ -151,9 +162,24 @@ checked by a headless build). config keys inlined. **Deferred (cosmetic): the client visual layer** — `OxygenFieldSyncPayload` + `ClientOxygenField` + the particle/haze/boundary overlay (gated on a visual-quality config). - [ ] **Deferred**: terraform breathability + criteria, hazard shields/feedback, gas-tank airlock refill. -- [ ] Terraformer + Terraform Monitor + Hydration Module (blocks/BE/menus/screens), `TerraformManager`, - `TerraformConversion`, `TerraformDrift`, `TerraformFauna`, `TerraformChunkLoader`, `TerraformResources`, - `GreenxertzAtmosphere`, terraformed biomes. Risk: **high** (world mutation, chunk-loading, events). +- **Terraforming** (signature endgame) — sliced; **slice 1 DONE (4 cells green)**, rest sequenced: + - [x] **Slice 1 — per-chunk data-attachment seam.** `IPlatformHelper.is/setTerraformed` + + `get/setTerraformStage(LevelChunk)` backed by NeoForge `AttachmentType` (chunk `getData`/`setData`) and + Fabric `AttachmentRegistry` (chunk `getAttachedOrCreate`/`setAttached`) — same registries as the player + oxygen attachment, just a `LevelChunk` target (no new API surface). Wired into `OxygenManager.isBreathable` + (terraformed chunk ⇒ breathable). Critical-path foundation for everything below. + - [x] **Slice 2 — biome + tag data.** `world/ModBiomes` (4 terraformed `ResourceKey` constants — + the multiloader ships biomes as committed datapack JSON, so no datagen bootstrap needed) + copied the 4 + terraformed biome JSON (`terraformed`/`_meadow`/`_savanna`/`_tundra`, feature-free / runtime-written) + + copied the 2 terraform block-tag JSON (`TERRAFORM_TO_GRASS`/`_DIRT` — TagKey constants already in `ModTags`). + All 4 cells green; JSON python-validated. (Inert until slice 3 consumes them.) + - [ ] **Slice 3 — conversion engine.** `TerraformConversion` (rewrite stage bookkeeping onto the seam, not + `chunk.getData(ModAttachments…)`), `TerraformResources`, `TerraformFauna`. Inline terraform config keys. + - [ ] **Slice 4 — Terraformer machine.** `TerraformerBlock`(+BE 584ln)+menu+screen + `TerraformManager` + (SavedData; 4-arg `SavedDataType`) + chunk-load catch-up hook (per-loader) + biome-sync packet. + - [ ] **Slice 5 — Hydration Module** (block/BE/menu/screen) + stage-2 wiring. + - [ ] **Slice 6 — Terraform Monitor** (block/BE/menu/screen) + `TerraformDrift` + `TerraformChunkLoader` + + `GreenxertzAtmosphere`. Risk: **high** (world mutation, chunk-loading, events). ### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) — **DONE (4 cells green)** - [x] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + `ModFeatures` diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java index e6b8e2a..892b9da 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java @@ -35,4 +35,21 @@ public interface IPlatformHelper { /** Stores the player's oxygen. */ void setOxygen(net.minecraft.world.entity.player.Player player, int value); + + // --- Per-chunk terraform data (data-attachment seam) -------------------- + // The Terraformer permanently flags converted chunks: TERRAFORMED (breathable at/above the surface) + // and TERRAFORM_STAGE (1 Rooted / 2 Hydrated / 3 Living). Same attachment APIs as the player oxygen, + // applied to a LevelChunk target. Persist with the chunk; not synced (breathability is server-side). + + /** Whether the chunk has been terraformed (breathable ground). Defaults false. */ + boolean isTerraformed(net.minecraft.world.level.chunk.LevelChunk chunk); + + /** Flags the chunk as terraformed. */ + void setTerraformed(net.minecraft.world.level.chunk.LevelChunk chunk, boolean value); + + /** The highest terraform stage any column in the chunk has completed (0 none .. 3 Living). */ + int getTerraformStage(net.minecraft.world.level.chunk.LevelChunk chunk); + + /** Records the chunk's terraform stage. */ + void setTerraformStage(net.minecraft.world.level.chunk.LevelChunk chunk, int value); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/ModBiomes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/ModBiomes.java new file mode 100644 index 0000000..ab0705b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/ModBiomes.java @@ -0,0 +1,41 @@ +package za.co.neroland.nerospace.world; + +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Cross-loader {@link ResourceKey} handles for the terraform biomes. Unlike the root (which registers + * these via a datagen {@code BootstrapContext}), the multiloader ships the biomes as committed datapack + * JSON (under {@code data/nerospace/worldgen/biome/}); this class only exposes the keys so the + * {@code TerraformConversion} engine can look the biomes up and write them onto converted columns. + * + *

{@link #TERRAFORMED} is the vibrant intermediate look (stages 1–2); the three mature biomes are + * the per-planet stage-3 payoff (DEEPER_TERRAFORM_DESIGN.md §4). The planet surface biomes + * (greenxertz/cindara/glacira) are pure datapack JSON with no Java handle needed.

+ */ +public final class ModBiomes { + + /** Intermediate terraformed look (stages 1–2): neon emerald/turquoise. */ + public static final ResourceKey TERRAFORMED = key("terraformed"); + + /** Greenxertz mature stage-3 biome: natural lush meadow. */ + public static final ResourceKey TERRAFORMED_MEADOW = key("terraformed_meadow"); + + /** Cindara mature stage-3 biome: warm gold-green savanna. */ + public static final ResourceKey TERRAFORMED_SAVANNA = key("terraformed_savanna"); + + /** Glacira mature stage-3 biome: cold sage-green tundra. */ + public static final ResourceKey TERRAFORMED_TUNDRA = key("terraformed_tundra"); + + private ModBiomes() { + } + + private static ResourceKey key(String name) { + return ResourceKey.create(Registries.BIOME, + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, name)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java index a1b1be2..03dec20 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java @@ -111,6 +111,10 @@ private static void mirrorToAirSupply(Player player, int oxygen, int max) { * the landing site (the pad is not a field source, so it stays a simple radius check). */ private static boolean isBreathable(ServerLevel level, BlockPos center) { + // Terraformed ground is permanently breathable (the Terraformer flags the chunk). + if (Services.PLATFORM.isTerraformed(level.getChunkAt(center))) { + return true; + } if (OxygenFieldManager.get(level).isBreathable(center)) { return true; } diff --git a/multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_dirt.json b/multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_dirt.json new file mode 100644 index 0000000..d60ea01 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_dirt.json @@ -0,0 +1,21 @@ +{ + "values": [ + "minecraft:stone", + "minecraft:deepslate", + "minecraft:dirt", + "minecraft:coarse_dirt", + "minecraft:gravel", + "minecraft:andesite", + "minecraft:diorite", + "minecraft:granite", + "minecraft:tuff", + "minecraft:calcite", + "minecraft:netherrack", + "minecraft:blackstone", + "minecraft:basalt", + "minecraft:end_stone", + "minecraft:sandstone", + "minecraft:sand", + "minecraft:red_sand" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_grass.json b/multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_grass.json new file mode 100644 index 0000000..9d2bdf9 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/tags/block/terraform_to_grass.json @@ -0,0 +1,27 @@ +{ + "values": [ + "minecraft:stone", + "minecraft:dirt", + "minecraft:coarse_dirt", + "minecraft:podzol", + "minecraft:gravel", + "minecraft:sand", + "minecraft:red_sand", + "minecraft:sandstone", + "minecraft:andesite", + "minecraft:diorite", + "minecraft:granite", + "minecraft:tuff", + "minecraft:calcite", + "minecraft:netherrack", + "minecraft:blackstone", + "minecraft:basalt", + "minecraft:end_stone", + "minecraft:terracotta", + "minecraft:clay", + "minecraft:mud", + "minecraft:snow_block", + "minecraft:mycelium", + "#minecraft:dirt" + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed.json new file mode 100644 index 0000000..34a90fe --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed.json @@ -0,0 +1,24 @@ +{ + "carvers": [], + "downfall": 0.4, + "effects": { + "dry_foliage_color": "#19e8c0", + "foliage_color": "#19e8c0", + "grass_color": "#2bffb0", + "water_color": "#1ff0e0" + }, + "features": [], + "has_precipitation": false, + "spawn_costs": {}, + "spawners": { + "ambient": [], + "axolotls": [], + "creature": [], + "misc": [], + "monster": [], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": 0.8 +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_meadow.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_meadow.json new file mode 100644 index 0000000..d96d154 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_meadow.json @@ -0,0 +1,37 @@ +{ + "carvers": [], + "downfall": 0.8, + "effects": { + "dry_foliage_color": "#3fb04a", + "foliage_color": "#3fb04a", + "grass_color": "#59c93c", + "water_color": "#3f76e4" + }, + "features": [], + "has_precipitation": true, + "spawn_costs": {}, + "spawners": { + "ambient": [], + "axolotls": [], + "creature": [ + { + "type": "nerospace:meadow_loper", + "maxCount": 4, + "minCount": 2, + "weight": 10 + }, + { + "type": "nerospace:alien_villager", + "maxCount": 2, + "minCount": 1, + "weight": 5 + } + ], + "misc": [], + "monster": [], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": 0.8 +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_savanna.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_savanna.json new file mode 100644 index 0000000..b0454e3 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_savanna.json @@ -0,0 +1,31 @@ +{ + "carvers": [], + "downfall": 0.3, + "effects": { + "dry_foliage_color": "#aea42a", + "foliage_color": "#aea42a", + "grass_color": "#bfb755", + "water_color": "#4c8fbf" + }, + "features": [], + "has_precipitation": true, + "spawn_costs": {}, + "spawners": { + "ambient": [], + "axolotls": [], + "creature": [ + { + "type": "nerospace:ember_strutter", + "maxCount": 4, + "minCount": 2, + "weight": 10 + } + ], + "misc": [], + "monster": [], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": 1.2 +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_tundra.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_tundra.json new file mode 100644 index 0000000..e5ab88c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/terraformed_tundra.json @@ -0,0 +1,31 @@ +{ + "carvers": [], + "downfall": 0.5, + "effects": { + "dry_foliage_color": "#60a17b", + "foliage_color": "#60a17b", + "grass_color": "#80b497", + "water_color": "#3d57d6" + }, + "features": [], + "has_precipitation": true, + "spawn_costs": {}, + "spawners": { + "ambient": [], + "axolotls": [], + "creature": [ + { + "type": "nerospace:woolly_drift", + "maxCount": 4, + "minCount": 2, + "weight": 10 + } + ], + "misc": [], + "monster": [], + "underground_water_creature": [], + "water_ambient": [], + "water_creature": [] + }, + "temperature": -0.3 +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java index cf8bbad..6649720 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java @@ -22,6 +22,18 @@ public final class FabricAttachments { .copyOnDeath() .buildAndRegister(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "oxygen")); + /** Per-chunk: the converted chunk is permanently breathable at/above the surface. */ + public static final AttachmentType TERRAFORMED = AttachmentRegistry.builder() + .initializer(() -> Boolean.FALSE) + .persistent(Codec.BOOL) + .buildAndRegister(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "terraformed")); + + /** Per-chunk: highest terraform stage completed (0 none / 1 Rooted / 2 Hydrated / 3 Living). */ + public static final AttachmentType TERRAFORM_STAGE = AttachmentRegistry.builder() + .initializer(() -> 0) + .persistent(Codec.INT) + .buildAndRegister(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "terraform_stage")); + private FabricAttachments() { } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java index a73208d..f9af215 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java @@ -3,6 +3,7 @@ import net.fabricmc.api.EnvType; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.chunk.LevelChunk; import za.co.neroland.nerospace.fabric.FabricAttachments; @@ -41,4 +42,24 @@ public int getOxygen(Player player) { public void setOxygen(Player player, int value) { player.setAttached(FabricAttachments.OXYGEN, value); } + + @Override + public boolean isTerraformed(LevelChunk chunk) { + return chunk.getAttachedOrCreate(FabricAttachments.TERRAFORMED); + } + + @Override + public void setTerraformed(LevelChunk chunk, boolean value) { + chunk.setAttached(FabricAttachments.TERRAFORMED, value); + } + + @Override + public int getTerraformStage(LevelChunk chunk) { + return chunk.getAttachedOrCreate(FabricAttachments.TERRAFORM_STAGE); + } + + @Override + public void setTerraformStage(LevelChunk chunk, int value) { + chunk.setAttached(FabricAttachments.TERRAFORM_STAGE, value); + } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java index c1804c2..024ce14 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java @@ -29,6 +29,20 @@ public final class NeoForgeAttachments { .copyOnDeath() .build()); + /** Per-chunk: the converted chunk is permanently breathable at/above the surface. */ + public static final Supplier> TERRAFORMED = ATTACHMENT_TYPES.register( + "terraformed", + () -> AttachmentType.builder(() -> Boolean.FALSE) + .serialize(Codec.BOOL.fieldOf("terraformed")) + .build()); + + /** Per-chunk: highest terraform stage completed (0 none / 1 Rooted / 2 Hydrated / 3 Living). */ + public static final Supplier> TERRAFORM_STAGE = ATTACHMENT_TYPES.register( + "terraform_stage", + () -> AttachmentType.builder(() -> 0) + .serialize(Codec.INT.fieldOf("terraform_stage")) + .build()); + private NeoForgeAttachments() { } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java index 737d976..3cb2e33 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -4,6 +4,7 @@ import net.neoforged.fml.loading.FMLEnvironment; import net.neoforged.api.distmarker.Dist; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.chunk.LevelChunk; import za.co.neroland.nerospace.neoforge.NeoForgeAttachments; @@ -44,4 +45,24 @@ public int getOxygen(Player player) { public void setOxygen(Player player, int value) { player.setData(NeoForgeAttachments.OXYGEN.get(), value); } + + @Override + public boolean isTerraformed(LevelChunk chunk) { + return chunk.getData(NeoForgeAttachments.TERRAFORMED.get()); + } + + @Override + public void setTerraformed(LevelChunk chunk, boolean value) { + chunk.setData(NeoForgeAttachments.TERRAFORMED.get(), value); + } + + @Override + public int getTerraformStage(LevelChunk chunk) { + return chunk.getData(NeoForgeAttachments.TERRAFORM_STAGE.get()); + } + + @Override + public void setTerraformStage(LevelChunk chunk, int value) { + chunk.setData(NeoForgeAttachments.TERRAFORM_STAGE.get(), value); + } } From 52bc76f750e36431482c8cd7d2391777044442f5 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:14:30 +0200 Subject: [PATCH 49/82] Add terraforming conversion engine and resources Update checklist to mark terraforming slice 3 as added and add three new common classes for the multiloader port: TerraformConversion (staged column conversion: convert/hydrate/vivify, idempotent, stage bookkeeping moved to Services.PLATFORM), TerraformResources (inlined Tier-3 ore id list, lazy Block resolution), and TerraformFauna (starter-herd seeding with population cap). Inlined config/tuning defaults and wired worldgen/biome write APIs for common use. Notes that slice 4 (Terraformer machine, TerraformManager SavedData and chunk-load catch-up hook) remains to be implemented. --- docs/MULTILOADER_PORT_CHECKLIST.md | 18 +- .../machine/TerraformConversion.java | 337 ++++++++++++++++++ .../nerospace/machine/TerraformResources.java | 56 +++ .../nerospace/world/TerraformFauna.java | 72 ++++ 4 files changed, 480 insertions(+), 3 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformConversion.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformResources.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformFauna.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 36e6a6e..c5ab91b 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~169 classes ported, ~95 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~172 classes ported, ~92 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 3 (conversion engine).** All 4 cells green. Added +> `machine/{TerraformConversion (335ln staged converter), TerraformResources}` + `world/TerraformFauna`; +> stage bookkeeping rewired from `chunk.getData(ModAttachments…)` onto the slice-1 `Services.PLATFORM` chunk +> seam. Worldgen APIs (TreeFeatures/ConfiguredFeature.place/PalettedContainer/EntityType.spawn) resolve on +> common. Next = slice 4: Terraformer machine (block/BE 584ln/menu/screen) + TerraformManager (SavedData) + +> chunk-load catch-up hook — the slice that makes terraforming actually run. + > **2026-06-21 update — terraforming slice 2 (biomes + tags data).** All 4 cells green. Added `world/ModBiomes` > (4 terraformed biome ResourceKey constants) + copied the 4 terraformed biome JSON + 2 terraform tag JSON. > Data foundation for the conversion engine (slice 3). @@ -173,8 +180,13 @@ checked by a headless build). terraformed biome JSON (`terraformed`/`_meadow`/`_savanna`/`_tundra`, feature-free / runtime-written) + copied the 2 terraform block-tag JSON (`TERRAFORM_TO_GRASS`/`_DIRT` — TagKey constants already in `ModTags`). All 4 cells green; JSON python-validated. (Inert until slice 3 consumes them.) - - [ ] **Slice 3 — conversion engine.** `TerraformConversion` (rewrite stage bookkeeping onto the seam, not - `chunk.getData(ModAttachments…)`), `TerraformResources`, `TerraformFauna`. Inline terraform config keys. + - [x] **Slice 3 — conversion engine.** `machine/TerraformConversion` (staged column conversion: stage 1 + Rooted = terrain→grass/dirt via `TERRAFORM_TO_GRASS/DIRT` tags + breathable flag + `TERRAFORMED` biome + + plants/ore; stage 2 Hydrated = basin water fill; stage 3 Living = mature biome + trees + herds — stage + bookkeeping rewired onto the `Services.PLATFORM` chunk seam), `machine/TerraformResources` (inlined ore + list), `world/TerraformFauna` (inlined herd config). Worldgen APIs (`TreeFeatures`, `ConfiguredFeature.place`, + `PalettedContainer` biome write, `EntityType.spawn`) all resolve on common. ~7 config/tuning keys inlined. + All 4 cells green. (Inert until slice 4's machine + manager drive it.) - [ ] **Slice 4 — Terraformer machine.** `TerraformerBlock`(+BE 584ln)+menu+screen + `TerraformManager` (SavedData; 4-arg `SavedDataType`) + chunk-load catch-up hook (per-loader) + biome-sync packet. - [ ] **Slice 5 — Hydration Module** (block/BE/menu/screen) + stage-2 wiring. diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformConversion.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformConversion.java new file mode 100644 index 0000000..41ffdf1 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformConversion.java @@ -0,0 +1,337 @@ +package za.co.neroland.nerospace.machine; + +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.features.TreeFeatures; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.levelgen.feature.ConfiguredFeature; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +import net.minecraft.world.level.chunk.PalettedContainerRO; +import net.minecraft.world.level.levelgen.Heightmap; + +import za.co.neroland.nerospace.platform.Services; +import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.registry.ModTags; +import za.co.neroland.nerospace.world.ModBiomes; +import za.co.neroland.nerospace.world.TerraformFauna; + +/** + * Shared, idempotent surface-column conversion for the Terraformer (terraform design §2.2; staged per + * DEEPER_TERRAFORM_DESIGN.md). Extracted so both the live frontier ({@link TerraformerBlockEntity}) + * and the chunk-load catch-up rescan ({@code TerraformManager}) convert columns identically. The + * caller guarantees the column's chunk is loaded. + * + *

Stages: {@link #convertColumn} = stage 1 (Rooted), {@link #hydrateColumn} = stage 2 (Hydrated), + * {@link #vivifyColumn} = stage 3 (Living). Each is a per-stage no-op when re-run, so persistence stays + * radii + cursors.

+ * + *

Cross-loader port note: the per-chunk stage bookkeeping (TERRAFORMED / TERRAFORM_STAGE) goes + * through the {@link Services#PLATFORM} chunk data-attachment seam rather than NeoForge's + * {@code chunk.getData/setData}; the terraform config/tuning keys are inlined (config seam deferred).

+ */ +public final class TerraformConversion { + + // --- Inlined from Config/Tuning (root shipped defaults) until the config seam lands --- + private static final boolean WATER_ENABLED = true; + private static final int WATER_MAX_DEPTH = 8; + private static final boolean PLANTS_ENABLED = true; + private static final boolean RESOURCES_ENABLED = true; + private static final double PLANT_CHANCE = 0.08D; + private static final double RESOURCE_CHANCE = 0.015D; + private static final double TREE_CHANCE = 0.03D; + + private TerraformConversion() { + } + + // --- Stage bookkeeping (DEEPER_TERRAFORM_DESIGN.md §2.2) ---------------- + + /** + * The chunk's effective terraform stage. Legacy chunks (pre-stage saves) carry only the + * {@code TERRAFORMED} boolean, which maps to stage 1 — the no-break contract. + */ + public static int effectiveStage(LevelChunk chunk) { + int stage = Services.PLATFORM.getTerraformStage(chunk); + return Math.max(stage, Services.PLATFORM.isTerraformed(chunk) ? 1 : 0); + } + + /** Raise the chunk's recorded stage to at least {@code stage} (never lowers it). */ + private static void bumpStage(LevelChunk chunk, int stage) { + if (Services.PLATFORM.getTerraformStage(chunk) < stage) { + Services.PLATFORM.setTerraformStage(chunk, stage); + chunk.markUnsaved(); + } + } + + /** + * Where stage-2 hydration draws its units from: the Terraformer's glacite-fed buffer for the live + * frontier, or {@code null} for the chunk-load catch-up (which converts for free). + */ + @FunctionalInterface + public interface HydrationSink { + /** @return units actually granted (0 = stall: stop advancing the stage-2 cursor). */ + int draw(int units); + } + + /** Convert one surface column: terrain → grass/dirt, flag breathable, write biome, plants, ore. */ + public static void convertColumn(ServerLevel level, int x, int z, int tier, Set biomeChanged) { + convertColumn(level, x, z, level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z), tier, biomeChanged); + } + + /** + * {@link #convertColumn(ServerLevel, int, int, int, Set)} with the surface height injected — the + * seam the gametests need because the data-driven test framework encases every arena in a barrier + * shell, so the in-arena heightmap always points at the barrier lid. + */ + public static void convertColumn(ServerLevel level, int x, int z, int surfaceY, int tier, + Set biomeChanged) { + BlockPos top = new BlockPos(x, surfaceY - 1, z); + BlockState topState = level.getBlockState(top); + + if (topState.is(ModTags.Blocks.TERRAFORM_TO_GRASS) && !topState.is(Blocks.GRASS_BLOCK)) { + level.setBlock(top, Blocks.GRASS_BLOCK.defaultBlockState(), Block.UPDATE_CLIENTS); + for (int d = 1; d <= 3; d++) { + BlockPos below = new BlockPos(x, surfaceY - 1 - d, z); + if (level.getBlockState(below).is(ModTags.Blocks.TERRAFORM_TO_DIRT)) { + level.setBlock(below, Blocks.DIRT.defaultBlockState(), Block.UPDATE_CLIENTS); + } + } + frontierFx(level, x, surfaceY, z); + } + + // Atmosphere payoff (§3.4): flag the chunk permanently breathable at/above the surface. + LevelChunk chunk = level.getChunkAt(top); + if (!Services.PLATFORM.isTerraformed(chunk)) { + Services.PLATFORM.setTerraformed(chunk, true); + chunk.markUnsaved(); + } + bumpStage(chunk, 1); + + if (writeBiomeColumn(level, chunk, x, z, ModBiomes.TERRAFORMED)) { + biomeChanged.add(chunk); + } + + scatterPlant(level, x, surfaceY, z); + seedResource(level, x, surfaceY, z, tier); + } + + // --- Stage 2: Hydrated (DEEPER_TERRAFORM_DESIGN.md §3.2) ---------------- + + /** + * Stage-2 conversion of one column: fill the basin below {@code waterTableY} with still water, + * one hydration unit per source placed. The scan walks down from the table; columns whose terrain + * sits at/above the table get no water (dry hills), and a chasm deeper than {@code WATER_MAX_DEPTH} + * is skipped entirely rather than part-filled mid-air. + * + * @param sink hydration supply, or {@code null} for the free chunk-load catch-up + * @return {@code false} when the sink ran dry mid-column (stall: re-run this column later); + * {@code true} when the column is fully hydrated (possibly needing no water at all) + */ + public static boolean hydrateColumn(ServerLevel level, int x, int z, int waterTableY, HydrationSink sink) { + LevelChunk chunk = level.getChunkAt(new BlockPos(x, 0, z)); + if (WATER_ENABLED) { + int maxDepth = WATER_MAX_DEPTH; + int table = Math.max(waterTableY, level.getMinY() + 1); + + // Find the basin floor: first non-fillable cell at/below the table, within the depth cap. + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(x, table, z); + int floorY = Integer.MIN_VALUE; + for (int depth = 0; depth <= maxDepth; depth++) { + int y = table - depth; + if (y <= level.getMinY()) { + break; + } + cursor.setY(y); + BlockState state = level.getBlockState(cursor); + if (!isWaterFillable(state)) { + floorY = y; + break; + } + } + + if (floorY != Integer.MIN_VALUE && floorY < table) { + // Fill bottom-up so a stall never leaves water floating above a gap. + for (int y = floorY + 1; y <= table; y++) { + cursor.setY(y); + BlockState state = level.getBlockState(cursor); + if (state.is(Blocks.WATER)) { + continue; // already hydrated — idempotent, no cost + } + if (!state.isAir() && !state.canBeReplaced()) { + break; // overhang closed the basin above this point + } + if (sink != null && sink.draw(1) <= 0) { + return false; // out of glacite — stall, keep the cursor on this column + } + level.setBlock(cursor, Blocks.WATER.defaultBlockState(), Block.UPDATE_CLIENTS); + } + } + } + bumpStage(chunk, 2); + return true; + } + + /** A cell the water fill may occupy: air, water, or replaceable ground cover (grass/flowers). */ + private static boolean isWaterFillable(BlockState state) { + return state.isAir() || state.is(Blocks.WATER) || state.canBeReplaced(); + } + + // --- Stage 3: Living (DEEPER_TERRAFORM_DESIGN.md §4–5) ------------------ + + /** + * Stage-3 conversion of one column: settle the mature per-planet biome (real weather), grow the + * occasional tree, and seed a starter herd of the planet's livestock. Idempotent per stage. + */ + public static void vivifyColumn(ServerLevel level, int x, int z, Set biomeChanged) { + LevelChunk chunk = level.getChunkAt(new BlockPos(x, 0, z)); + if (writeBiomeColumn(level, chunk, x, z, matureBiomeFor(level))) { + biomeChanged.add(chunk); + } + int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); + placeTree(level, x, surfaceY, z); + TerraformFauna.seedHerd(level, x, surfaceY, z); + bumpStage(chunk, 3); + } + + /** The mature stage-3 biome for this dimension (§4); unknown dimensions settle as meadow. */ + public static ResourceKey matureBiomeFor(ServerLevel level) { + ResourceKey dimension = level.dimension(); + if (ModDimensions.CINDARA_LEVEL.equals(dimension)) { + return ModBiomes.TERRAFORMED_SAVANNA; + } + if (ModDimensions.GLACIRA_LEVEL.equals(dimension)) { + return ModBiomes.TERRAFORMED_TUNDRA; + } + return ModBiomes.TERRAFORMED_MEADOW; + } + + /** Sparse GROWN trees on Living ground (§4): vanilla features, themed per planet. */ + private static void placeTree(ServerLevel level, int x, int surfaceY, int z) { + if (!PLANTS_ENABLED || level.getRandom().nextDouble() >= TREE_CHANCE) { + return; + } + BlockPos ground = new BlockPos(x, surfaceY - 1, z); + BlockPos above = ground.above(); + if (!level.getBlockState(ground).is(Blocks.GRASS_BLOCK) || !level.getBlockState(above).isAir()) { + return; + } + ResourceKey dimension = level.dimension(); + ResourceKey> tree; + if (ModDimensions.CINDARA_LEVEL.equals(dimension)) { + tree = TreeFeatures.ACACIA; + } else if (ModDimensions.GLACIRA_LEVEL.equals(dimension)) { + tree = TreeFeatures.SPRUCE; + } else { + tree = level.getRandom().nextBoolean() ? TreeFeatures.OAK : TreeFeatures.BIRCH; + } + level.registryAccess().lookupOrThrow(Registries.CONFIGURED_FEATURE).get(tree) + .ifPresent(holder -> holder.value().place( + level, level.getChunkSource().getGenerator(), level.getRandom(), above)); + } + + /** Write {@code biomeKey} down this column's sections. @return true if anything changed. */ + @SuppressWarnings("unchecked") + private static boolean writeBiomeColumn(ServerLevel level, LevelChunk chunk, int x, int z, + ResourceKey biomeKey) { + Holder terra = level.registryAccess() + .lookupOrThrow(Registries.BIOME).getOrThrow(biomeKey); + int bx = (x & 15) >> 2; // biome cells are 4-block resolution (0..3 within a section) + int bz = (z & 15) >> 2; + boolean changed = false; + for (LevelChunkSection section : chunk.getSections()) { + PalettedContainerRO> ro = section.getBiomes(); + if (!(ro instanceof PalettedContainer)) { + continue; // not writable — skip rather than risk a cast error + } + PalettedContainer> biomes = (PalettedContainer>) ro; + for (int by = 0; by < 4; by++) { + if (biomes.getAndSet(bx, by, bz, terra) != terra) { + changed = true; + } + } + } + if (changed) { + chunk.markUnsaved(); + } + return changed; + } + + /** Sparse grass/flower/sapling scatter on freshly grassed ground (terraform design §2.2). */ + private static void scatterPlant(ServerLevel level, int x, int surfaceY, int z) { + if (!PLANTS_ENABLED) { + return; + } + RandomSource rnd = level.getRandom(); + if (rnd.nextDouble() >= PLANT_CHANCE) { + return; + } + BlockPos ground = new BlockPos(x, surfaceY - 1, z); + BlockPos above = new BlockPos(x, surfaceY, z); + if (!level.getBlockState(ground).is(Blocks.GRASS_BLOCK) || !level.getBlockState(above).isAir()) { + return; + } + double roll = rnd.nextDouble(); + Block plant; + if (roll < 0.06D) { + plant = Blocks.OAK_SAPLING; + } else if (roll < 0.30D) { + plant = switch (rnd.nextInt(4)) { + case 0 -> Blocks.POPPY; + case 1 -> Blocks.DANDELION; + case 2 -> Blocks.CORNFLOWER; + default -> Blocks.AZURE_BLUET; + }; + } else { + plant = Blocks.SHORT_GRASS; + } + level.setBlock(above, plant.defaultBlockState(), Block.UPDATE_CLIENTS); + } + + /** Tier-3 low-rate ore seeding into the converted subsurface (terraform design §2.2 / §T3). */ + private static void seedResource(ServerLevel level, int x, int surfaceY, int z, int tier) { + if (tier < 3 || !RESOURCES_ENABLED) { + return; + } + RandomSource rnd = level.getRandom(); + if (rnd.nextDouble() >= RESOURCE_CHANCE) { + return; + } + Block ore = TerraformResources.pickOre(rnd); + if (ore == null) { + return; + } + int y = surfaceY - 4 - rnd.nextInt(8); + BlockPos orePos = new BlockPos(x, y, z); + if (level.hasChunk(orePos.getX() >> 4, orePos.getZ() >> 4) + && level.getBlockState(orePos).is(ModTags.Blocks.TERRAFORM_TO_DIRT)) { + level.setBlock(orePos, ore.defaultBlockState(), Block.UPDATE_CLIENTS); + } + } + + /** Sparse green frontier dust + a soft soil sound as a shell converts (terraform design §2.4). */ + private static void frontierFx(ServerLevel level, int x, int surfaceY, int z) { + RandomSource rnd = level.getRandom(); + if (rnd.nextFloat() < 0.10F) { + level.sendParticles(ParticleTypes.HAPPY_VILLAGER, + x + 0.5D, surfaceY + 0.1D, z + 0.5D, 2, 0.3D, 0.2D, 0.3D, 0.0D); + } + if (rnd.nextFloat() < 0.02F) { + level.playSound(null, x + 0.5D, surfaceY, z + 0.5D, + SoundEvents.GRASS_PLACE, SoundSource.BLOCKS, 0.3F, 0.8F + rnd.nextFloat() * 0.3F); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformResources.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformResources.java new file mode 100644 index 0000000..0240901 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformResources.java @@ -0,0 +1,56 @@ +package za.co.neroland.nerospace.machine; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; + +import org.jetbrains.annotations.Nullable; + +/** + * Resolves the Tier-3 terraform ore list (terraform design §2.2) into {@link Block}s. Defaults to + * Nerospace ores so seeding doesn't trivialise vanilla mining; resolved lazily and cached. + * + *

Cross-loader port note: the root drew the id list from {@code Config.TERRAFORM_RESOURCE_ORES}; + * the config seam is deferred, so the list is inlined to the root's shipped default.

+ */ +public final class TerraformResources { + + /** Inlined from {@code Config.TERRAFORM_RESOURCE_ORES} default until the config seam lands. */ + private static final List ORE_IDS = List.of( + "nerospace:nerosteel_ore", "nerospace:xertz_quartz_ore", "nerospace:nerosium_ore"); + + private static List cached; + + private TerraformResources() { + } + + @Nullable + public static Block pickOre(RandomSource rnd) { + List ores = resolve(); + return ores.isEmpty() ? null : ores.get(rnd.nextInt(ores.size())); + } + + private static synchronized List resolve() { + if (cached != null) { + return cached; + } + List out = new ArrayList<>(); + for (String id : ORE_IDS) { + Identifier rl = Identifier.tryParse(id); + if (rl == null) { + continue; + } + Block block = BuiltInRegistries.BLOCK.getValue(rl); + if (block != null && block != Blocks.AIR) { + out.add(block); + } + } + cached = out; + return out; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformFauna.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformFauna.java new file mode 100644 index 0000000..2f47122 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformFauna.java @@ -0,0 +1,72 @@ +package za.co.neroland.nerospace.world; + +import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.registry.ModEntities; + +/** + * Starter-herd seeding for Living terraformed ground (DEEPER_TERRAFORM_DESIGN.md §5). Natural + * CREATURE spawning alone is too slow post-worldgen, so a sparse fraction of stage-3 columns actively + * spawns a pair of the planet's livestock — capped by a nearby-population count so herds never balloon. + * Biome spawn settings on the mature biomes remain the long-term backstop. + * + *

Cross-loader port note: the herd config (enable/chance/cap/radius) is inlined to the root's + * shipped defaults (config seam deferred).

+ */ +public final class TerraformFauna { + + private static final boolean FAUNA_ENABLED = true; + private static final double HERD_CHANCE = 0.02D; + private static final int HERD_RADIUS = 48; + private static final int HERD_CAP = 8; + + private TerraformFauna() { + } + + /** The livestock species seeded on this dimension's Living ground (§5), or null for none. */ + @Nullable + public static EntityType livestockFor(ServerLevel level) { + ResourceKey dimension = level.dimension(); + if (ModDimensions.CINDARA_LEVEL.equals(dimension)) { + return ModEntities.EMBER_STRUTTER.get(); + } + if (ModDimensions.GLACIRA_LEVEL.equals(dimension)) { + return ModEntities.WOOLLY_DRIFT.get(); + } + return ModEntities.MEADOW_LOPER.get(); + } + + /** Maybe seed a starter pair on a freshly Living column (sparse, population-capped). */ + public static void seedHerd(ServerLevel level, int x, int surfaceY, int z) { + if (!FAUNA_ENABLED) { + return; + } + RandomSource rnd = level.getRandom(); + if (rnd.nextDouble() >= HERD_CHANCE) { + return; + } + EntityType species = livestockFor(level); + if (species == null) { + return; + } + BlockPos ground = new BlockPos(x, surfaceY, z); + int nearby = level.getEntities(species, new AABB(ground).inflate(HERD_RADIUS, HERD_RADIUS, HERD_RADIUS), + e -> e.isAlive()).size(); + if (nearby >= HERD_CAP) { + return; + } + for (int i = 0; i < 2; i++) { + species.spawn(level, ground, EntitySpawnReason.EVENT); + } + } +} From 773fd8b74f6a7b15796fdedb6f2d972d29150a38 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:22:25 +0200 Subject: [PATCH 50/82] Add TerraformManager and chunk-load catch-up hooks Introduce TerraformManager SavedData to track per-terraformer stage radii/tier and replay staged conversions when chunks load. Wire the manager into both Fabric and NeoForge entry points so chunk-load events invoke TerraformManager.onChunkLoaded (Fabric: ServerChunkEvents.CHUNK_LOAD SAM with a 3-param newlyGenerated flag; NeoForge: ChunkEvent.Load). Update the multiloader port checklist docs to mark slice 4a and document the chunk-load seam and terraform manager addition. --- docs/MULTILOADER_PORT_CHECKLIST.md | 20 +- .../nerospace/world/TerraformManager.java | 233 ++++++++++++++++++ .../nerospace/fabric/NerospaceFabric.java | 6 + .../nerospace/neoforge/NerospaceNeoForge.java | 10 + 4 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformManager.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index c5ab91b..46d5b19 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,15 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~172 classes ported, ~92 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~173 classes ported, ~91 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 4a (TerraformManager + chunk-load seam).** All 4 cells green. +> Added `world/TerraformManager` (3rd SavedData; per-terraformer radii + `onChunkLoaded` catch-up) + a +> per-loader chunk-load hook. **26.x gotcha: Fabric `ServerChunkEvents.Load` SAM is 3-param +> `(ServerLevel, LevelChunk, boolean)`** (probed). Remaining: slice 4b = the Terraformer machine BE (rewrite +> onto EnergyBuffer/Container, defer force-load) + block/menu/screen — the slice that drives the engine. + > **2026-06-21 update — terraforming slice 3 (conversion engine).** All 4 cells green. Added > `machine/{TerraformConversion (335ln staged converter), TerraformResources}` + `world/TerraformFauna`; > stage bookkeeping rewired from `chunk.getData(ModAttachments…)` onto the slice-1 `Services.PLATFORM` chunk @@ -187,8 +193,16 @@ checked by a headless build). list), `world/TerraformFauna` (inlined herd config). Worldgen APIs (`TreeFeatures`, `ConfiguredFeature.place`, `PalettedContainer` biome write, `EntityType.spawn`) all resolve on common. ~7 config/tuning keys inlined. All 4 cells green. (Inert until slice 4's machine + manager drive it.) - - [ ] **Slice 4 — Terraformer machine.** `TerraformerBlock`(+BE 584ln)+menu+screen + `TerraformManager` - (SavedData; 4-arg `SavedDataType`) + chunk-load catch-up hook (per-loader) + biome-sync packet. + - [x] **Slice 4a — TerraformManager + chunk-load seam.** `world/TerraformManager` (3rd `SavedData`, + 4-arg `SavedDataType`; tracks per-terraformer stage radii; `onChunkLoaded` replays staged conversion on + in-range columns of newly-loaded chunks + biome-sync packet). Per-loader chunk-load hook: NeoForge + `ChunkEvent.Load` (filter `ServerLevel`+`LevelChunk`), Fabric `ServerChunkEvents.CHUNK_LOAD` (**3-param + SAM `(ServerLevel, LevelChunk, boolean newlyGenerated)`** — probed). All 4 cells green. (Inert until 4b.) + - [ ] **Slice 4b — Terraformer machine.** `TerraformerBlock`(+BE 584ln)+menu+screen — rewrite the BE onto + `EnergyBuffer` + a `Container`/`NonNullList` upgrade slot (drop NeoForge `SimpleEnergyHandler`/ + `MachineItemHandler`/`ResourceHandler`); **defer force-load** (opt-in, off by default, needs + `TerraformChunkLoader`); inline Tuning/Config; calls `TerraformManager.update`. + register block/item/BE/menu + + screen (per-loader) + energy/item caps + assets + lang. This is the slice that makes terraforming run. - [ ] **Slice 5 — Hydration Module** (block/BE/menu/screen) + stage-2 wiring. - [ ] **Slice 6 — Terraform Monitor** (block/BE/menu/screen) + `TerraformDrift` + `TerraformChunkLoader` + `GreenxertzAtmosphere`. Risk: **high** (world mutation, chunk-loading, events). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformManager.java new file mode 100644 index 0000000..cc7feac --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformManager.java @@ -0,0 +1,233 @@ +package za.co.neroland.nerospace.world; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import it.unimi.dsi.fastutil.longs.Long2IntMap; +import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.machine.TerraformConversion; + +/** + * Per-{@link ServerLevel} registry of active Terraformers and how far each has expanded (terraform + * design §2.3, lazy chunk handling; staged per DEEPER_TERRAFORM_DESIGN.md). The live frontier skips + * columns whose chunk is unloaded; this registry lets a chunk-load handler convert any in-range + * columns when those chunks load later, so a terraformed planet finishes converting as the player + * explores it — without force-loading. + * + *

Persists each terraformer's {@code (centre, radius, hydrationRadius, lifeRadius, tier)} so the + * catch-up works even while the terraformer's own chunk is unloaded. The stage radii are OPTIONAL in + * the codec (default 0) so pre-stage saves parse unchanged.

+ * + *

Cross-loader port note: the multiloader's third {@link SavedData} (4-arg {@code SavedDataType}, + * DataFixTypes null); the chunk-load catch-up is driven per-loader (NeoForge {@code ChunkEvent.Load}, + * Fabric {@code ServerChunkEvents.CHUNK_LOAD}).

+ */ +public final class TerraformManager extends SavedData { + + public static final Identifier ID = Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "terraformers"); + + public static final SavedDataType TYPE = new SavedDataType<>( + ID, TerraformManager::new, codec(), null); + + /** Terraformer centre (packed BlockPos) → current horizontal stage-1 radius. */ + private final Long2IntOpenHashMap radius = new Long2IntOpenHashMap(); + /** Terraformer centre (packed BlockPos) → machine tier. */ + private final Long2IntOpenHashMap tier = new Long2IntOpenHashMap(); + /** Terraformer centre (packed BlockPos) → stage-2 (Hydrated) radius. */ + private final Long2IntOpenHashMap hydrationRadius = new Long2IntOpenHashMap(); + /** Terraformer centre (packed BlockPos) → stage-3 (Living) radius. */ + private final Long2IntOpenHashMap lifeRadius = new Long2IntOpenHashMap(); + + public TerraformManager() { + this.radius.defaultReturnValue(-1); + this.tier.defaultReturnValue(1); + this.hydrationRadius.defaultReturnValue(0); + this.lifeRadius.defaultReturnValue(0); + } + + /** Public for the save-compat gametest, which decodes a legacy (pre-stage) payload through it. */ + public static Codec codec() { + return RecordCodecBuilder.create(inst -> inst.group( + Codec.LONG.listOf().fieldOf("positions").forGetter(m -> new ArrayList<>(m.radius.keySet())), + Codec.INT.listOf().fieldOf("radii").forGetter(m -> m.inKeyOrder(m.radius, 0)), + Codec.INT.listOf().fieldOf("tiers").forGetter(m -> m.inKeyOrder(m.tier, 1)), + // Stage radii are additive (DEEPER_TERRAFORM_DESIGN.md §9): absent in legacy saves. + Codec.INT.listOf().optionalFieldOf("hydration_radii", List.of()) + .forGetter(m -> m.inKeyOrder(m.hydrationRadius, 0)), + Codec.INT.listOf().optionalFieldOf("life_radii", List.of()) + .forGetter(m -> m.inKeyOrder(m.lifeRadius, 0)) + ).apply(inst, TerraformManager::fromLists)); + } + + /** The map's values in {@link #radius} key order (the codec's shared ordering), with a default. */ + private List inKeyOrder(Long2IntOpenHashMap map, int fallback) { + List out = new ArrayList<>(); + for (long k : this.radius.keySet()) { + out.add(map.containsKey(k) ? map.get(k) : fallback); + } + return out; + } + + private static TerraformManager fromLists(List positions, List radii, + List tiers, List hydrationRadii, List lifeRadii) { + TerraformManager m = new TerraformManager(); + for (int i = 0; i < positions.size(); i++) { + long key = positions.get(i); + m.radius.put(key, i < radii.size() ? radii.get(i) : 0); + m.tier.put(key, i < tiers.size() ? tiers.get(i) : 1); + m.hydrationRadius.put(key, i < hydrationRadii.size() ? hydrationRadii.get(i) : 0); + m.lifeRadius.put(key, i < lifeRadii.size() ? lifeRadii.get(i) : 0); + } + return m; + } + + public static TerraformManager get(ServerLevel level) { + return level.getDataStorage().computeIfAbsent(TYPE); + } + + /** Visits every registered terraformer with its centre and per-stage radii. */ + public interface MachineVisitor { + void visit(BlockPos center, int radius, int hydrationRadius, int lifeRadius); + } + + /** Iterates the registered machines (cosmetic drift target selection — design §2.3). */ + public void forEachMachine(MachineVisitor visitor) { + for (Long2IntMap.Entry e : this.radius.long2IntEntrySet()) { + long key = e.getLongKey(); + visitor.visit(BlockPos.of(key), Math.max(0, e.getIntValue()), + this.hydrationRadius.get(key), this.lifeRadius.get(key)); + } + } + + /** + * The recorded radius of {@code center}'s stage frontier (1 = Rooted, 2 = Hydrated, 3 = Living); + * 0 for unknown machines. Used by the Terraform Monitor readout and the save-compat gametest. + */ + public int stageRadius(BlockPos center, int stage) { + long key = center.asLong(); + return switch (stage) { + case 2 -> this.hydrationRadius.get(key); + case 3 -> this.lifeRadius.get(key); + default -> Math.max(0, this.radius.get(key)); + }; + } + + /** A terraformer reports its current reach (all stage frontiers) each work cycle. */ + public void update(BlockPos center, int currentRadius, int currentHydrationRadius, + int currentLifeRadius, int machineTier) { + long key = center.asLong(); + boolean changed = this.radius.get(key) != currentRadius + || this.tier.get(key) != machineTier + || this.hydrationRadius.get(key) != currentHydrationRadius + || this.lifeRadius.get(key) != currentLifeRadius; + if (changed) { + this.radius.put(key, currentRadius); + this.tier.put(key, machineTier); + this.hydrationRadius.put(key, currentHydrationRadius); + this.lifeRadius.put(key, currentLifeRadius); + setDirty(); + } + } + + public void remove(BlockPos center) { + long key = center.asLong(); + if (this.radius.remove(key) != this.radius.defaultReturnValue()) { + this.tier.remove(key); + this.hydrationRadius.remove(key); + this.lifeRadius.remove(key); + setDirty(); + } + } + + /** + * Catch-up conversion when a chunk loads: replay, per column, every stage whose radius reaches it + * and that the chunk hasn't recorded yet. Stage replays above the chunk's recorded stage only. + */ + public void onChunkLoaded(ServerLevel level, LevelChunk chunk) { + if (this.radius.isEmpty()) { + return; + } + int chunkStage = TerraformConversion.effectiveStage(chunk); + if (chunkStage >= 3) { + return; // fully Living — nothing left to replay + } + ChunkPos cp = chunk.getPos(); + int minX = cp.getMinBlockX(); + int minZ = cp.getMinBlockZ(); + Set biomeChanged = new HashSet<>(); + boolean any = false; + + for (Long2IntMap.Entry e : this.radius.long2IntEntrySet()) { + BlockPos center = BlockPos.of(e.getLongKey()); + int r = e.getIntValue(); + if (r <= 0) { + continue; + } + // Skip terraformers whose radius can't reach this chunk at all. + int cx = center.getX(); + int cz = center.getZ(); + if (cx + r < minX || cx - r > minX + 15 || cz + r < minZ || cz - r > minZ + 15) { + continue; + } + long key = e.getLongKey(); + long rSq = (long) r * r; + int hydR = this.hydrationRadius.get(key); + long hydSq = (long) hydR * hydR; + int lifeR = this.lifeRadius.get(key); + long lifeSq = (long) lifeR * lifeR; + int t = this.tier.getOrDefault(key, 1); + // The catch-up water table mirrors TerraformerBlockEntity#waterTableY (machine base − 1). + int tableY = center.getY() - 1; + + for (int dx = 0; dx < 16; dx++) { + for (int dz = 0; dz < 16; dz++) { + int x = minX + dx; + int z = minZ + dz; + long ddx = x - cx; + long ddz = z - cz; + long dSq = ddx * ddx + ddz * ddz; + if (dSq > rSq) { + continue; + } + if (chunkStage < 1) { + TerraformConversion.convertColumn(level, x, z, t, biomeChanged); + any = true; + } + if (chunkStage < 2 && hydR > 0 && dSq <= hydSq) { + TerraformConversion.hydrateColumn(level, x, z, tableY, null); + any = true; + } + if (chunkStage < 3 && lifeR > 0 && dSq <= lifeSq) { + TerraformConversion.vivifyColumn(level, x, z, biomeChanged); + any = true; + } + } + } + } + + if (any && !biomeChanged.isEmpty()) { + ClientboundChunksBiomesPacket packet = + ClientboundChunksBiomesPacket.forChunks(new ArrayList<>(biomeChanged)); + for (ServerPlayer player : level.players()) { + player.connection.send(packet); + } + } + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 745d9c4..68bdbbf 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -1,6 +1,7 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; @@ -27,6 +28,7 @@ import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.world.OxygenFieldEvents; +import za.co.neroland.nerospace.world.TerraformManager; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.world.OxygenManager; @@ -87,6 +89,10 @@ public void register(EntityType type, SpawnPlacementType plac MeteorEvents.tick(server); OxygenFieldEvents.tick(server); }); + // Terraform catch-up: convert any in-range columns on chunks that load after the frontier passed. + // (Fabric's Load SAM passes a third "newly generated" flag, which we don't need.) + ServerChunkEvents.CHUNK_LOAD.register((serverLevel, chunk, newlyGenerated) -> + TerraformManager.get(serverLevel).onChunkLoaded(serverLevel, chunk)); // Item-storage capability (Fabric Transfer API) — counterpart to NeoForge // Capabilities.Item.BLOCK; lets mod pipes move items in/out of the item store. diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index da243de..8488c4d 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -8,9 +8,12 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import net.neoforged.neoforge.event.entity.RegisterSpawnPlacementsEvent; +import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent; +import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.Mob; import net.minecraft.world.entity.SpawnPlacementType; @@ -25,6 +28,7 @@ import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; import za.co.neroland.nerospace.world.OxygenManager; +import za.co.neroland.nerospace.world.TerraformManager; /** * NeoForge entry point. Runs shared init (building the DeferredRegisters via the @@ -57,6 +61,12 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { MeteorEvents.tick(event.getServer()); OxygenFieldEvents.tick(event.getServer()); }); + // Terraform catch-up: convert any in-range columns on chunks that load after the frontier passed. + NeoForge.EVENT_BUS.addListener((ChunkEvent.Load event) -> { + if (event.getLevel() instanceof ServerLevel serverLevel && event.getChunk() instanceof LevelChunk chunk) { + TerraformManager.get(serverLevel).onChunkLoaded(serverLevel, chunk); + } + }); // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated // Nerospace tab registered via the vanilla CREATIVE_MODE_TAB registry), so no NeoForge-specific // BuildCreativeModeTabContentsEvent injection is needed. From 4dab9c69c279f62a5ceba527729895cceef26e56 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:36:03 +0200 Subject: [PATCH 51/82] Add Terraformer machine (block, BE, UI, assets) Implements the Terraformer machine (slice 4b): adds TerraformerBlock, TerraformerBlockEntity (rebuilt on EnergyBuffer with a WorldlyContainer upgrade slot), TerraformerMenu and TerraformerScreen, plus block/item/BE/menu registrations and Fabric client/server capability bindings. Includes models, blockstates, textures, GUI texture and English lang keys. Tile entity persists frontier state (radius/cursors/hydration/tier), advances three-stage terraforming frontiers, updates TerraformManager for chunk-load catch-up (active force-load deferred), and exposes synced ContainerData for the UI. Also updates MULTILOADER_PORT_CHECKLIST to mark slice 4b done. --- docs/MULTILOADER_PORT_CHECKLIST.md | 22 +- .../nerospace/client/TerraformerScreen.java | 52 ++ .../nerospace/machine/MachineRedstone.java | 29 + .../nerospace/machine/TerraformerBlock.java | 94 ++++ .../machine/TerraformerBlockEntity.java | 522 ++++++++++++++++++ .../nerospace/menu/TerraformerMenu.java | 143 +++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 6 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../nerospace/blockstates/terraformer.json | 19 + .../assets/nerospace/items/terraformer.json | 6 + .../assets/nerospace/lang/en_us.json | 9 + .../nerospace/models/block/terraformer.json | 74 +++ .../nerospace/textures/block/terraformer.png | Bin 0 -> 529 bytes .../textures/block/terraformer_front.png | Bin 0 -> 428 bytes .../textures/block/terraformer_top.png | Bin 0 -> 410 bytes .../nerospace/textures/gui/terraformer.png | Bin 0 -> 5751 bytes .../nerospace/fabric/NerospaceFabric.java | 8 + .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeCapabilities.java | 12 + .../neoforge/NeoForgeClientSetup.java | 2 + 22 files changed, 1006 insertions(+), 7 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformerScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/MachineRedstone.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformerMenu.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/terraformer.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/terraformer.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/terraformer.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_front.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_top.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraformer.png diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 46d5b19..360f4a5 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~173 classes ported, ~91 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~178 classes ported, ~86 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 4b: the Terraformer machine.** All 4 cells green. Added +> `machine/{MachineRedstone, TerraformerBlock, TerraformerBlockEntity}` + `menu/TerraformerMenu` + +> `client/TerraformerScreen`; the 584-line BE rewritten onto `EnergyBuffer` + a `WorldlyContainer` upgrade slot +> (force-load deferred). Registered + capped + assets/lang. **Placing a Terraformer now greens the planet +> outward through the three stages** — the signature feature is functional cross-loader. Remaining terraform +> slices: 5 Hydration Module, 6 Monitor + Drift + ChunkLoader + GreenxertzAtmosphere. + > **2026-06-21 update — terraforming slice 4a (TerraformManager + chunk-load seam).** All 4 cells green. > Added `world/TerraformManager` (3rd SavedData; per-terraformer radii + `onChunkLoaded` catch-up) + a > per-loader chunk-load hook. **26.x gotcha: Fabric `ServerChunkEvents.Load` SAM is 3-param @@ -198,11 +205,14 @@ checked by a headless build). in-range columns of newly-loaded chunks + biome-sync packet). Per-loader chunk-load hook: NeoForge `ChunkEvent.Load` (filter `ServerLevel`+`LevelChunk`), Fabric `ServerChunkEvents.CHUNK_LOAD` (**3-param SAM `(ServerLevel, LevelChunk, boolean newlyGenerated)`** — probed). All 4 cells green. (Inert until 4b.) - - [ ] **Slice 4b — Terraformer machine.** `TerraformerBlock`(+BE 584ln)+menu+screen — rewrite the BE onto - `EnergyBuffer` + a `Container`/`NonNullList` upgrade slot (drop NeoForge `SimpleEnergyHandler`/ - `MachineItemHandler`/`ResourceHandler`); **defer force-load** (opt-in, off by default, needs - `TerraformChunkLoader`); inline Tuning/Config; calls `TerraformManager.update`. + register block/item/BE/menu - + screen (per-loader) + energy/item caps + assets + lang. This is the slice that makes terraforming run. + - [x] **Slice 4b — Terraformer machine DONE (4 cells green).** `machine/{MachineRedstone, TerraformerBlock, + TerraformerBlockEntity}` + `menu/TerraformerMenu` + `client/TerraformerScreen`. BE rewritten onto + `EnergyBuffer` + a vanilla `WorldlyContainer`/`NonNullList` upgrade slot (dropped NeoForge + `SimpleEnergyHandler`/`MachineItemHandler`/`ResourceHandler`); **force-load deferred** (unloaded columns + handled by the slice-4a catch-up); Tuning/Config inlined; drives `TerraformConversion` 3-stage frontier + + `TerraformManager.update` + biome-sync packet. Registered block/item/BE/menu + per-loader screen + energy/item + caps; copied block (3 textures, FACING blockstate, multi-tex model) + GUI texture + 9 lang keys. **Placing a + Terraformer now greens the planet outward (Rooted→Hydrated→Living).** - [ ] **Slice 5 — Hydration Module** (block/BE/menu/screen) + stage-2 wiring. - [ ] **Slice 6 — Terraform Monitor** (block/BE/menu/screen) + `TerraformDrift` + `TerraformChunkLoader` + `GreenxertzAtmosphere`. Risk: **high** (world mutation, chunk-loading, events). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformerScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformerScreen.java new file mode 100644 index 0000000..b6f9087 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformerScreen.java @@ -0,0 +1,52 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.menu.TerraformerMenu; + +/** + * Screen for the Terraformer (grid-only): a power-buffer gauge (fed by pipes), the tier-upgrade slot, + * and a live tier / per-stage frontier-radius readout, themed green. + */ +public class TerraformerScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/terraformer.png"); + private static final int ACCENT = 0xFF54D46A; // green (terraform) + + public TerraformerScreen(TerraformerMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 166); + this.titleLabelX = 10; + this.inventoryLabelX = 10; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int energy = this.menu.getEnergy(); + int max = this.menu.getMaxEnergy(); + int pct = max == 0 ? 0 : energy * 100 / max; + float energyFrac = max == 0 ? 0f : (float) energy / max; + + label(g, Component.translatable("gui.nerospace.terraformer.power", pct), 8, 20, 0xFFCFE7FF); + segGauge(g, 8, 31, 160, 6, energyFrac, ACCENT); + + label(g, Component.translatable("gui.nerospace.terraformer.tier", this.menu.getTier()), 8, 48, ACCENT); + label(g, Component.translatable("gui.nerospace.terraformer.stages", this.menu.getRadius(), + this.menu.getHydrationRadius(), this.menu.getLifeRadius()), 8, 60, 0xFFB7E8C2); + + boolean active = this.menu.isActive(); + label(g, Component.translatable(active + ? "gui.nerospace.terraformer.working" + : "gui.nerospace.terraformer.idle"), 116, 20, active ? 0xFF9CF0C0 : 0xFF7E8EA0); + + label(g, Component.translatable("gui.nerospace.terraformer.hydration", + this.menu.getHydration()), 116, 48, 0xFF78D2F0); + if (this.menu.isHydrationStalled()) { + label(g, Component.translatable("gui.nerospace.terraformer.needs_glacite"), 116, 60, 0xFFFF6A5E); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/MachineRedstone.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/MachineRedstone.java new file mode 100644 index 0000000..7f906a4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/MachineRedstone.java @@ -0,0 +1,29 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +/** + * Redstone control for the world-changing machines (Terraformer, …). A machine with NO adjacent + * redstone component keeps the classic always-on behaviour — existing builds are untouched. Wiring any + * signal source against it (a lever, a button, dust, …) turns that signal into a real switch: powered = + * running, unpowered = idle. + */ +public final class MachineRedstone { + + private MachineRedstone() { + } + + /** Whether redstone allows the machine at {@code pos} to run this tick (see class javadoc). */ + public static boolean allowsRun(Level level, BlockPos pos) { + boolean wired = false; + for (Direction side : Direction.values()) { + if (level.getBlockState(pos.relative(side)).isSignalSource()) { + wired = true; + break; + } + } + return !wired || level.hasNeighborSignal(pos); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlock.java new file mode 100644 index 0000000..8fa4097 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlock.java @@ -0,0 +1,94 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The Terraformer block (terraform design §2): a ticking machine backed by + * {@link TerraformerBlockEntity} that advances an expanding terrain-conversion frontier while powered. + */ +public class TerraformerBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(TerraformerBlock::new); + /** The core lens faces the placer. Visual only. */ + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public TerraformerBlock(Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new TerraformerBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.TERRAFORMER.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof TerraformerBlockEntity be) { + serverPlayer.openMenu(be); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof TerraformerBlockEntity be ? be.comparatorSignal() : 0; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java new file mode 100644 index 0000000..7d71b0f --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java @@ -0,0 +1,522 @@ +package za.co.neroland.nerospace.machine; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.menu.TerraformerMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.world.TerraformManager; + +/** + * Terraformer machine (terraform design §2). An internal energy buffer (grid-fed) slowly converts a + * dead planet into livable ground by advancing an expanding circular frontier from its own column + * outward, in three trailing stages (Rooted → Hydrated → Living). Conversion is idempotent, so only + * {@code radius + cursor} per stage need persisting; energy is the throttle. Higher tiers convert more + * columns per cycle and unlock ore seeding. + * + *

Cross-loader port note: rebuilt on the shared {@link EnergyBuffer} + a vanilla + * {@link WorldlyContainer} upgrade slot (the root used the NeoForge transfer API + {@code + * MachineItemHandler}). Tuning/Config values are inlined. Opt-in active force-loading is deferred — the + * {@link TerraformManager} chunk-load catch-up converts unloaded columns when they load instead.

+ */ +public class TerraformerBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int UPGRADE_SLOT = 0; + public static final int SIZE = 1; + public static final int DATA_COUNT = 9; + + // --- Inlined Tuning/Config base values (config seam deferred) --- + public static final int ENERGY_BUFFER = 100_000; + public static final int ENERGY_MAX_INSERT = 2_000; + private static final int ENERGY_PER_BLOCK = 12; // stage 1 + private static final int STAGE2_ENERGY_PER_BLOCK = 24; // ×2 + private static final int STAGE3_ENERGY_PER_BLOCK = 48; // ×4 + private static final int WORK_INTERVAL_TICKS = 8; + private static final int HYDRATION_CAP = 1_024; + private static final int MAX_COLUMNS_PER_TICK = 48; + + private static final int[] SLOTS = {UPGRADE_SLOT}; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + private final EnergyBuffer energy = new EnergyBuffer(ENERGY_BUFFER, ENERGY_MAX_INSERT, 0, this::setChanged); + + /** Machine tier (1..3): more columns per cycle, and Tier 3 unlocks ore seeding. */ + private int tier = 1; + /** Expanding stage-1 frontier: horizontal radius + within-ring column cursor. */ + private int radius; + private int cursor; + /** Trailing stage frontiers (invariant: {@code lifeRadius <= hydrationRadius <= radius}). */ + private int hydrationRadius; + private int hydrationCursor; + private int lifeRadius; + private int lifeCursor; + /** Buffered hydration units (melted glacite, fed by an adjacent Hydration Module — §3.1). */ + private int hydration; + /** Transient: stage 2 wants water but the hydration buffer ran dry (GUI/Monitor stall reason). */ + private transient boolean hydrationStalled; + + /** Transient per-stage caches of the current ring's column offsets (recomputed on load). */ + private final transient List[] rings = newRingCache(); + private final transient int[] ringsFor = {-1, -1, -1}; + + @SuppressWarnings("unchecked") + private static List[] newRingCache() { + return (List[]) new List[3]; + } + + /** + * Synced to the menu: [0]=energy [1]=capacity [2]=tier [3]=radius [4]=hydration + * [5]=hydrationCap [6]=hydrationRadius [7]=lifeRadius [8]=hydrationStalled. + */ + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> energy.getRaw(); + case 1 -> ENERGY_BUFFER; + case 2 -> tier; + case 3 -> radius; + case 4 -> hydration; + case 5 -> HYDRATION_CAP; + case 6 -> hydrationRadius; + case 7 -> lifeRadius; + case 8 -> hydrationStalled ? 1 : 0; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + switch (index) { + case 2 -> tier = value; + case 3 -> radius = value; + case 4 -> hydration = value; + case 6 -> hydrationRadius = value; + case 7 -> lifeRadius = value; + case 8 -> hydrationStalled = value != 0; + default -> { } + } + } + + @Override + public int getCount() { + return DATA_COUNT; + } + }; + + public TerraformerBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.TERRAFORMER.get(), pos, state); + } + + /** Exposed via the mod's energy capability/lookup (insert-only — grid powered). */ + public NerospaceEnergyStorage getEnergy() { + return this.energy; + } + + public ContainerData getDataAccess() { + return this.dataAccess; + } + + public int getTier() { + return this.tier; + } + + public boolean isActive() { + return this.energy.getAmount() >= ENERGY_PER_BLOCK; + } + + public int comparatorSignal() { + int stored = this.energy.getRaw(); + return stored <= 0 ? 0 : 1 + (int) (stored / (double) ENERGY_BUFFER * 14.0D); + } + + /** Stage-1 columns converted per work cycle, by tier (capped for TPS). */ + private int budgetPerCycle() { + int base = switch (this.tier) { + case 3 -> 6; + case 2 -> 3; + default -> 1; + }; + return Math.min(base, MAX_COLUMNS_PER_TICK); + } + + /** Per-stage column budget for one work cycle: trailing stages get smaller guaranteed shares. */ + private int stageBudget(int stage) { + int base = budgetPerCycle(); + return switch (stage) { + case 2 -> Math.max(1, base / 2); + case 3 -> Math.max(1, base / 4); + default -> base; + }; + } + + /** Per-column energy cost of a stage (stage 2 ×2, stage 3 ×4). */ + private static int stageCost(int stage) { + return switch (stage) { + case 2 -> STAGE2_ENERGY_PER_BLOCK; + case 3 -> STAGE3_ENERGY_PER_BLOCK; + default -> ENERGY_PER_BLOCK; + }; + } + + /** The stage-2 water table: one below the machine's own base, derived (not persisted). */ + public int waterTableY() { + return this.worldPosition.getY() - 1; + } + + public int getHydration() { + return this.hydration; + } + + /** Accepts melted glacite from an adjacent Hydration Module (§3.1). @return units actually accepted. */ + public int acceptHydration(int units) { + int accepted = Math.min(units, HYDRATION_CAP - this.hydration); + if (accepted > 0) { + this.hydration += accepted; + setChanged(); + } + return accepted; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (!(level instanceof ServerLevel serverLevel)) { + return; + } + + // Auto-consume tier upgrades: a Nerosteel Ingot → T2, a Cindrite → T3. + ItemStack upgrade = this.items.get(UPGRADE_SLOT); + if (!upgrade.isEmpty()) { + if (this.tier < 2 && upgrade.is(ModItems.NEROSTEEL_INGOT.get())) { + upgrade.shrink(1); + this.tier = 2; + setChanged(); + } else if (this.tier < 3 && upgrade.is(ModItems.CINDRITE.get())) { + upgrade.shrink(1); + this.tier = 3; + setChanged(); + } + } + + // Redstone switch: wired machines only sweep while powered. + if (serverLevel.getGameTime() % WORK_INTERVAL_TICKS == 0 && MachineRedstone.allowsRun(level, pos)) { + work(serverLevel, pos); + } + } + + private void work(ServerLevel level, BlockPos center) { + boolean changed = false; + Set biomeChanged = new HashSet<>(); + + // Outermost first: stage 1 keeps priority so breathable ground never waits on water. + this.hydrationStalled = false; + changed |= workStage(level, center, 1, biomeChanged); + changed |= workStage(level, center, 2, biomeChanged); + changed |= workStage(level, center, 3, biomeChanged); + + // Publish our reach so the chunk-load handler can catch up unloaded columns later. + TerraformManager.get(level).update(this.worldPosition, + this.radius, this.hydrationRadius, this.lifeRadius, this.tier); + + // Resync any chunks whose biome changed so clients recolour the terraformed ground. + if (!biomeChanged.isEmpty()) { + ClientboundChunksBiomesPacket packet = + ClientboundChunksBiomesPacket.forChunks(new ArrayList<>(biomeChanged)); + for (ServerPlayer player : level.players()) { + player.connection.send(packet); + } + } + + if (changed) { + setChanged(); + } + } + + /** + * Runs one stage's frontier for this cycle: up to {@link #stageBudget} columns, each costing + * {@link #stageCost} energy. A trailing frontier only advances while strictly inside its + * predecessor's radius; a stage-2 column that needs water it can't pay for stalls in place. + */ + private boolean workStage(ServerLevel level, BlockPos center, int stage, Set biomeChanged) { + int cost = stageCost(stage); + int budget = stageBudget(stage); + boolean changed = false; + + for (int processed = 0; processed < budget; processed++) { + if (this.energy.getAmount() < cost) { + break; + } + int stageRadius = switch (stage) { + case 2 -> this.hydrationRadius; + case 3 -> this.lifeRadius; + default -> this.radius; + }; + // Trailing frontiers stay strictly inside the predecessor (its current ring is mid-work). + if (stage == 2 && stageRadius >= this.radius) { + break; + } + if (stage == 3 && stageRadius >= this.hydrationRadius) { + break; + } + + List r = ring(stage, stageRadius); + int stageCursor = switch (stage) { + case 2 -> this.hydrationCursor; + case 3 -> this.lifeCursor; + default -> this.cursor; + }; + if (stageCursor >= r.size()) { + stageRadius++; // grow outward — no cap + stageCursor = 0; + setStageFrontier(stage, stageRadius, stageCursor); + changed = true; + continue; + } + + int[] off = r.get(stageCursor); + int x = center.getX() + off[0]; + int z = center.getZ() + off[1]; + if (!level.hasChunk(x >> 4, z >> 4)) { + // Lazy: leave unloaded columns for the chunk-load catch-up (TerraformManager). Active + // force-loading is deferred (opt-in, off by default in the root). No energy spent. + setStageFrontier(stage, stageRadius, stageCursor + 1); + changed = true; + continue; + } + + switch (stage) { + case 2 -> { + boolean done = TerraformConversion.hydrateColumn(level, x, z, waterTableY(), + this::drawHydration); + if (!done) { + // Out of glacite mid-column: stall without advancing — the GUI says why. + this.hydrationStalled = true; + return changed; + } + } + case 3 -> TerraformConversion.vivifyColumn(level, x, z, biomeChanged); + default -> TerraformConversion.convertColumn(level, x, z, this.tier, biomeChanged); + } + this.energy.consume(cost); + setStageFrontier(stage, stageRadius, stageCursor + 1); + changed = true; + } + return changed; + } + + /** {@link TerraformConversion.HydrationSink} for the live frontier: pays from the buffer. */ + private int drawHydration(int units) { + int granted = Math.min(units, this.hydration); + if (granted > 0) { + this.hydration -= granted; + setChanged(); + } + return granted; + } + + private void setStageFrontier(int stage, int newRadius, int newCursor) { + switch (stage) { + case 2 -> { + this.hydrationRadius = newRadius; + this.hydrationCursor = newCursor; + } + case 3 -> { + this.lifeRadius = newRadius; + this.lifeCursor = newCursor; + } + default -> { + this.radius = newRadius; + this.cursor = newCursor; + } + } + } + + /** The cached ring offsets for {@code stage} at {@code r} (cache invalidates on radius change). */ + private List ring(int stage, int r) { + int slot = stage - 1; + if (this.rings[slot] != null && this.ringsFor[slot] == r) { + return this.rings[slot]; + } + List out = ringOffsets(r); + this.rings[slot] = out; + this.ringsFor[slot] = r; + return out; + } + + /** Column offsets of the circular shell {@code [r, r+1)} (the original frontier geometry). */ + static List ringOffsets(int r) { + List out = new ArrayList<>(); + if (r == 0) { + out.add(new int[] {0, 0}); + } else { + long inner = (long) r * r; + long outer = (long) (r + 1) * (r + 1); + for (int dx = -r - 1; dx <= r + 1; dx++) { + for (int dz = -r - 1; dz <= r + 1; dz++) { + long d = (long) dx * dx + (long) dz * dz; + if (d >= inner && d < outer) { + out.add(new int[] {dx, dz}); + } + } + } + } + return out; + } + + @Override + public void setRemoved() { + if (this.level instanceof ServerLevel serverLevel) { + TerraformManager.get(serverLevel).remove(this.worldPosition); + } + super.setRemoved(); + } + + // --- Persistence -------------------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Energy", this.energy.getRaw()); + output.putInt("Tier", this.tier); + output.putInt("Radius", this.radius); + output.putInt("Cursor", this.cursor); + output.putInt("HydrationRadius", this.hydrationRadius); + output.putInt("HydrationCursor", this.hydrationCursor); + output.putInt("LifeRadius", this.lifeRadius); + output.putInt("LifeCursor", this.lifeCursor); + output.putInt("Hydration", this.hydration); + output.store("Upgrade", ItemStack.OPTIONAL_CODEC, this.items.get(UPGRADE_SLOT)); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.energy.setRaw(input.getIntOr("Energy", 0)); + this.tier = Math.max(1, input.getIntOr("Tier", 1)); + this.radius = input.getIntOr("Radius", 0); + this.cursor = input.getIntOr("Cursor", 0); + this.hydrationRadius = input.getIntOr("HydrationRadius", 0); + this.hydrationCursor = input.getIntOr("HydrationCursor", 0); + this.lifeRadius = input.getIntOr("LifeRadius", 0); + this.lifeCursor = input.getIntOr("LifeCursor", 0); + this.hydration = input.getIntOr("Hydration", 0); + this.ringsFor[0] = this.ringsFor[1] = this.ringsFor[2] = -1; + this.items.set(UPGRADE_SLOT, input.read("Upgrade", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + + // --- MenuProvider ------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.terraformer"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new TerraformerMenu(containerId, playerInventory, this, this.dataAccess); + } + + // --- WorldlyContainer: a single tier-upgrade slot ----------------------- + + private static boolean slotAccepts(ItemStack stack) { + return stack.is(ModItems.NEROSTEEL_INGOT.get()) || stack.is(ModItems.CINDRITE.get()); + } + + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return slotAccepts(stack); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return false; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return slotAccepts(stack); + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.get(UPGRADE_SLOT).isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + if (this.level == null || this.level.getBlockEntity(this.worldPosition) != this) { + return false; + } + return player.distanceToSqr(this.worldPosition.getX() + 0.5, + this.worldPosition.getY() + 0.5, this.worldPosition.getZ() + 0.5) <= 64.0; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformerMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformerMenu.java new file mode 100644 index 0000000..3dd8944 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformerMenu.java @@ -0,0 +1,143 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.machine.TerraformerBlockEntity; +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * Menu for the Terraformer (grid-only): a tier-upgrade slot plus the synced stage/energy data. Power + * arrives exclusively through the Universal Pipe network. Non-extended (entity ref stays server-side; + * the client reads the synced {@link ContainerData}). + */ +public class TerraformerMenu extends AbstractContainerMenu { + + private static final int UPGRADE_SLOT = 0; + private static final int PLAYER_INV_START = 1; + private static final int PLAYER_INV_END = PLAYER_INV_START + 36; + + private final Container container; + private final ContainerData data; + + public TerraformerMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, new SimpleContainer(TerraformerBlockEntity.SIZE), + new SimpleContainerData(TerraformerBlockEntity.DATA_COUNT)); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public TerraformerMenu(int containerId, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.TERRAFORMER.get(), containerId); + checkContainerSize(container, TerraformerBlockEntity.SIZE); + checkContainerDataCount(data, TerraformerBlockEntity.DATA_COUNT); + this.container = container; + this.data = data; + + this.addSlot(new UpgradeSlot(container, TerraformerBlockEntity.UPGRADE_SLOT, 80, 46)); + this.addStandardInventorySlots(playerInventory, 8, 84); + this.addDataSlots(data); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack moved = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack raw = slot.getItem(); + moved = raw.copy(); + if (index == UPGRADE_SLOT) { + if (!this.moveItemStackTo(raw, PLAYER_INV_START, PLAYER_INV_END, true)) { + return ItemStack.EMPTY; + } + } else if (raw.is(ModItems.NEROSTEEL_INGOT.get()) || raw.is(ModItems.CINDRITE.get())) { + if (!this.moveItemStackTo(raw, UPGRADE_SLOT, UPGRADE_SLOT + 1, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (raw.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + if (raw.getCount() == moved.getCount()) { + return ItemStack.EMPTY; + } + slot.onTake(player, raw); + } + return moved; + } + + // --- Screen helpers ----------------------------------------------------- + + public int getEnergy() { + return this.data.get(0); + } + + public int getMaxEnergy() { + return this.data.get(1); + } + + public int getTier() { + return this.data.get(2); + } + + public int getRadius() { + return this.data.get(3); + } + + public int getHydration() { + return this.data.get(4); + } + + public int getHydrationCap() { + return this.data.get(5); + } + + public int getHydrationRadius() { + return this.data.get(6); + } + + public int getLifeRadius() { + return this.data.get(7); + } + + public boolean isHydrationStalled() { + return this.data.get(8) != 0; + } + + public boolean isActive() { + return getEnergy() > 0; + } + + private static class UpgradeSlot extends Slot { + UpgradeSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModItems.NEROSTEEL_INGOT.get()) || stack.is(ModItems.CINDRITE.get()); + } + + @Override + public int getMaxStackSize() { + return 1; + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index a963b21..5fe69a6 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -14,6 +14,7 @@ import za.co.neroland.nerospace.machine.OxygenGeneratorBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; +import za.co.neroland.nerospace.machine.TerraformerBlockEntity; import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; @@ -116,6 +117,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("meteor_core", key -> new BlockEntityType<>(MeteorCoreBlockEntity::new, java.util.Set.of(ModBlocks.METEOR_CORE.get()))); + public static final RegistryEntry> TERRAFORMER = + BLOCK_ENTITIES.register("terraformer", + key -> new BlockEntityType<>(TerraformerBlockEntity::new, java.util.Set.of(ModBlocks.TERRAFORMER.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 00186e0..5c8e158 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -20,6 +20,7 @@ import za.co.neroland.nerospace.machine.OxygenGeneratorBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; import za.co.neroland.nerospace.machine.SolarPanelBlock; +import za.co.neroland.nerospace.machine.TerraformerBlock; import za.co.neroland.nerospace.machine.quarry.MinerTier; import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlock; import za.co.neroland.nerospace.machine.quarry.QuarryFrameBlock; @@ -198,6 +199,11 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.METAL).strength(3.5F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL))); + public static final RegistryEntry TERRAFORMER = BLOCKS.register("terraformer", + key -> new TerraformerBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_GREEN).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 6).sound(SoundType.METAL))); + public static final RegistryEntry SOLAR_PANEL = BLOCKS.register("solar_panel", key -> new SolarPanelBlock(BlockBehaviour.Properties.of() .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index a66a841..32f6840 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -86,6 +86,7 @@ public final class ModItems { public static final RegistryEntry CREATIVE_ITEM_STORE_ITEM = blockItem("creative_item_store", ModBlocks.CREATIVE_ITEM_STORE); public static final RegistryEntry GAS_TANK_ITEM = blockItem("gas_tank", ModBlocks.GAS_TANK); public static final RegistryEntry OXYGEN_GENERATOR_ITEM = blockItem("oxygen_generator", ModBlocks.OXYGEN_GENERATOR); + public static final RegistryEntry TERRAFORMER_ITEM = blockItem("terraformer", ModBlocks.TERRAFORMER); public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); public static final RegistryEntry ROCKET_LAUNCH_PAD_ITEM = blockItem("rocket_launch_pad", ModBlocks.ROCKET_LAUNCH_PAD); public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); @@ -276,7 +277,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), SPEED_MODULE.get(), EFFICIENCY_MODULE.get(), FORTUNE_MODULE.get(), SILK_TOUCH_MODULE.get(), CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get())); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index d5a8ab8..e0b980d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -11,6 +11,7 @@ import za.co.neroland.nerospace.menu.FuelTankMenu; import za.co.neroland.nerospace.machine.quarry.QuarryMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; +import za.co.neroland.nerospace.menu.TerraformerMenu; import za.co.neroland.nerospace.rocket.RocketMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -48,6 +49,10 @@ public final class ModMenuTypes { MENUS.register("quarry_controller", key -> new MenuType<>(QuarryMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> TERRAFORMER = + MENUS.register("terraformer", + key -> new MenuType<>(TerraformerMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/terraformer.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/terraformer.json new file mode 100644 index 0000000..186b879 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/terraformer.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "nerospace:block/terraformer", + "y": 90 + }, + "facing=north": { + "model": "nerospace:block/terraformer" + }, + "facing=south": { + "model": "nerospace:block/terraformer", + "y": 180 + }, + "facing=west": { + "model": "nerospace:block/terraformer", + "y": 270 + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/terraformer.json b/multiloader/common/src/main/resources/assets/nerospace/items/terraformer.json new file mode 100644 index 0000000..7da2d6f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/terraformer.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/terraformer" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 86afec1..3f887d0 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -51,6 +51,7 @@ "block.nerospace.solar_panel": "Solar Panel", "block.nerospace.station_floor": "Station Floor", "block.nerospace.station_wall": "Station Wall", + "block.nerospace.terraformer": "Terraformer", "block.nerospace.trash_can": "Trash Can", "block.nerospace.universal_pipe": "Universal Pipe", "block.nerospace.village_core": "Village Core", @@ -63,6 +64,7 @@ "container.nerospace.passive_generator": "Passive Generator", "container.nerospace.quarry_controller": "Quarry Controller", "container.nerospace.rocket": "Rocket", + "container.nerospace.terraformer": "Terraformer", "entity.nerospace.alien_villager": "Alien Villager", "entity.nerospace.cinder_stalker": "Cinder Stalker", "entity.nerospace.ember_strutter": "Ember Strutter", @@ -85,6 +87,13 @@ "gui.nerospace.quarry.state.mining": "Mining", "gui.nerospace.quarry.state.paused": "Paused", "gui.nerospace.rocket.launch": "Launch", + "gui.nerospace.terraformer.hydration": "Water: %s", + "gui.nerospace.terraformer.idle": "Idle", + "gui.nerospace.terraformer.needs_glacite": "Needs glacite", + "gui.nerospace.terraformer.power": "Power: %s%%", + "gui.nerospace.terraformer.stages": "Radii: %s / %s / %s", + "gui.nerospace.terraformer.tier": "Tier %s", + "gui.nerospace.terraformer.working": "Terraforming", "item.nerospace.alien_core": "Alien Core", "item.nerospace.alien_fragment": "Alien Fragment", "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/terraformer.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/terraformer.json new file mode 100644 index 0000000..30c1717 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/terraformer.json @@ -0,0 +1,74 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 0, + 0, + 0 + ], + "to": [ + 16, + 4, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#front" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 1, + 4, + 1 + ], + "to": [ + 15, + 16, + 15 + ] + } + ], + "textures": { + "front": "nerospace:block/terraformer_front", + "particle": "nerospace:block/terraformer", + "side": "nerospace:block/terraformer", + "top": "nerospace:block/terraformer_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer.png new file mode 100644 index 0000000000000000000000000000000000000000..9d9a847feb3ea0dc77a1909903beafa4a1598b94 GIT binary patch literal 529 zcmV+s0`C2ZP)%L^bjqI)A(M!6hSIT!h(m`C4kF?d zf`dZ|rGySrB!gT+xV%2X<8qtfEjrY9lb}Pzvz*Jh_k8#Ke&^o150^jrk0=4WK6*tE zMwm1q){3anL{BGFgopHe^mIZHMog|Qs0DRmtq?+@etf5plb$?%hV*=dkeDOlQ&0>EIl99nQ_qG2VgK7Ll0uD0QmU+1R*5zL@xo-^8r}c6xErr z*WPA#W9Rm=4TJuetf!s4e1ZZ%MR;q*tTh9>8$0Z^x1Bs|4N;@H1mwR<&v)d%MG!_T zY>E)l)e5T*Oqw{&!<#c#AKsiX7>$`+T@ZwkGj+|xTt7h8(+v7!XybrcPrIM%GwZY==|o6@LCrq_n>jyW!>^)38Mb68!j zu>{;2cfW}$d&EVB%_O#H&~u#^~$kKWe`KhNcR4 T5(qR}00000NkvXXu0mjf0+I9E literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_front.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_front.png new file mode 100644 index 0000000000000000000000000000000000000000..d89e1dcb363c515b4e00e147ae186ced02d4cd87 GIT binary patch literal 428 zcmV;d0aN~oP) zF-yZh7=|B9=@3K6P)Y^~9fEXl2px)plQ{V^@*^CJzd)QFia){0Nt_&{LkZHMNCpX| z91==6&JJC4h{*9>$$emw8&tqf#Y;JTi9 zd>E#dNZ)$C;lT5o0Q64I5lOlgqI;2~bdGu^vpQ56=sRp5k|!y_AhtGYao9ezW_6u` zEDPfVAWv+uaX8Jayy^gAnz8IVl+3=z4gv87AZG?RG;r_+%@rVjdxd1<|IQLr4!;1# W8>4xsq{q7e0000gfP;aK1OS*WH!w|y*=j3Y zRjwUxuHPSpog?A$^&Uw~QrNHOqiG~D;c)jN>;#~!40bn9;=XA*OqUzS$g(ofnHued zXLn%Jl9)uh9V*xT5)ESqhD~H6ZHHB^``iG^r>)=dK!BRg8Qr-LyvB}+woV4SdV7%V z0_WaP6I>{F4Y4=f%=>9dn0f^?&1~bbbnE(I)07*qoM6N<$ Ef^%N9lK=n! literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraformer.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraformer.png new file mode 100644 index 0000000000000000000000000000000000000000..35e56fcb9f08565ec6e6b295de7a6a81d40d5a27 GIT binary patch literal 5751 zcmdT|`#TeE*x#IzoO7H|sf0*8Y_ zqRAXS?tow0c<=5u| zJnhkrB_Cs1rs@$z;TSj<7(wuDwVWDQ?pjlavmjg@8#8Qoa+AjW` z`TXR#gsKte3%rf%!W%8NyPdHAX^VL$VRN8laNX!*FrjX;d3U&E5EybT>J?u)f|2_#a(X5b>t(xjsx%TOh z?LfV5=4nF_4Wqri_1u)QP}Uu{vXG$3@n1eS;|IZ?Vy2|l>pC06zU z-&&=vtB%*5Sa9m5RwRi=b#AY7SDJDo$xZw9FT|oMAfH)Hcyx^Wq!=mlx@*%o^dO4d zWS6c7if0LS>qR!2!?`$BR2pEN<6SJ7%xD&ElDx*;av5=8ZL5i^1ut^q&XT z;}(sv`5Dw`edrX@72>e|?2UP`%K>&x$FAkV*%gG5weP?CzNq5pwE9qT)3}u3n&At; z{aJ`+BCGo}TRitk16-@BORWu$JW zibQnRrZCI$2xgE@=(HqtC%8rQDe^ll2U2A~@JYsPM&L=)}L0F_;geIXR2Ci%FVV_3T_Nrb6RxaZzu=eWuf@5Yu(U}bRDn7JM@rY>7YSw z)Whn;az;Q=DfR89n;rhOt-7N`WTq_P<6|%4DQ5<0<~8Hn2S*OcdbvD@;T!w-GSqV! zJN?ZsM~7tk>6rEb&gSCxAxLqpi-Lys&!YkHATq=KClBZ2{esH* zeDu(9&%!W|K#a>$OG?9LgCx&2!^IwiR{nv_@DHTTu=Q|uWkhB#(w2Q*zZP4lZ#4og z8OE3`vopuqqIlQE=Pwysi_6v*(4;mEbZxqtpgB{!8)_JZK15%UzdtI7Tz8@<9iM@{ z95q-{=6Cdxx+`>Lr!7axqS4S@mxJnjVn{~(ga*8Ab@FQz0#Or|0xk=|yY%68&>bYI zwE3y{1~yChfuzIb=%=Q&WkupwvgYp0RIYJjuOZ`d8QpH~-k?&Mo50cme;X!!y`c_* z9>)F_akEkxIgjWAnR}0>u~O~5mwXQ<@;ALd_M{5&4U5L{4TYRgv{PvrcpR~4t2qXB zMXUer>KEaifHkTo7c!23$F(SEPQiN2PBKlZGq22Utoyah3?h-K5;BH$&}?f!84O9h zvmXJ3W8ICStAycgE={`h#b$lY<(_ae=Cq&F*yS~g&LM#kY9&l^_g3?VnB>(2+n8bU z)zN>j$n=C^FeOm3l{*hg1RWi}tAq+qEQo*{ed77HKS0r_(cjI&iiOuc_g_Sb#iO^S zrghJz-9B7GHm$|n7=#S;D%ECEkY_|F%sYg-p<8%}nr4KeLRw@Jp`^NU0M>P8ziRS& z9APf=tIk_6ee9JP8}U=eP%HtKrx0mFP*&(j3a8v0-q{4x`AmaCm` zVSbAfb+x-5^1bePv0^p^GAKp0YE<_si8|*{HeW>gqr4i$y)KVRr9B9l$+x$zzp(MX zKC1n~BpmdltBApPZ3&qh?yICnhO5ir_MATR1crh94TLzMm35w*~Fpb51&F7<%^cf0KX z>}wlhR3dVcTv*_FYE%%6+d~7ocgKEkj3{T<7p1>>jtV+`&j4zrSKp-=(xprejyN9` zD1GyOLy+XHCP0=N7KP~H+LZxU0M1!UA+qnfLu3Hv$Et4By%g{F$>s^j1Mrg1G}6uu0-4h5a_@gJx zgm8E%0suUu!7`O??E~b#5XL~)VpTmvt$7P(Ix82@8FCfO0UZn>O%({E1iwwWDy-EG zfLWvu`2yHC95IBZF@Tl`?@*t16%TFK6S{#wWp_@AVZ3FW<=_4IHa8(C7xiH*olN{b z&gZqbOl|W`;C8ak70jewE@hFEA)!*k`zVSU73WyGV+ywMc`l=%SLK@~BRachY3P8%= zV%_0&KHAUIEWE9hh;0QlD2T((Y?Xx=&|IAb9&RV?m8 zWKtLZGosczSf0b8IG^+}7;n_nMRW8eK@i|V*L288`p`|tAzFME^p2-Q#|Wd&e*qZj zpNBosxZvk;fTOh~<-GG1p7y5}3MJ_NnZNEBulB;bd0h?(TWnj^>Eu&a@Ser_R`e3H zFO0Uvpvhm&ElxQT3f0wH(d=C;EmIk&Eye{>ViU1k(f}%4#H5Db3(Jp}baVm_T`g7v z#1cHc*8~l5dxMxJ4XjH??Pnor%cX{27kQO3Tuys>UJuU@GV@UC44wL+7XVzAV5sE| z&%fyBdC19-5QnM2$`2|2=0B;8g(#83+|0{LNmP_@ABON@fak{}HHz!%Cmv-u5aA^R zSbm?2`Cr1lOR7(q;dfSG%)7p5XuH$$`S!^RmL%)LZKFVeOJZg%5p>vVZ67_PfdQ~6 zafe9R$;pHE493Ae zHDQxAByYa`?X>&Dp%Uktr^3H>T@yu;8~Nl}-A${4x64#BvA0C_uiE)+#y@-?>L=$X zyS}~o@Au>@x4wTg`1p8R=UOIHGw^1gExXEd@ABJHwO;c+`@UPt1Iv|_MT9tCj*e!& zv1XJ5uC-{68oJpaY-zH8LV6+3SX89qr9dzV61sZ-7fX3aK%(sn_Op862SqTtUZP#W zi~OQmzb#%l9sl5(;^g)CyLVz|1s$%vY9>;oShoSH z!)DJP4*Mcwr0pOup*4o3=6PMOtaZ69_z_zQT;*UK8}>jCicq$ps<{W}yBL)fGo_m2 z@|WA0gVYIi=#r2K00x|y?8^)Qpra;+l|%rqJIcznQ_ld*Hf!oTcqo&~C~1ml=ArhHzGUan zuP`B6ymxsq!?d#|jCsS}+(d*|1^~2a5aZ{5O>bK6V%fpi$6zoD2%k z`g-cdJF*$Q!=}75vzvRBf-KHW9O@LKsPA~51Y=OegBU`(l7BqsBhKN;;}Ah+!-D6@ zY+E`2p`hrQzpOaF$-Ec89lL8C16Q6%glKhI z7WB5OQqC^A=Vjz4YV-6L9mA}K+10!8RAFK&3)$4Uc%fV59|F!m7WXD_J~Wt5%9vGt zK$Kkl08*JUq+1rO#(dT429Uvf@R3d%>BE9>Y;`0KSe0@|;Ry561>X8A?mqk>NP4J8 zY5Aa>J@0KY5ws*45LMT0axJr6lRP;q6oS+{vi6|Z+r+S%Qb~3chXLocclhqfi^I%o zM3`ib>kISe@e4`bCeDH_#Mjc824P*^OWjGsQ}&oQDw@107crgFSR~+rd4MAehs+(6 zs}@)`CXP$WBsx}Jv_c3}4IN||c8d3>D%ks{r}|%dTQrrnIxj=IuvOQw_qYi5pKt zFw&1j9hf)=#?ev6eosavKR!h7m-i{k?I|!_9TNn+uc2MVhgj@;{tScY@dXftLIV}; zO_`4NKs{kT4YO_>l_))`1H{-K&rOhfHUE#RSoLZ<5O#8_oRlJ?ojWU?eOAUfvsENJ z5vjlZ7l7?I*@)4`NZ;DTeFTKbvji-TWB4K61&110j0O-wCqO?s^M(rE_p2ZQ9Ak>j z;j3?-^*bcX863vbV<2`Epk?=rTLt*$Lk3+Cx(ZGl{y$V?v7(m$LEiRRwe2juN!a_I zd#5ge{C8r1#l$K(jpJ!+FF)szA|%}6`dL?McgTzy2{)P?Ixk-?alQivzrP>U=@qjX zHEvnC`yWa&$}xXcGgT@Jr*CH-O|@gsd+$l(XHXk{Dxo2)n95oXlM z?LI>i+R4*5&+p#__WFN!CjXY8xpS&3)V9A%MepxUeLakuF-3AjsXBT+gB43!=0=Zb zIF1`Y%{ml(xj4S0DiPJ`lasNGX0}zbO*(i&+7lL`q+^LvS)F{sVK^WdI`ACUc`Cs^ zuWCBA>6Og)usO3ngV(J^r~&NKBAv96Rcc|NL9UFvONX zs{FGVj6Qg;`6YfNchaUxgrqCf(y}w!!<8xWyO$m=21*tDK){|*4CpB<9to}@SdycC zRu*1X+ZMCXV?Laoc3MnO7b{`DTB{7X zU&eeDf_s_26jig${Bcl*Ye63Db{n5L^-MrJgCs;_=yfB$8eEu;%bq5`?z&%xdS-8$ z@j^!D7_8mEI513U5$r+H*Qm14)qe^(KL5PtNSTMHsS~fq2F~_@pd{?BkUWepwT>-+ zB58+nfg)tFH{|W6hr7r3EW*X!wcTbx%bYaWa0Y17oeCZ3r%HJ!Z7U>J-ICxV3dNf| z`3PXk1qld^1_1E|8cy%{vuzv10EcZuM><{D{_yBXP&%(HQKoaizZ5Vi*Uxk7Pli^k)o=B)=AmTUUU|OVRuK*Ns;?HRF9Cz?YvU;^<#%CyKJeRmR{2+Bxy3SHa#CFIpYi6x?R>+!r*24|!#tC1dP9Rx^3qs`Q-m%jZT$%hQW* z(cAG=Cj{ucrt5CT8JeOBsS@m+YW?`3sNdUy#_1u`&5nP!8uIl^ zZiXr;AQtNpicv8~K0-4H)cuoBuZ|0E6xf9OC8!3dGpw&EPXITDa$|v!8O=)%hX+f& z7k#b?)N~hCpp1t2QM0Cb#F&Q0$P#2^psCxFCK{{b2;AeMSuJqKS3HSQQjP0>;P5HS z1sN!p#T=WhWVrg`U6z$nqu)eOThzk^;1ktXWI;*zuHg-D|ECa?1h%$3pY5Zr`4t)} z3tM;fuZxtRA{+e2opNrU)pmTaV}aPaFSbI$g6YPsq{aiu@~FJe(z|}H&4|b o4nZ^8xU=-d!-?$wyM@_0ND0(5W%j2X``_5DuiINxn|UPt51?`U-T(jq literal 0 HcmV?d00001 diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 68bdbbf..cb86391 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -154,6 +154,14 @@ public void register(EntityType type, SpawnPlacementType plac (be, direction) -> be.getEnergy(), ModBlockEntities.SOLAR_PANEL.get()); + // Terraformer: grid power in, upgrade slot in. + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.TERRAFORMER.get()); + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.TERRAFORMER.get()); + ItemStorage.SIDED.registerForBlockEntity( (be, direction) -> ContainerStorage.of(be, direction), ModBlockEntities.TRASH_CAN.get()); diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 15ada54..8db8cae 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -18,6 +18,7 @@ import za.co.neroland.nerospace.client.PassiveGeneratorScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; +import za.co.neroland.nerospace.client.TerraformerScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; /** Fabric client entry point — screen + entity-renderer registration. */ @@ -34,6 +35,7 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); MenuScreens.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); MenuScreens.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); + MenuScreens.register(ModMenuTypes.TERRAFORMER.get(), TerraformerScreen::new); ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { @Override diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index be9567a..712e949 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -136,6 +136,18 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ModBlockEntities.SOLAR_PANEL.get(), (be, side) -> be.getEnergy()); + // Terraformer: grid power in, upgrade slot in. + event.registerBlockEntity( + ENERGY, + ModBlockEntities.TERRAFORMER.get(), + (be, side) -> be.getEnergy()); + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.TERRAFORMER.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + event.registerBlockEntity( Capabilities.Item.BLOCK, ModBlockEntities.TRASH_CAN.get(), diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 39af98f..d01f64f 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -24,6 +24,7 @@ import za.co.neroland.nerospace.client.PassiveGeneratorScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; +import za.co.neroland.nerospace.client.TerraformerScreen; import za.co.neroland.nerospace.fluid.ModFluids; import za.co.neroland.nerospace.registry.ModMenuTypes; @@ -58,6 +59,7 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); event.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); event.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); + event.register(ModMenuTypes.TERRAFORMER.get(), TerraformerScreen::new); } /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ From 417a5ddce54ffecc8182a97dfbdaa5066f39929f Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:45:51 +0200 Subject: [PATCH 52/82] Add Hydration Module machine and assets Introduce the Hydration Module feature: adds HydrationModuleBlock, HydrationModuleBlockEntity, HydrationModuleMenu and HydrationModuleScreen plus block/item model, textures, blockstate, GUI texture and loot table. Registers the block, block entity, item and menu types and the hydration_input item tag; wires item storage/capabilities for Fabric and NeoForge. The BE is rebuilt on a vanilla WorldlyContainer/NonNullList single input slot that melts glacite items into a touching Terraformer's hydration buffer (no energy buffer of its own), with synced menu data for link state and hydration values. Also adds a missing loot table for the Terraformer and updates docs/lang entries and the port checklist to mark slice 5 done. --- docs/MULTILOADER_PORT_CHECKLIST.md | 16 +- .../client/HydrationModuleScreen.java | 42 +++ .../machine/HydrationModuleBlock.java | 85 ++++++ .../machine/HydrationModuleBlockEntity.java | 242 ++++++++++++++++++ .../nerospace/menu/HydrationModuleMenu.java | 109 ++++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 6 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../blockstates/hydration_module.json | 19 ++ .../nerospace/items/hydration_module.json | 6 + .../assets/nerospace/lang/en_us.json | 5 + .../models/block/hydration_module.json | 106 ++++++++ .../textures/block/hydration_module.png | Bin 0 -> 490 bytes .../textures/block/hydration_module_front.png | Bin 0 -> 388 bytes .../textures/block/hydration_module_top.png | Bin 0 -> 494 bytes .../textures/gui/hydration_module.png | Bin 0 -> 5767 bytes .../loot_table/blocks/hydration_module.json | 21 ++ .../loot_table/blocks/terraformer.json | 21 ++ .../nerospace/tags/item/hydration_input.json | 6 + .../nerospace/fabric/NerospaceFabric.java | 5 + .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeCapabilities.java | 8 + .../neoforge/NeoForgeClientSetup.java | 2 + 24 files changed, 711 insertions(+), 3 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/HydrationModuleScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/HydrationModuleMenu.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/hydration_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/hydration_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/hydration_module.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_front.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_top.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/hydration_module.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/hydration_module.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraformer.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/tags/item/hydration_input.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 360f4a5..9f1e4f9 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~178 classes ported, ~86 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~182 classes ported, ~82 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 5: Hydration Module.** All 4 cells green. Added +> `machine/{HydrationModuleBlock, HydrationModuleBlockEntity}` + `menu/HydrationModuleMenu` + +> `client/HydrationModuleScreen` (BE on `WorldlyContainer`/`NonNullList`, melts glacite into a touching +> Terraformer's water-stage buffer). Registered + item cap + assets + `hydration_input` tag + loot table + lang. +> Also fixed a 4b omission: the Terraformer block had no loot table (added — it would have dropped nothing). +> Remaining terraform: slice 6 = Monitor + Drift + ChunkLoader + GreenxertzAtmosphere (all secondary). + > **2026-06-21 update — terraforming slice 4b: the Terraformer machine.** All 4 cells green. Added > `machine/{MachineRedstone, TerraformerBlock, TerraformerBlockEntity}` + `menu/TerraformerMenu` + > `client/TerraformerScreen`; the 584-line BE rewritten onto `EnergyBuffer` + a `WorldlyContainer` upgrade slot @@ -213,7 +220,12 @@ checked by a headless build). `TerraformManager.update` + biome-sync packet. Registered block/item/BE/menu + per-loader screen + energy/item caps; copied block (3 textures, FACING blockstate, multi-tex model) + GUI texture + 9 lang keys. **Placing a Terraformer now greens the planet outward (Rooted→Hydrated→Living).** - - [ ] **Slice 5 — Hydration Module** (block/BE/menu/screen) + stage-2 wiring. + - [x] **Slice 5 — Hydration Module DONE (4 cells green).** `machine/{HydrationModuleBlock, + HydrationModuleBlockEntity}` + `menu/HydrationModuleMenu` + `client/HydrationModuleScreen`. Melts glacite + (the `hydration_input` tag) from a `WorldlyContainer`/`NonNullList` slot into a TOUCHING Terraformer's + hydration buffer (`acceptHydration`); no energy of its own. Registered block/item/BE/menu + per-loader + screen + item cap; copied block (3 tex, FACING blockstate, model) + GUI + loot table + `hydration_input` + tag JSON + 5 lang keys. **Also fixed: the Terraformer block was missing its loot table from 4b (added).** - [ ] **Slice 6 — Terraform Monitor** (block/BE/menu/screen) + `TerraformDrift` + `TerraformChunkLoader` + `GreenxertzAtmosphere`. Risk: **high** (world mutation, chunk-loading, events). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/HydrationModuleScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/HydrationModuleScreen.java new file mode 100644 index 0000000..2e059ef --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/HydrationModuleScreen.java @@ -0,0 +1,42 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.menu.HydrationModuleMenu; + +/** + * Screen for the Hydration Module (DEEPER_TERRAFORM_DESIGN.md §3.1): the glacite input slot, the + * linked Terraformer's hydration-unit gauge and the link status, themed glacite cyan. + */ +public class HydrationModuleScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/hydration_module.png"); + private static final int ACCENT = 0xFF78D2F0; // glacite cyan (water stage) + + public HydrationModuleScreen(HydrationModuleMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 166); + this.titleLabelX = 10; + this.inventoryLabelX = 10; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int hydration = this.menu.getHydration(); + int cap = this.menu.getHydrationCap(); + float frac = cap == 0 ? 0f : (float) hydration / cap; + + label(g, Component.translatable("gui.nerospace.hydration_module.buffer", hydration, cap), + 8, 20, 0xFFCFE7FF); + fluidGauge(g, 8, 31, 160, 6, frac, ACCENT); + + boolean linked = this.menu.isLinked(); + label(g, Component.translatable(linked + ? "gui.nerospace.hydration_module.linked" + : "gui.nerospace.hydration_module.no_link"), 8, 48, linked ? 0xFF9CF0C0 : 0xFFFF6A5E); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlock.java new file mode 100644 index 0000000..b8aef5d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlock.java @@ -0,0 +1,85 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The Hydration Module block (DEEPER_TERRAFORM_DESIGN.md §3.1): a ticking machine backed by + * {@link HydrationModuleBlockEntity} that melts glacite into hydration units for a TOUCHING + * Terraformer's water stage. + */ +public class HydrationModuleBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(HydrationModuleBlock::new); + /** The melt window faces the placer. Visual only. */ + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public HydrationModuleBlock(Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new HydrationModuleBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.HYDRATION_MODULE.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof HydrationModuleBlockEntity be) { + serverPlayer.openMenu(be); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java new file mode 100644 index 0000000..ac28733 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java @@ -0,0 +1,242 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.menu.HydrationModuleMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.registry.ModTags; + +/** + * Hydration Module (DEEPER_TERRAFORM_DESIGN.md §3.1): the glacite intake of the water stage. It must + * TOUCH a Terraformer; each work pulse it melts one item from its input slot ({@code + * nerospace:hydration_input} — glacite by default) into the Terraformer's hydration-unit buffer. No + * energy buffer of its own — melting is part of the Terraformer's stage-2 column cost. + * + *

Cross-loader port note: rebuilt on a vanilla {@link WorldlyContainer} + {@link NonNullList} input + * slot (the root used the NeoForge transfer API + {@code MachineItemHandler}); Tuning values inlined.

+ */ +public class HydrationModuleBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { + + public static final int INPUT_SLOT = 0; + public static final int SIZE = 1; + public static final int DATA_COUNT = 3; + + // --- Inlined Tuning base values (config seam deferred) --- + private static final int HYDRATION_PER_GLACITE = 16; + private static final int HYDRATION_PER_GLACITE_BLOCK = 9 * HYDRATION_PER_GLACITE; + private static final int HYDRATION_CAP = 1_024; + /** Ticks between melt pulses (cheap; one item per pulse). */ + private static final int WORK_INTERVAL_TICKS = 10; + + private static final int[] SLOTS = {INPUT_SLOT}; + + private final NonNullList items = NonNullList.withSize(SIZE, ItemStack.EMPTY); + + /** Transient link state for the GUI (recomputed each work pulse). */ + private transient boolean linked; + private transient int linkedHydration; + + /** Synced to the menu: [0]=linked [1]=linked terraformer's hydration [2]=hydration cap. */ + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> linked ? 1 : 0; + case 1 -> linkedHydration; + case 2 -> HYDRATION_CAP; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + switch (index) { + case 0 -> linked = value != 0; + case 1 -> linkedHydration = value; + default -> { } + } + } + + @Override + public int getCount() { + return DATA_COUNT; + } + }; + + public HydrationModuleBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.HYDRATION_MODULE.get(), pos, state); + } + + public ContainerData getDataAccess() { + return this.dataAccess; + } + + /** Hydration units one input item melts into (tag-driven; unknown tag members melt as glacite). */ + public static int hydrationUnits(ItemStack stack) { + if (stack.is(ModItems.GLACITE_BLOCK_ITEM.get())) { + return HYDRATION_PER_GLACITE_BLOCK; + } + return HYDRATION_PER_GLACITE; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (!(level instanceof ServerLevel serverLevel) + || serverLevel.getGameTime() % WORK_INTERVAL_TICKS != 0) { + return; + } + meltPulse(serverLevel, pos); + } + + /** One melt pulse (public so the gametests can drive it without waiting on the interval). */ + public void meltPulse(ServerLevel serverLevel, BlockPos pos) { + TerraformerBlockEntity terraformer = findAdjacentTerraformer(serverLevel, pos); + this.linked = terraformer != null; + this.linkedHydration = terraformer == null ? 0 : terraformer.getHydration(); + if (terraformer == null) { + return; + } + + ItemStack input = this.items.get(INPUT_SLOT); + if (input.isEmpty()) { + return; + } + int units = hydrationUnits(input); + // Only melt when the buffer can take the whole item's yield — units must never be lost. + if (terraformer.getHydration() + units > HYDRATION_CAP) { + return; + } + input.shrink(1); + terraformer.acceptHydration(units); + this.linkedHydration = terraformer.getHydration(); + setChanged(); + } + + /** The Terraformer this module feeds: the first one TOUCHING any of the six faces. */ + @Nullable + private static TerraformerBlockEntity findAdjacentTerraformer(ServerLevel level, BlockPos pos) { + for (Direction direction : Direction.values()) { + if (level.getBlockEntity(pos.relative(direction)) instanceof TerraformerBlockEntity be) { + return be; + } + } + return null; + } + + // --- Persistence -------------------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.store("Input", ItemStack.OPTIONAL_CODEC, this.items.get(INPUT_SLOT)); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.items.set(INPUT_SLOT, input.read("Input", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); + } + + // --- MenuProvider ------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.hydration_module"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new HydrationModuleMenu(containerId, playerInventory, this, this.dataAccess); + } + + // --- WorldlyContainer: a single glacite input slot ---------------------- + + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; + } + + @Override + public boolean canPlaceItemThroughFace(int slot, ItemStack stack, @Nullable Direction side) { + return stack.is(ModTags.Items.HYDRATION_INPUT); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return false; + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + return stack.is(ModTags.Items.HYDRATION_INPUT); + } + + @Override + public int getContainerSize() { + return SIZE; + } + + @Override + public boolean isEmpty() { + return this.items.get(INPUT_SLOT).isEmpty(); + } + + @Override + public ItemStack getItem(int slot) { + return this.items.get(slot); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(this.items, slot, amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(this.items, slot); + } + + @Override + public void setItem(int slot, ItemStack stack) { + this.items.set(slot, stack); + this.setChanged(); + } + + @Override + public boolean stillValid(Player player) { + if (this.level == null || this.level.getBlockEntity(this.worldPosition) != this) { + return false; + } + return player.distanceToSqr(this.worldPosition.getX() + 0.5, + this.worldPosition.getY() + 0.5, this.worldPosition.getZ() + 0.5) <= 64.0; + } + + @Override + public void clearContent() { + this.items.clear(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/HydrationModuleMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/HydrationModuleMenu.java new file mode 100644 index 0000000..3a94127 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/HydrationModuleMenu.java @@ -0,0 +1,109 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.machine.HydrationModuleBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; +import za.co.neroland.nerospace.registry.ModTags; + +/** + * Menu for the Hydration Module (DEEPER_TERRAFORM_DESIGN.md §3.1): one glacite input slot plus three + * synced data values (link state, the linked Terraformer's hydration units, the buffer cap). + */ +public class HydrationModuleMenu extends AbstractContainerMenu { + + private static final int INPUT_SLOT = 0; + private static final int PLAYER_INV_START = 1; + private static final int PLAYER_INV_END = PLAYER_INV_START + 36; + + private final Container container; + private final ContainerData data; + + public HydrationModuleMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, new SimpleContainer(HydrationModuleBlockEntity.SIZE), + new SimpleContainerData(HydrationModuleBlockEntity.DATA_COUNT)); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public HydrationModuleMenu(int containerId, Inventory playerInventory, Container container, ContainerData data) { + super(ModMenuTypes.HYDRATION_MODULE.get(), containerId); + checkContainerSize(container, HydrationModuleBlockEntity.SIZE); + checkContainerDataCount(data, HydrationModuleBlockEntity.DATA_COUNT); + this.container = container; + this.data = data; + + this.addSlot(new InputSlot(container, HydrationModuleBlockEntity.INPUT_SLOT, 80, 46)); + this.addStandardInventorySlots(playerInventory, 8, 84); + this.addDataSlots(data); + } + + @Override + public boolean stillValid(Player player) { + return this.container.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + ItemStack moved = ItemStack.EMPTY; + Slot slot = this.slots.get(index); + if (slot != null && slot.hasItem()) { + ItemStack raw = slot.getItem(); + moved = raw.copy(); + if (index == INPUT_SLOT) { + if (!this.moveItemStackTo(raw, PLAYER_INV_START, PLAYER_INV_END, true)) { + return ItemStack.EMPTY; + } + } else if (raw.is(ModTags.Items.HYDRATION_INPUT)) { + if (!this.moveItemStackTo(raw, INPUT_SLOT, INPUT_SLOT + 1, false)) { + return ItemStack.EMPTY; + } + } else { + return ItemStack.EMPTY; + } + + if (raw.isEmpty()) { + slot.setByPlayer(ItemStack.EMPTY); + } else { + slot.setChanged(); + } + if (raw.getCount() == moved.getCount()) { + return ItemStack.EMPTY; + } + slot.onTake(player, raw); + } + return moved; + } + + // --- Screen helpers ----------------------------------------------------- + + public boolean isLinked() { + return this.data.get(0) != 0; + } + + public int getHydration() { + return this.data.get(1); + } + + public int getHydrationCap() { + return this.data.get(2); + } + + private static class InputSlot extends Slot { + InputSlot(Container container, int slot, int x, int y) { + super(container, slot, x, y); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return stack.is(ModTags.Items.HYDRATION_INPUT); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 5fe69a6..626af54 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -13,6 +13,7 @@ import za.co.neroland.nerospace.machine.quarry.QuarryLandmarkBlockEntity; import za.co.neroland.nerospace.machine.OxygenGeneratorBlockEntity; import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; +import za.co.neroland.nerospace.machine.HydrationModuleBlockEntity; import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; import za.co.neroland.nerospace.machine.TerraformerBlockEntity; import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; @@ -121,6 +122,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("terraformer", key -> new BlockEntityType<>(TerraformerBlockEntity::new, java.util.Set.of(ModBlocks.TERRAFORMER.get()))); + public static final RegistryEntry> HYDRATION_MODULE = + BLOCK_ENTITIES.register("hydration_module", + key -> new BlockEntityType<>(HydrationModuleBlockEntity::new, java.util.Set.of(ModBlocks.HYDRATION_MODULE.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 5c8e158..afa69ae 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -19,6 +19,7 @@ import za.co.neroland.nerospace.machine.NerosiumGrinderBlock; import za.co.neroland.nerospace.machine.OxygenGeneratorBlock; import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; +import za.co.neroland.nerospace.machine.HydrationModuleBlock; import za.co.neroland.nerospace.machine.SolarPanelBlock; import za.co.neroland.nerospace.machine.TerraformerBlock; import za.co.neroland.nerospace.machine.quarry.MinerTier; @@ -204,6 +205,11 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_GREEN).strength(3.5F, 6.0F) .requiresCorrectToolForDrops().lightLevel(s -> 6).sound(SoundType.METAL))); + public static final RegistryEntry HYDRATION_MODULE = BLOCKS.register("hydration_module", + key -> new HydrationModuleBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_LIGHT_BLUE).strength(3.5F, 6.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 4).sound(SoundType.METAL))); + public static final RegistryEntry SOLAR_PANEL = BLOCKS.register("solar_panel", key -> new SolarPanelBlock(BlockBehaviour.Properties.of() .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 32f6840..5e3611e 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -87,6 +87,7 @@ public final class ModItems { public static final RegistryEntry GAS_TANK_ITEM = blockItem("gas_tank", ModBlocks.GAS_TANK); public static final RegistryEntry OXYGEN_GENERATOR_ITEM = blockItem("oxygen_generator", ModBlocks.OXYGEN_GENERATOR); public static final RegistryEntry TERRAFORMER_ITEM = blockItem("terraformer", ModBlocks.TERRAFORMER); + public static final RegistryEntry HYDRATION_MODULE_ITEM = blockItem("hydration_module", ModBlocks.HYDRATION_MODULE); public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); public static final RegistryEntry ROCKET_LAUNCH_PAD_ITEM = blockItem("rocket_launch_pad", ModBlocks.ROCKET_LAUNCH_PAD); public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); @@ -277,7 +278,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), HYDRATION_MODULE_ITEM.get(), SPEED_MODULE.get(), EFFICIENCY_MODULE.get(), FORTUNE_MODULE.get(), SILK_TOUCH_MODULE.get(), CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get())); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index e0b980d..7fc7e61 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -9,6 +9,7 @@ import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; import za.co.neroland.nerospace.menu.FuelRefineryMenu; import za.co.neroland.nerospace.menu.FuelTankMenu; +import za.co.neroland.nerospace.menu.HydrationModuleMenu; import za.co.neroland.nerospace.machine.quarry.QuarryMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; import za.co.neroland.nerospace.menu.TerraformerMenu; @@ -53,6 +54,10 @@ public final class ModMenuTypes { MENUS.register("terraformer", key -> new MenuType<>(TerraformerMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> HYDRATION_MODULE = + MENUS.register("hydration_module", + key -> new MenuType<>(HydrationModuleMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/hydration_module.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/hydration_module.json new file mode 100644 index 0000000..5cfbd6a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/hydration_module.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "nerospace:block/hydration_module", + "y": 90 + }, + "facing=north": { + "model": "nerospace:block/hydration_module" + }, + "facing=south": { + "model": "nerospace:block/hydration_module", + "y": 180 + }, + "facing=west": { + "model": "nerospace:block/hydration_module", + "y": 270 + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/hydration_module.json b/multiloader/common/src/main/resources/assets/nerospace/items/hydration_module.json new file mode 100644 index 0000000..34b9014 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/hydration_module.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/hydration_module" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 3f887d0..0e5fb52 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -22,6 +22,7 @@ "block.nerospace.gas_tank": "Gas Tank", "block.nerospace.glacite_block": "Block of Glacite", "block.nerospace.glacite_ore": "Glacite Ore", + "block.nerospace.hydration_module": "Hydration Module", "block.nerospace.item_store": "Item Store", "block.nerospace.launch_gantry": "Launch Gantry", "block.nerospace.launch_gantry.boarded": "Boarded the rocket — strap in", @@ -59,6 +60,7 @@ "container.nerospace.combustion_generator": "Combustion Generator", "container.nerospace.fuel_refinery": "Fuel Refinery", "container.nerospace.fuel_tank": "Fuel Tank", + "container.nerospace.hydration_module": "Hydration Module", "container.nerospace.item_store": "Item Store", "container.nerospace.nerosium_grinder": "Nerosium Grinder", "container.nerospace.passive_generator": "Passive Generator", @@ -81,6 +83,9 @@ "fluid_type.nerospace.rocket_fuel": "Rocket Fuel", "gas.nerospace.empty": "Empty", "gas.nerospace.oxygen": "Oxygen", + "gui.nerospace.hydration_module.buffer": "Hydration: %s / %s", + "gui.nerospace.hydration_module.linked": "Feeding Terraformer", + "gui.nerospace.hydration_module.no_link": "No Terraformer touching", "gui.nerospace.quarry.state.building_frame": "Building frame", "gui.nerospace.quarry.state.done": "Finished", "gui.nerospace.quarry.state.idle": "Idle — place landmarks", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/hydration_module.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/hydration_module.json new file mode 100644 index 0000000..a586d7f --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/hydration_module.json @@ -0,0 +1,106 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 0, + 0, + 1 + ], + "to": [ + 16, + 15, + 16 + ] + }, + { + "faces": { + "down": { + "texture": "#front" + }, + "east": { + "texture": "#front" + }, + "north": { + "texture": "#front" + }, + "south": { + "texture": "#front" + }, + "up": { + "texture": "#front" + }, + "west": { + "texture": "#front" + } + }, + "from": [ + 2, + 2, + 0 + ], + "to": [ + 14, + 13, + 1 + ] + }, + { + "faces": { + "down": { + "texture": "#top" + }, + "east": { + "texture": "#top" + }, + "north": { + "texture": "#top" + }, + "south": { + "texture": "#top" + }, + "up": { + "texture": "#top" + }, + "west": { + "texture": "#top" + } + }, + "from": [ + 3, + 15, + 4 + ], + "to": [ + 13, + 16, + 12 + ] + } + ], + "textures": { + "front": "nerospace:block/hydration_module_front", + "particle": "nerospace:block/hydration_module", + "side": "nerospace:block/hydration_module", + "top": "nerospace:block/hydration_module_top" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module.png new file mode 100644 index 0000000000000000000000000000000000000000..013f4b3a5a04080e3339080f1a64075d18b83f77 GIT binary patch literal 490 zcmVDP}55gSgI1UYk(hl;STqq``55zm&yYGA7`|h56_>}(QKcWe+xR?^PV*rXx z?uv(dQ9DK}iSPsREFtash^G+IeYqsX&_$=exP$y$Li zn#IKwy9Q_subGd!0GwUjBhtBRFgu?R_XgFuL%CLRV|dNws7t>E{T5t~x_s}C0T6yb z7MvfqaK2+w6m*w(y|7m zW#xmNTPsNzb^i3g7Q?8+X*zc#794zi=Lm%#P#RU=g1uMa2iSjaGhuwA@jO8b*c#2Y zcyNvE2u<6)J$=tGe9F!CCzE|19?7%u8`i63MNn*VZpRBg_Xemec~pMBAE#+e+?`ZK zzxTLY{K#t)wUYk?_V+$G1;P(_p}rw@cNKH`g%iNsek26q-5vUwq&(PX|B#vh>(%n; g!}(i%Ckp_60~ts8b7otaApigX07*qoM6N<$f`r=LBme*a literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_front.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_front.png new file mode 100644 index 0000000000000000000000000000000000000000..e65079df6ee9e2cc07a872597ae5346727dd9aa6 GIT binary patch literal 388 zcmV-~0ek+5P)D%>JTs2Vuuso@_6@nzwhJY7S69kJB+A!W|Z!@UvUWdDF<9BBst zLOAU19UB4G9dJEgXW)81$v9$BEE@sxY=~1^(GFB7mRK?ZkWbyHH^15D%@Cl}-zSL% z(8C<0ZE?l@W;V>!m0K%0E6{>_d zggCZX#37btxH3+0-pvVtoBYi(d$TkDKmXjt!}I!$|G)s?`_~(a=`39a-vDrMHN|vB zwOOH*MkG*7XEfsB!~o*(r0q|T_av!7M%oVD~2iqNRLGVA|X+(Hmmfo+N@GMh{;#? zPb_;RhmiPt>i-d7lLn6*EqC;t50ST+12}PS3oIE@C~i_)TI>izbN^09G1w{0KcFAAp!B1)i}r~m)}07*qoM6N<$g3wCkg8%>k literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/hydration_module.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/hydration_module.png new file mode 100644 index 0000000000000000000000000000000000000000..a8234125d50babb965b0b3375bc20ca3c4eb3d7b GIT binary patch literal 5767 zcmdT|_dgrn_l~X9YKc*!r7cCx zXh5Y4001=9)l@TkQLs_indYVYI>E8!#87%vPlus6`C7Nd+W*)7kb#Om24|QzhJSM+)sQa|BWvxId4=yDlEFvwU zax_iqI0^%Vvyk_r&ZHtQJ|d&GM#)DCfm|Hh$;imlQ8M5K7#V&%{n&NUbI3gMXzRE% zF|nzsDW%szh?_fygTr5L{qGt!Xx+#69Nznmn3(u$cJAx9Z_ZGt?Kg+cmJF}Yy z6*ffvgVuw~5f*bf|48XKkVyFW=^E)hA-FR<$5~}P#KvYN@Uv2Bc*>eC(Fj?*O1yyE z;d^&$Nf3(YLFXB7B$CiagzTyu8xp?_RFl5VM^6@4Y>?&n54I<;IlYH$>3>U3Z`{9F z+VS{RDe{j_@L+iv9}?>QCUt5osmZe})IheZZIfat6bfx%t&Spg>O-y_ICM&+ibhFH z@gG$4|K_P5BR@KB%tlQhGhIW%q$LR&#@)Zxrbeff+(gGuUqJ4vZy(iO!aM%1HoQy9 z$2)Wqcit>d1dx1u`&+jHg2m=@t_=(}Rrp|fF}XQ;P+5TG>*`P2UMwpZF} zrgqVk&^4mYJ9k$LgKF~N_cH3TtY@?aOso+&9f?%QYg{pTLT3hXDBB$8UDlbprB8w7 ze%Sdx&xe&(E~;zNR^9Tf1rq&!9uFuC9P>PLEZ^-{6u8tRsiFLsSlo6Mn@~2k_H~De zFybLqUhOfG*orHgZ-pHoT6Od#?yEPg zAn@ABTC2k8ZkvUp;jK?VPOmXZ1kNeAIzs?k*BBXm_m|AVz(>O+;=s6TWfiis3b^O$ z$*0hD|2X77qdM;utfcbhQY# z(`>FSTCQ4=`-9et{AE;+eq1`jfv)c->tO zojIk?-E7k(#RVExcOMo;JYgg2KP1mWpLSQd&Eof!8>T`^y|DuI;$~8Ig>gqZKR{ll z^rmz6kW@&$o2E?GY+E1}JbDw}URvl}mL>dw7a8R#D6$69QtZfaH)FGAeSk%tNYsgV z2bE;UyR8m(4vD!XY;CuMeK+rN&$kj%E;C71JEOt6g0@zOiU&g;TFr5wosoY*_m1k` zKURKlff}Vn$i5kkUZ;blJ@79g%ULNVq|GGfoDCaEr}0A-?&%YI3R6l~wYaX_18LjB zrtm(Su6d1;xl-RP)a+w~2R<~h_3RRquSP^h*5;cdi-89C#(#Fip;MqZCzP=%YZNP) zyWDiE;LKJazD0uzZ&E=uQ};e-IB})UP|xe3qIJ(T$EnfRPD(WvTCMYSZ#|S6>6OyS zjjUJeic`UQo*ARBC&HBTH%FCZ?>GJ%!CD9rK3qbwL^`R#XI-pjFC*lbtQlk^)Z!Ky z4|~0qMRr%DMK>=Wu+GJ+c(7D0Wf&@d-m(|`#*mR}ckOL@RDEn_j`T{3LJih4=}xE@ zqk^CztiZYrJNdfbE9OYB?yCC_T$tHzxp_%XT+GTlnGC#2gYuhMw8Y1%wvOk4o ztn^g}qnK|Ch=WtBHLALQwUW(XIu5MUOE-bh3x+l!YgIB2Lof0xP-85nUY67ZQx~e1 zPbH~~$#7Rbbps6Idn`I4isXC4`K%HK>Bb z(!(`4j;LHlp8=8{|1-Rr%at+J{6n$Rw{wQmq@vuaT`q3$qcWfN$_umI8Y}6swVLL2 zLf5X<)aYhW0JAD``g@x!Yw*WDgT-~P-*?Q#TAq=<(39KQ*bHaq`%4aW;pW5d*H*9r zR~Jn)zjfG^SHYit#3npFKVJw}U{=M3O|WZuvhf9La$>KV#3fnLZ7fRJPLg&M9wOUs z5JG-8+gvAqnY~ldnGR0#I2=L8d%^|m(%YuLUD&fiHZGUCgTJ}zfQ1UT-WJE$5vae% zy$b${>wjIG6$0D%`8(UH!%JI3;AIW}0$J>Fv`FZ#@3?LRw6-?^$KT*C5>@4MjDO_8 z>qg^M=mR>(#lI!{&-bo8Be!}A8$7-s=^v}XA^6U)@qvApexmX1PG14-v#Z=8ME3Ck zl~C?)ttusaYwo(I2bPY#3;Rb0QGPC#$kc=LG{T{A4y~GFe>um$99fNlx>B$18Yba z)mP{sQ->3pE_uOI&Ym?y15hR*sULh^UC+D)PP*zhutpDG}@ z)YP!$>vZ;Pvgj0gsc3l^L#%N(Mwm~P_LfX~3-|TEo1ez70G&9qcyXm)Y4v$ryFFfT zy5?#;W8ydY`;`hE;}OX@uCz%D_$zLiYqIAihm`&0nks{4Ju$~q9ziA4l2-*yk3-E| zro{+YgNat|NF8PsFP{x{t*K`W~fzm+SfxmI9e$(EcJj>_>te2jfCj z=HE4*62nq2`W>3PR2J&#HxlGw1nQOTVSq%8h@W7Hnp!%?Q@=W;g0q*Qp(mRWr`s2i ziZ>o!%&mOd6|029OrkHboy&EQt4VDzpe-HjoKSq3>i6`9+%x+JtS16Q!24()0RY!! ziJ0f1f_h4TN$Rc-D^G$b9&h<}VRjF#GsBv43zQ|roN*=l8Nf1ieNU{i8O9vFBi*G} zM%Tq2bZ^_UGu>4!sCw2GXmTHO15cNBm~H?DUjIf8 z2L-P7#6U0vzDW@-PGA?bB@2=A#a;`>3yS;zhfyQOq8+pXO08(6&86=U0RW3zLujYQ zHK+nJqCbwygc+dHrPQxp1is*)W+SxDpDa>xs)-CBfTEmP+6_|y{25wa<{Ak<*J9_~ zgNs38wP@7=*=6-c=&>q_fUqSaE%A*>w1c3G_|~UgN2)Fl4E+LJOifMXmp2_zeXyTQ zk@_4%Wl@TK@<6jt&zI`Dje}YM<9{bDqyu1ttM*q<16Q-DMQH7{<8zvn++$&G?NdD*%*63 z0N_g-#E?cKXYN%?2d%wvaoTRB7I)R}mKfoLvrQirfH#b9wof%weXCI=@^k189X)-+zAVzqzP_T`9ig9)sFYaD&K3v$6BUC z@fHHU^(^R{VZR?$r{_R$BhMSlG5!Brb|$!vBTbIx$(GTItOO!eS1I}GONp_tIvQ`fQUK+ z)w(p#9+R2m>}IiRRgRr=wQm~lCPm{pdBCOdgX5w zCMF7}G%=WO&lE&=1gbU6S2v9I$lnQDH(|`AEqBw9YP~xCh==t-&+%CBtgc z-yFT0KgMM;yk=ch;;IKsGk|hhzfiqmq)jeWX3W*#<^=!14A%FER}RtOpSVzs&Lq(R zJ{A`u59HH7Uooy%sQ{v5jO2Ti;vKplG}_C+`t#}myJA#oS0n+Jub}$hFL#BvJVTtO zP_%}Ds(o+lb2a4o!RlpqfdI*v2FUMaXv-{uV`Q@W%ObT!*CCsxlJ~@|Jy?yo4I2Q% z)B$J28#mBE{1iawz6eDS1+5Lcpgw(V66hEotsg2fIiFb(NpV~w-qVA|*mA{d8CpP$ z6zseirUCeVf#JidI9pFFhWD%5xdKKRwq{9Ze*LjJ2{4>9=PO2>qa1V5NsZ7of$#8> z%}1*lxM!;?+A_0O0fazNfaGs4ZCjf=QkPfTa4@|#?Mv6031eDxla0Bf?w2e+-*FpN8o5uB4+r13%^2#UjAR5>d`> z{&-OuEsP}3q#Ekt7g{#w3LAosVzPo3OQ4$upw>*B*Fv2w*O?KNF2%w#BDBP~HCon( z6qPP5!22)S`AM;(;}d9pFo4p1_&~zB9?|8)_fxuIwI>H}_R`)muawIm$r}_vW1bb;QS2}L;;8189oxc9} zo|dovT;kyv>xYW8l=!x(gLH4WqOKnRTOweMCK&b5LHZ(M2eB^|onBn*WK#l|R+XGmXB;=nb&okKLUXN@ph*lT#whhY!( zkm+|9v{6DZwxKb=yF6k82$bnlYQaaacoX;FP)J{TvCP1aSiRQtfQA5_ixnh`xi6aZ}&B0jc~Cg!aY8%+%mwL z%0txpN#sPHFbrgIDx4!5IaCz1VeR-Bl#>c>Lt8HkjZ`EUwOu3S=Ch@){Ute=0q;M3 zb?F71E9!*Ez1@dGej`Qg@3P5CvmB1jWe8r%R#Rg;neAic_HTUQ<_WEMKyRgy^N8Vm zJ<*Yd9aYxxg0#@$=AM}_+2r*uuwn+NA}~-iRwRB@x9Bn5ZYmn-JBMPiRaDRJi>sRK zoUDKMZSK{6p_^d+uBSjMek#JfiQo@QDQMv0ItcX^UC~E`LYeMH#aGA5!%#G>@gDxv z>$g@Pag@Yq1s(^X(62@p%ez;Sr(o@08tEP13xAU zsnH0^z@jRc9c#bTIKHEwJPu+0vHNyO=RrbwKZe6ju*rCKmXPNNaJNNrUK&YeM{lIK< zm2_~CmEK+7QQf*hoAz9;f5p>hpp-WZ4>led$J2+H4?wCF+eK@~LO^Ry%8F$@xHu%6 z#vZQ+hkJ89+9Luekvv^hl}W*?=M5VnHvcAmXg2@ityVuj9}tQ3$Ji&053J>UhUBsP2SoZVdo z$Hw@mMhAv|1eI6H8`=wohlLHpV5!R54DqwJ+SWD2x0wqBNagIh?P<6t3} z#Kv4<001>zU%Md#Dap(4934x`L{sMU1*LdbnkBF3iCgXJA;ZC=U4R7W_htG( zG3MysIdTAp8{rV~!BRz@n+|dSiBDYCRJi{pKdddOFnrCoc$UEwU^0#abTPRBI4R)1 z*%OoeR_oTf$B2V21zJjP41F$CmPIh9xSK?GZjVI4MkjjB+}Q_B3c5_{plqO}Zb8Tw zR>3oi+KreXE81xjP@jpdl0j61G9*OTCr)8kr}dsbgnY07wB9c}OfTUMBkwk!pMz&j zx2Ylf#iXg&;!pvPP`19eIgnBw0?F1+lkUAnBX`%|o*;Eq>L_g;{bn_=6$_A;sM&%gT?O^}Ky+x{ zT+f6Tmaz03dicuhwb{~`e!%e1&Fsauw6?onfY*GLjYJs2MEw|0_?g(J9t!#MKo@Ia zc29sQ3uZ}8zj4jg>BfsLqx36(cI>lrnCeGltyo%C)t<--c$sYa_L9PtvabR4ann#v=TQG9>W!0jR3!0oiu%6m24+QmqyM%wf39BI1ovORlL zLxs|{FZUiAXq%QQgNGG}@mFJwC&iu4ic$j#<1B~LMp>c;^>G1NtYMYKo%^ArU#0Db zT9|y9#i0R(5Vi8haL9?Fc8n}(fB5ea0B=IuB?hVt?s(pj9G}AD`6>L9f}mMLPr9ou zbU?0@6?MqQti-5MJ?D%~mpwH(+NxI4(wr)Kq~8#%cX48tw=c+bT|xhsxa;I=Ohy?J_NN zHUIPv0sA5SM7>ajn~f!%HT|!+@X}i&m9W)tN`?0ST~j@!Df+}!{Kw^$-ha01Y8h(Q IsN2Wb%7 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/hydration_module.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/hydration_module.json new file mode 100644 index 0000000..d2d7161 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/hydration_module.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:hydration_module" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/hydration_module" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraformer.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraformer.json new file mode 100644 index 0000000..e63b8a1 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraformer.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:terraformer" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/terraformer" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/tags/item/hydration_input.json b/multiloader/common/src/main/resources/data/nerospace/tags/item/hydration_input.json new file mode 100644 index 0000000..b423dde --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/tags/item/hydration_input.json @@ -0,0 +1,6 @@ +{ + "values": [ + "nerospace:glacite", + "nerospace:glacite_block" + ] +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index cb86391..31524f9 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -162,6 +162,11 @@ public void register(EntityType type, SpawnPlacementType plac (be, direction) -> ContainerStorage.of(be, direction), ModBlockEntities.TERRAFORMER.get()); + // Hydration Module: glacite in (no energy of its own). + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.HYDRATION_MODULE.get()); + ItemStorage.SIDED.registerForBlockEntity( (be, direction) -> ContainerStorage.of(be, direction), ModBlockEntities.TRASH_CAN.get()); diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 8db8cae..95c1ad8 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -16,6 +16,7 @@ import za.co.neroland.nerospace.client.FuelRefineryScreen; import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.HydrationModuleScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.client.TerraformerScreen; @@ -36,6 +37,7 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); MenuScreens.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); MenuScreens.register(ModMenuTypes.TERRAFORMER.get(), TerraformerScreen::new); + MenuScreens.register(ModMenuTypes.HYDRATION_MODULE.get(), HydrationModuleScreen::new); ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { @Override diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index 712e949..b295d04 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -148,6 +148,14 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { ? new WorldlyContainerWrapper(be, side) : VanillaContainerWrapper.of(be)); + // Hydration Module: glacite in (no energy of its own). + event.registerBlockEntity( + Capabilities.Item.BLOCK, + ModBlockEntities.HYDRATION_MODULE.get(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + event.registerBlockEntity( Capabilities.Item.BLOCK, ModBlockEntities.TRASH_CAN.get(), diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index d01f64f..343d67f 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -22,6 +22,7 @@ import za.co.neroland.nerospace.client.FuelRefineryScreen; import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.HydrationModuleScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; import za.co.neroland.nerospace.client.TerraformerScreen; @@ -60,6 +61,7 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); event.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); event.register(ModMenuTypes.TERRAFORMER.get(), TerraformerScreen::new); + event.register(ModMenuTypes.HYDRATION_MODULE.get(), HydrationModuleScreen::new); } /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ From b0e1ebc6c4221e41e1987022be38571eba3f9588 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 11:53:00 +0200 Subject: [PATCH 53/82] Add Terraform Monitor block, BE, menu, screen Implements the Terraform Monitor (terraforming slice 6a): adds TerraformMonitorBlock, TerraformMonitorBlockEntity, TerraformMonitorMenu and TerraformMonitorScreen plus registrations and assets. Registers block, block entity, item and menu type in ModBlocks/ModBlockEntities/ModItems/ModMenuTypes, and wires client screen in Fabric and NeoForge clients. Includes blockstate, model, textures, gui, loot table and nine new lang keys; the monitor is a readout-only UI (no inventory) with a ContainerData sync of seven fields and comparator output reporting the local column stage. Also updates the multiloader port checklist to reflect the new slice. --- docs/MULTILOADER_PORT_CHECKLIST.md | 19 +- .../client/TerraformMonitorScreen.java | 46 +++++ .../machine/TerraformMonitorBlock.java | 95 ++++++++++ .../machine/TerraformMonitorBlockEntity.java | 172 ++++++++++++++++++ .../nerospace/menu/TerraformMonitorMenu.java | 81 +++++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 6 + .../neroland/nerospace/registry/ModItems.java | 3 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../blockstates/terraform_monitor.json | 19 ++ .../nerospace/items/terraform_monitor.json | 6 + .../assets/nerospace/lang/en_us.json | 9 + .../models/block/terraform_monitor.json | 114 ++++++++++++ .../textures/block/terraform_monitor.png | Bin 0 -> 402 bytes .../block/terraform_monitor_front.png | Bin 0 -> 368 bytes .../textures/gui/terraform_monitor.png | Bin 0 -> 5736 bytes .../loot_table/blocks/terraform_monitor.json | 21 +++ .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeClientSetup.java | 2 + 19 files changed, 601 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformMonitorScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformMonitorMenu.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/terraform_monitor.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/terraform_monitor.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/terraform_monitor.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor_front.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraform_monitor.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraform_monitor.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 9f1e4f9..372a6d7 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,15 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~182 classes ported, ~82 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~186 classes ported, ~78 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 6a: Terraform Monitor.** All 4 cells green. Added +> `machine/{TerraformMonitorBlock, TerraformMonitorBlockEntity}` + `menu/TerraformMonitorMenu` + +> `client/TerraformMonitorScreen` (pure readout, no inventory; reads `TerraformManager`). Registered + assets + +> loot table + lang. Terraforming is now slices 1–5 + 6a done; only optional ambient bits remain (6b: Drift, +> ChunkLoader, GreenxertzAtmosphere). + > **2026-06-21 update — terraforming slice 5: Hydration Module.** All 4 cells green. Added > `machine/{HydrationModuleBlock, HydrationModuleBlockEntity}` + `menu/HydrationModuleMenu` + > `client/HydrationModuleScreen` (BE on `WorldlyContainer`/`NonNullList`, melts glacite into a touching @@ -226,8 +232,15 @@ checked by a headless build). hydration buffer (`acceptHydration`); no energy of its own. Registered block/item/BE/menu + per-loader screen + item cap; copied block (3 tex, FACING blockstate, model) + GUI + loot table + `hydration_input` tag JSON + 5 lang keys. **Also fixed: the Terraformer block was missing its loot table from 4b (added).** - - [ ] **Slice 6 — Terraform Monitor** (block/BE/menu/screen) + `TerraformDrift` + `TerraformChunkLoader` + - `GreenxertzAtmosphere`. Risk: **high** (world mutation, chunk-loading, events). + - [x] **Slice 6a — Terraform Monitor DONE (4 cells green).** `machine/{TerraformMonitorBlock, + TerraformMonitorBlockEntity}` + `menu/TerraformMonitorMenu` + `client/TerraformMonitorScreen`. Pure readout + (no inventory — `MenuProvider` + `ContainerData`): finds the nearest Terraformer via `TerraformManager`, + shows stage radii / hydration / stall + the local column's stage on a comparator. Registered + per-loader + screen + assets + loot table + 9 lang keys. No caps (no inventory). + - [ ] **Slice 6b — ambient/secondary (optional).** `TerraformDrift` (idle ground-cover garnish), + `TerraformChunkLoader` (the deferred opt-in active force-loader — needs a chunk-force-ticket seam), + `GreenxertzAtmosphere` (assess vs the already-ported OxygenManager/field/terraformed-flag — may be superseded). + None required for terraforming to function. ### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) — **DONE (4 cells green)** - [x] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + `ModFeatures` diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformMonitorScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformMonitorScreen.java new file mode 100644 index 0000000..abe3dca --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TerraformMonitorScreen.java @@ -0,0 +1,46 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.menu.TerraformMonitorMenu; + +/** + * Screen for the Terraform Monitor (DEEPER_TERRAFORM_DESIGN.md §6): the nearest Terraformer's stage + * radii, hydration buffer and stall reason, plus the LOCAL column's stage — themed terraform green. + */ +public class TerraformMonitorScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/terraform_monitor.png"); + private static final int ACCENT = 0xFF54D46A; // green (terraform family) + + public TerraformMonitorScreen(TerraformMonitorMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 176, 166); + this.titleLabelX = 10; + this.inventoryLabelX = 10; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + int stage = this.menu.getLocalStage(); + label(g, Component.translatable("gui.nerospace.terraform_monitor.stage." + stage), 8, 20, + stage >= 3 ? 0xFF9CF0C0 : (stage > 0 ? ACCENT : 0xFF7E8EA0)); + + if (!this.menu.isLinked()) { + label(g, Component.translatable("gui.nerospace.terraform_monitor.no_link"), 8, 36, 0xFF7E8EA0); + return; + } + label(g, Component.translatable("gui.nerospace.terraform_monitor.radii", + this.menu.getRootedRadius(), this.menu.getHydrationRadius(), this.menu.getLifeRadius()), + 8, 36, 0xFFB7E8C2); + label(g, Component.translatable("gui.nerospace.terraform_monitor.hydration", + this.menu.getHydration()), 8, 48, 0xFF78D2F0); + if (this.menu.isStalled()) { + label(g, Component.translatable("gui.nerospace.terraformer.needs_glacite"), 8, 60, 0xFFFF6A5E); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlock.java new file mode 100644 index 0000000..f49b7c2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlock.java @@ -0,0 +1,95 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.phys.BlockHitResult; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The Terraform Monitor block (DEEPER_TERRAFORM_DESIGN.md §6): a readout backed by + * {@link TerraformMonitorBlockEntity}; its comparator output is the LOCAL column's terraform stage + * (0/5/10/15). + */ +public class TerraformMonitorBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(TerraformMonitorBlock::new); + /** The screen faces the placer. Visual only. */ + public static final EnumProperty FACING = BlockStateProperties.HORIZONTAL_FACING; + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public TerraformMonitorBlock(Properties properties) { + super(properties); + this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH)); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(FACING); + } + + @Override + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite()); + } + + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new TerraformMonitorBlockEntity(pos, state); + } + + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.TERRAFORM_MONITOR.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof TerraformMonitorBlockEntity be) { + serverPlayer.openMenu(be); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof TerraformMonitorBlockEntity be ? be.comparatorSignal() : 0; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlockEntity.java new file mode 100644 index 0000000..f6f539e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformMonitorBlockEntity.java @@ -0,0 +1,172 @@ +package za.co.neroland.nerospace.machine; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.menu.TerraformMonitorMenu; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.world.TerraformManager; + +/** + * Terraform Monitor (DEEPER_TERRAFORM_DESIGN.md §6): the readout block. Finds the nearest registered + * Terraformer within {@link #LINK_RANGE} via {@link TerraformManager} (cheap — the SavedData already + * indexes every machine), shows its stage radii / hydration / stall reason, and reports the LOCAL + * column's effective stage on a comparator (0/5/10/15) so ranches can automate on "the land turned + * Living". + */ +public class TerraformMonitorBlockEntity extends BlockEntity implements MenuProvider { + + public static final int DATA_COUNT = 7; + /** How far (blocks, horizontal) the Monitor searches for a Terraformer to read. */ + public static final int LINK_RANGE = 32; + /** Ticks between readout refreshes (display only — nothing gameplay-critical). */ + private static final int REFRESH_INTERVAL_TICKS = 20; + + private transient boolean linked; + private transient int rootedRadius; + private transient int hydrationRadius; + private transient int lifeRadius; + private transient int hydration; + private transient boolean stalled; + private transient int localStage; + + /** + * Synced to the menu: [0]=linked [1]=rootedRadius [2]=hydrationRadius [3]=lifeRadius + * [4]=hydration [5]=stalled [6]=localStage. + */ + private final ContainerData dataAccess = new ContainerData() { + @Override + public int get(int index) { + return switch (index) { + case 0 -> linked ? 1 : 0; + case 1 -> rootedRadius; + case 2 -> hydrationRadius; + case 3 -> lifeRadius; + case 4 -> hydration; + case 5 -> stalled ? 1 : 0; + case 6 -> localStage; + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + switch (index) { + case 0 -> linked = value != 0; + case 1 -> rootedRadius = value; + case 2 -> hydrationRadius = value; + case 3 -> lifeRadius = value; + case 4 -> hydration = value; + case 5 -> stalled = value != 0; + case 6 -> localStage = value; + default -> { } + } + } + + @Override + public int getCount() { + return DATA_COUNT; + } + }; + + public TerraformMonitorBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.TERRAFORM_MONITOR.get(), pos, state); + } + + public ContainerData getDataAccess() { + return this.dataAccess; + } + + /** Comparator: the LOCAL column's effective stage, scaled to redstone (0/5/10/15). */ + public int comparatorSignal() { + return this.localStage * 5; + } + + public void tick(Level level, BlockPos pos, BlockState state) { + if (!(level instanceof ServerLevel serverLevel) + || serverLevel.getGameTime() % REFRESH_INTERVAL_TICKS != 0) { + return; + } + refresh(serverLevel, pos); + } + + /** One readout refresh (public so the gametest can drive it without waiting on the interval). */ + public void refresh(ServerLevel level, BlockPos pos) { + int oldStage = this.localStage; + this.localStage = TerraformConversion.effectiveStage(level.getChunkAt(pos)); + + BlockPos nearest = nearestTerraformer(level, pos); + this.linked = nearest != null; + if (nearest != null) { + TerraformManager manager = TerraformManager.get(level); + this.rootedRadius = manager.stageRadius(nearest, 1); + this.hydrationRadius = manager.stageRadius(nearest, 2); + this.lifeRadius = manager.stageRadius(nearest, 3); + // Live hydration/stall come from the machine itself when its chunk is loaded. + if (level.isLoaded(nearest) + && level.getBlockEntity(nearest) instanceof TerraformerBlockEntity terraformer) { + this.hydration = terraformer.getHydration(); + this.stalled = terraformer.getDataAccess().get(8) != 0; + } else { + this.hydration = 0; + this.stalled = false; + } + } else { + this.rootedRadius = this.hydrationRadius = this.lifeRadius = this.hydration = 0; + this.stalled = false; + } + + if (oldStage != this.localStage) { + level.updateNeighbourForOutputSignal(pos, getBlockState().getBlock()); + } + } + + @Nullable + private BlockPos nearestTerraformer(ServerLevel level, BlockPos pos) { + BlockPos[] best = {null}; + long[] bestDist = {(long) LINK_RANGE * LINK_RANGE}; + TerraformManager.get(level).forEachMachine((center, r1, r2, r3) -> { + long dx = center.getX() - pos.getX(); + long dz = center.getZ() - pos.getZ(); + long d = dx * dx + dz * dz; + if (d <= bestDist[0]) { + bestDist[0] = d; + best[0] = center; + } + }); + return best[0]; + } + + // --- MenuProvider ------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.terraform_monitor"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new TerraformMonitorMenu(containerId, playerInventory, this, this.dataAccess); + } + + /** Menu range check (no Container interface — the Monitor has no inventory). */ + public boolean stillValid(Player player) { + if (this.level == null || this.level.getBlockEntity(this.worldPosition) != this) { + return false; + } + return player.distanceToSqr(this.worldPosition.getX() + 0.5, + this.worldPosition.getY() + 0.5, this.worldPosition.getZ() + 0.5) <= 64.0; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformMonitorMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformMonitorMenu.java new file mode 100644 index 0000000..541fdbb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/TerraformMonitorMenu.java @@ -0,0 +1,81 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.machine.TerraformMonitorBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * Menu for the Terraform Monitor (DEEPER_TERRAFORM_DESIGN.md §6): pure readout — no slots, seven + * synced data values (link, three stage radii, hydration, stall flag, local stage). + */ +public class TerraformMonitorMenu extends AbstractContainerMenu { + + @Nullable + private final TerraformMonitorBlockEntity monitor; + private final ContainerData data; + + public TerraformMonitorMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, null, + new SimpleContainerData(TerraformMonitorBlockEntity.DATA_COUNT)); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public TerraformMonitorMenu(int containerId, Inventory playerInventory, + @Nullable TerraformMonitorBlockEntity monitor, ContainerData data) { + super(ModMenuTypes.TERRAFORM_MONITOR.get(), containerId); + checkContainerDataCount(data, TerraformMonitorBlockEntity.DATA_COUNT); + this.monitor = monitor; + this.data = data; + this.addStandardInventorySlots(playerInventory, 8, 84); + this.addDataSlots(data); + } + + @Override + public boolean stillValid(Player player) { + TerraformMonitorBlockEntity local = this.monitor; // local copy for the null analysis + return local == null || local.stillValid(player); + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; // readout only — nothing to move into + } + + // --- Screen helpers ----------------------------------------------------- + + public boolean isLinked() { + return this.data.get(0) != 0; + } + + public int getRootedRadius() { + return this.data.get(1); + } + + public int getHydrationRadius() { + return this.data.get(2); + } + + public int getLifeRadius() { + return this.data.get(3); + } + + public int getHydration() { + return this.data.get(4); + } + + public boolean isStalled() { + return this.data.get(5) != 0; + } + + public int getLocalStage() { + return this.data.get(6); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 626af54..03a42ea 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -15,6 +15,7 @@ import za.co.neroland.nerospace.machine.PassiveGeneratorBlockEntity; import za.co.neroland.nerospace.machine.HydrationModuleBlockEntity; import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; +import za.co.neroland.nerospace.machine.TerraformMonitorBlockEntity; import za.co.neroland.nerospace.machine.TerraformerBlockEntity; import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; @@ -126,6 +127,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("hydration_module", key -> new BlockEntityType<>(HydrationModuleBlockEntity::new, java.util.Set.of(ModBlocks.HYDRATION_MODULE.get()))); + public static final RegistryEntry> TERRAFORM_MONITOR = + BLOCK_ENTITIES.register("terraform_monitor", + key -> new BlockEntityType<>(TerraformMonitorBlockEntity::new, java.util.Set.of(ModBlocks.TERRAFORM_MONITOR.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index afa69ae..2f67423 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -21,6 +21,7 @@ import za.co.neroland.nerospace.machine.PassiveGeneratorBlock; import za.co.neroland.nerospace.machine.HydrationModuleBlock; import za.co.neroland.nerospace.machine.SolarPanelBlock; +import za.co.neroland.nerospace.machine.TerraformMonitorBlock; import za.co.neroland.nerospace.machine.TerraformerBlock; import za.co.neroland.nerospace.machine.quarry.MinerTier; import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlock; @@ -210,6 +211,11 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_LIGHT_BLUE).strength(3.5F, 6.0F) .requiresCorrectToolForDrops().lightLevel(s -> 4).sound(SoundType.METAL))); + public static final RegistryEntry TERRAFORM_MONITOR = BLOCKS.register("terraform_monitor", + key -> new TerraformMonitorBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_GREEN).strength(3.0F, 6.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 7).sound(SoundType.METAL))); + public static final RegistryEntry SOLAR_PANEL = BLOCKS.register("solar_panel", key -> new SolarPanelBlock(BlockBehaviour.Properties.of() .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 5e3611e..7532f7d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -88,6 +88,7 @@ public final class ModItems { public static final RegistryEntry OXYGEN_GENERATOR_ITEM = blockItem("oxygen_generator", ModBlocks.OXYGEN_GENERATOR); public static final RegistryEntry TERRAFORMER_ITEM = blockItem("terraformer", ModBlocks.TERRAFORMER); public static final RegistryEntry HYDRATION_MODULE_ITEM = blockItem("hydration_module", ModBlocks.HYDRATION_MODULE); + public static final RegistryEntry TERRAFORM_MONITOR_ITEM = blockItem("terraform_monitor", ModBlocks.TERRAFORM_MONITOR); public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); public static final RegistryEntry ROCKET_LAUNCH_PAD_ITEM = blockItem("rocket_launch_pad", ModBlocks.ROCKET_LAUNCH_PAD); public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); @@ -278,7 +279,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), HYDRATION_MODULE_ITEM.get(), + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), HYDRATION_MODULE_ITEM.get(), TERRAFORM_MONITOR_ITEM.get(), SPEED_MODULE.get(), EFFICIENCY_MODULE.get(), FORTUNE_MODULE.get(), SILK_TOUCH_MODULE.get(), CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get())); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 7fc7e61..5627a50 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -12,6 +12,7 @@ import za.co.neroland.nerospace.menu.HydrationModuleMenu; import za.co.neroland.nerospace.machine.quarry.QuarryMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; +import za.co.neroland.nerospace.menu.TerraformMonitorMenu; import za.co.neroland.nerospace.menu.TerraformerMenu; import za.co.neroland.nerospace.rocket.RocketMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -58,6 +59,10 @@ public final class ModMenuTypes { MENUS.register("hydration_module", key -> new MenuType<>(HydrationModuleMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> TERRAFORM_MONITOR = + MENUS.register("terraform_monitor", + key -> new MenuType<>(TerraformMonitorMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/terraform_monitor.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/terraform_monitor.json new file mode 100644 index 0000000..94fad52 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/terraform_monitor.json @@ -0,0 +1,19 @@ +{ + "variants": { + "facing=east": { + "model": "nerospace:block/terraform_monitor", + "y": 90 + }, + "facing=north": { + "model": "nerospace:block/terraform_monitor" + }, + "facing=south": { + "model": "nerospace:block/terraform_monitor", + "y": 180 + }, + "facing=west": { + "model": "nerospace:block/terraform_monitor", + "y": 270 + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/terraform_monitor.json b/multiloader/common/src/main/resources/assets/nerospace/items/terraform_monitor.json new file mode 100644 index 0000000..80496eb --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/terraform_monitor.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/terraform_monitor" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 0e5fb52..8614419 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -52,6 +52,7 @@ "block.nerospace.solar_panel": "Solar Panel", "block.nerospace.station_floor": "Station Floor", "block.nerospace.station_wall": "Station Wall", + "block.nerospace.terraform_monitor": "Terraform Monitor", "block.nerospace.terraformer": "Terraformer", "block.nerospace.trash_can": "Trash Can", "block.nerospace.universal_pipe": "Universal Pipe", @@ -66,6 +67,7 @@ "container.nerospace.passive_generator": "Passive Generator", "container.nerospace.quarry_controller": "Quarry Controller", "container.nerospace.rocket": "Rocket", + "container.nerospace.terraform_monitor": "Terraform Monitor", "container.nerospace.terraformer": "Terraformer", "entity.nerospace.alien_villager": "Alien Villager", "entity.nerospace.cinder_stalker": "Cinder Stalker", @@ -92,6 +94,13 @@ "gui.nerospace.quarry.state.mining": "Mining", "gui.nerospace.quarry.state.paused": "Paused", "gui.nerospace.rocket.launch": "Launch", + "gui.nerospace.terraform_monitor.hydration": "Water: %s", + "gui.nerospace.terraform_monitor.no_link": "No Terraformer within 32 blocks", + "gui.nerospace.terraform_monitor.radii": "Radii: %s / %s / %s", + "gui.nerospace.terraform_monitor.stage.0": "This ground: Dead", + "gui.nerospace.terraform_monitor.stage.1": "This ground: Rooted", + "gui.nerospace.terraform_monitor.stage.2": "This ground: Hydrated", + "gui.nerospace.terraform_monitor.stage.3": "This ground: Living", "gui.nerospace.terraformer.hydration": "Water: %s", "gui.nerospace.terraformer.idle": "Idle", "gui.nerospace.terraformer.needs_glacite": "Needs glacite", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/terraform_monitor.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/terraform_monitor.json new file mode 100644 index 0000000..dea6ade --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/terraform_monitor.json @@ -0,0 +1,114 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 4, + 0, + 4 + ], + "to": [ + 12, + 2, + 12 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#side" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 6, + 2, + 6 + ], + "to": [ + 10, + 6, + 10 + ] + }, + { + "faces": { + "down": { + "texture": "#side" + }, + "east": { + "texture": "#side" + }, + "north": { + "texture": "#front" + }, + "south": { + "texture": "#side" + }, + "up": { + "texture": "#side" + }, + "west": { + "texture": "#side" + } + }, + "from": [ + 1, + 5, + 6 + ], + "rotation": { + "angle": -22.5, + "axis": "x", + "origin": [ + 8, + 6, + 7 + ] + }, + "to": [ + 15, + 14, + 8 + ] + } + ], + "textures": { + "front": "nerospace:block/terraform_monitor_front", + "particle": "nerospace:block/terraform_monitor", + "side": "nerospace:block/terraform_monitor" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..98ff520dfafb56d290e4be5254e29babd347f0db GIT binary patch literal 402 zcmV;D0d4+?P)2#U^hXo2LUqJp6Sc$zpualOMCM={`lq}Q3MD_0a~TF(mPs{RcytMkZg^@7){Bq zFrQ5+WSgzWCz4gn)~JKSj*u9mafHN`9>!=o-5y(`a)Njs5e!qJ=f~;^Z!p($mGXX%W6u(^kJHgf{JKO?5vWhWA9|_x$q8*(kJD7nZ z9(Y`SE>3#?N|U474;mbvZbaZ+{Qv*}07*qoM6N<$f|Kj8Z~y=R literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor_front.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor_front.png new file mode 100644 index 0000000000000000000000000000000000000000..4d4c15fdc05f78c3493dcf47a12ad4c7b334def6 GIT binary patch literal 368 zcmV-$0gwKPP)UYe9sB|PF%IJ3Bn}~i6i4YG4jIIugbWf& zIZBXg2pzKM5bly-r47DQ?!NDR?!Ax8(VhGL%|Fxt@E-4p#;aP!Y%2JQMGi=0Oy~9t zu?%UScL4|&KBw)DHduD6vS;-j-sVp^U@ao%FAsUA|Ex_x@cQdywb$!i0AK)>lMa>g z7%JXAd7ByDW~SF`5fy+$#|B_LTQ%5RwL&Z@KYfYRRH{&&+Ad;EuV z1`p!l#U2b~|E4IP{vos*wllrca4iqmnn1*7UJ6p!p1a_#tk0)D0K2v^gK{m0^k`bb+R!!Y4hn~j z;eG+$?*f0XG6{BZzbLTYei0DhUA8a9cw$Hym~XJTPDyWze9)FlU})RxP0@^ zt~ep)`b2p(iqop*P0GGur)Fn?E;bB`dcE@!M$o^`Wq#tncl^tE!sCU{CZEs`j_Btv zx2k0>m&cTR8$V5e4eyejDu3={Yg7uNhN~jJy#nf*bRm;lt(yO27`i1e@BNcMeu{;x zjHp&tiu!2`Qxs3>WVUdz#~U+?f+{ALL{;JOf@0VGt21nF75d6GrOIb{E!>Ld-Kz9v zpY^#idY?bz*8Ie^cn4Sc#*9Yvk)KJ$VbdI6|70klnkx;#1~j%5K0n^(y%Cq3rF9L( zyBOdeSs%c{$^8V5X5*Eo(v0vBQ3Y#wajx3iM!pk|ru^ZGm7xhgEn0txLD})2P2>=M znwOOFme4o~Ar|4IcLQ8FSJ={n85V98i^gaI!W9RoEI(xfNsp;<0?QA*0>OR; z&jo=}2$8(3TlgZ`SV1i;H~$e&Fd@|X$@7cZ@~H)-=}2cptX9#I+cB%d?ndD~#AyQv z=WA1l0LK)mDVT;qdr)$;6-#BTMLj6nlB(W1ymIk#DRbLSQFg7UAc8Yw*r<; z^?FH7(FJCtfH9gS%0s4(HYa&`VXvb9^N=t9@Eawe`((rH_C@VX`5`T8B=i%*%}?ge z?TzR61i`eLA%ES)68G2TSUC#urExkfsP`U{!}V<}!>~4Xrjp?nRHA*pAHFbhkHG(T z3vQsgJVN#pqh@={9FNTx!86CHjm(%Fr#fjZZ`3x7nZM7qFd>`_|!ndMqx`ZHKo%a zgk{Yro$7Ax5sQ&4rW&6TdzA|s@XfQ65T5Vp&SfwbRD?1X4)pTnk>f|Z9aq@l5*9pN zKXq8diuV`OA`C*~5}O`QdCp{seVQn{9C>LTW+^4*xl1u$wyWeAwUnspI7=VWEwsqJ zM_`uXv`E*BYEI+615QJvQB9rK)|rp@==`XRq2{CAyKW-MUwex|=#gdhp&};~J+Cyx zAgSyO&1n{^Q*394w#u^d=|k)CeS_P>NXmdz8YbVp91{DJBU;y)3vwjMfy;|`L3{LX z48COdBL6a5+Q+*7(nYw)Jaq|l9PG!wL6RT;xnE=D`ch~oG3D89YN^{k(>Oe%8SA#$ zg%mbul(KJ~@2Xya5V7$#e-oW`^YFbU{bF|30r<}qX8Emw&G`Ynog%{hlK{PO*Yd)) zF)O>Ff$nntaNOKxhNvD&(Hc3obfzYGE5?#JUO9jLi@@*glKf&9ZL*mzsXQiKUdTm} z^gQ#TS|!7YA;P3)rG5R4DD?2KaVS0|HN`V|F84G_&_K#I3ho?G1m~qG>X!nfa?JG} zHRfOSY|Hs*wY-%B+ZxkmbHL=glyhtPWjyp)!Qo>{qO8Z2xU(fK(xc{ollSN>5K>Zk zOM@gxcH>yZc6ZbBLQ)4(A=&VWS+uEjOwstOywj8wj^Sw+^%Aj}U4}vR?pQab8Bs_gHG zbgdSi?w;Tj2dhd$%)y~U@uLRny`{(xzj;RY*m@#92Gx6}Q)z30l^;6un>ZRvExCm1 z4xP57+m*98-yG~_jwM>KU0-vFnheSPXj80(oRQ30%&(jIy*a*|8sq|Dvj^j1^*xjo&RSBbK*$_Oug1UVNmxQN*?m718Jr zgb#DKh}f>}6BZ}x?oWxxW>-UHRq2Q>`)cqs(&W{*POO%1mdyAwjN~urPqp9*1~Q#X z2H0bQZr2c_%ntL)i9p0`AVoPjFXS|aD1xDBJp!TSrAV8%gDxp4-2Hb($Yn~7Cke3;N7 zB}=5AymaiKtpK`n`jsh|lvKgmc+?^Dh3(uLg;81(7FBdL#I3nI>TQDtLw? zU@i$BltyLTTLqJ)!LauW0k0U#SVFIu2G_>0h1%N(gZm77l#?+9f~u+@5^lxEZ^i7& zwG_}VdPd>A8mc>*nTY2X+jb|bgvK8Tm=2cSzykNa>x1m21|1+jryo(vW>x>;_OZN1 zTU=Yvsdj|a`wJq5RME(0&s<5kVo@dU40!&q2z@p}#Tq`_e89Lz#p~dp*`gimPON1- z0@576Mme`5G%^t^K#66QP~-MA!B>dX%3IDyXK(Q3Gg_JSS>-&t*L_hU_2%<;Y?$KS zfRQx(yQBmes=iGJhliE)Frwm?R?hz_C;KdLCv#@Aln<`^(d9yRz`>k7Z7!(@7{AF2 zhD0;(fjF}iohm!-_Fq+Hq57_U0}PE)vOOC)E# z)$_0hS+Sf=TRibeqW=>GY-^H8>}>yOWsR*UTRPLl;;KmrlEjaq#@uu|1)@ zc;YK-j=`iWsc;(|j&J2tT;)){)lH3AO3mbVr{y9Gc4y|bUoi#!J6f};uU7s>pkwac zjI~;941U?Jgsel${f%0OnD*ynij;6_xE!wy5W&f9i?Q;v7X2$+4=z6N9^D{NGofUd zY6P=e#~Dw%FB|n8R6ivQ?25;EU&}VPG^1E}eIe%d8`H?RH?V^{ zYu}R?lCstukNXnpDZa}YRz=K@<*c|nwc;CbL-|uXaRMzJV4ZLCqp?1ZR_!-q+Hx@i zihh=dm1mzFN(SIATsv@{>0bGM#fLzKULCiM!tr^jd$e}&X{)e)h56;t_}*`eY^?9! zrVvWwwjKR^#Z>F|o2+t*J4Z$M_Qx0K!vJ9=Nm8xeeN8!w^@-w%_4lmhm6grMcx(@H zvbNxTu+Z*CvC7%(+eo?ykF{u*(5K+6P95rmTE)(wDwaAL?cW)CHK2WYVHZ|>2rT1Q zfplsG$SYdq6=QU+)`K)Xd4|g#+yIjL0hEbkPdyoO*)rB4^s>4=><7QG>qk=t4&e6P z(d6@0+B_6IDEN@KuQVeKOh!O+o>t&nf63H%Rm#Q4U|)F>iU=qG_>k-?L70qgs_$PR zL1g22Rq=QS`d*kJo4wMZ&fQ~>zQ8I?%oTf}tj{Qr+ep8KGf_`mNji!ZD9dS&tL5|( z4Mp(2-~?@qm}R}UzZ8nd8PakW0pedPJP*diLi}GirgiF-t;LtpNY+qEeJ#xVI zv(jE_r$j!TXhR#p8%VlMbetZ}BPk~8Re2HM>HsU%@)=27A4G*bXP)dO#cViup}zc_ zTMs=XH%JNoR5$`uPZ%HKP7{J*d_hfxkIVwA(*1P&!cj%A`7VqJ=gY8=%oqWWSWT}* z{dikLUE{+-SN6ZK7^nO(=?xP#bdL%@b1mG%uFJ#UqeWXO-iwpnUA0OG_b!k5CqD`p zV*rFn5|jS@D56K*_b32=x4Ly4o!x-^dcepLyrT~g=@)h<84W#ko`BS;?dbtwX3x{K zGfCO5{4x5UvpH2PAO7~V=4{KYQYRb@pVK-B8d>)BL^8t&qa^!^QJ6VgWyN&+S_Lh& zWh@%_=D6!cTby(6`)vYI$cumSgMox`m*W5*-TCZ6P<#23zR~=f3R;cU5NMys(uZqw za|vR!ayZMUwPDb&!oB`mjljpNkq`iR{=Aj&_Adgq*$t_+)IKrOA^A85#SeR)$U@jV zgZTjD7Ndr25#_>7P4McwY!tXAe}k4ADMSB)W+Ilj$-#Qwi-*97D>HYIjhkrGcnMB zV&8l}C)xW1r-(`?|I*3;_nj%N;FMl|37oD@so_=_mRLqn_C+U-{{E6#?P8j6!5-~L z55vaZC(+t_2D)o6C8!A-2#>xF)eNw@XXfFL89AN9uY*C_=7Gx}>F#a4k)viH%ol!1$Sg z_n3)bjgPZ{ITGfv{V|)80?|`f^2hlzJT|f#Ddzj zEd1<6LuoJb(%3^EUQFo2DxV`z^g$3gQi)dIU_NOqGhHYzW5oL=;|FM~C=exvDCq>Bj#-J+km{ZSdG(mnPpqEd_xLX0K z_D*O@610mBcIBdsJqA6@K6d=;w_`zh#`A zouan&%4J`qFR~>%`(A(Q&YApveW*mX`U)0R#d$N%x z&)3AHd){FvlsP3*NO8yl_|19AX-{tY}b?dyL3BWQyNJ1&4GJ& zSCqGF8mKHV1IHz*CR4c+@mw!7E6`npz;NbYiu;EZy}BezDXzJDcu?a=Um3AV~K#&a|Gqy+a(4~kotAwH_@zNO{Llxo@ww<#nkU2}aY zsPZaWh^rhPt6TWT0!^O2Z(tzT*Je0W{#Z1hmCtJYS5e4V(ch@ne)`(6R^Y{aUPqnvPS%CR267~4<}b>&?KAUV zUtG~IFx=U@_m!Zy`t|EEk*2m8uJU7Uelx#26|>BZ%#_paHsxKaf80t>hWj!coHO!0 z(|Bw0o|}ykWo~@wz!Tf7=lgi(VC+fHtSG9 Date: Sun, 21 Jun 2026 11:59:48 +0200 Subject: [PATCH 54/82] Add TerraformDrift and wire server ticks Introduce TerraformDrift: a new cross-loader, server-driven cosmetic drift pass that sparsely places ground cover on settled terraformed land. The class runs once per second (DRIFT_PER_SECOND = 4), checks loaded chunks and nearby players, queries TerraformManager for rooted machines, and places grass tufts/flowers/rare saplings as garnish. Wire TerraformDrift.tick(...) into both Fabric and NeoForge server tick hooks. Update the port checklist to mark Slice 6b (TerraformDrift) as done and note the terraforming port status. --- docs/MULTILOADER_PORT_CHECKLIST.md | 20 +++- .../nerospace/world/TerraformDrift.java | 105 ++++++++++++++++++ .../nerospace/fabric/NerospaceFabric.java | 2 + .../nerospace/neoforge/NerospaceNeoForge.java | 4 +- 4 files changed, 125 insertions(+), 6 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformDrift.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 372a6d7..f79ebc9 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,15 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~186 classes ported, ~78 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~187 classes ported, ~77 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — terraforming slice 6b: TerraformDrift (ambient cosmetic).** All 4 cells green. +> `world/TerraformDrift` ticked from the shared server hook. **Terraforming is now essentially complete** +> (slices 1–5 + 6a + drift); only the opt-in force-loader remains (off by default). Note: `GreenxertzAtmosphere` +> is the root's oxygen-survival class, already superseded by the ported `OxygenManager` — reclassified out of +> terraforming; its hazard-shield + airlock-refill extras are a separate optional oxygen enhancement. + > **2026-06-21 update — terraforming slice 6a: Terraform Monitor.** All 4 cells green. Added > `machine/{TerraformMonitorBlock, TerraformMonitorBlockEntity}` + `menu/TerraformMonitorMenu` + > `client/TerraformMonitorScreen` (pure readout, no inventory; reads `TerraformManager`). Registered + assets + @@ -237,10 +243,14 @@ checked by a headless build). (no inventory — `MenuProvider` + `ContainerData`): finds the nearest Terraformer via `TerraformManager`, shows stage radii / hydration / stall + the local column's stage on a comparator. Registered + per-loader screen + assets + loot table + 9 lang keys. No caps (no inventory). - - [ ] **Slice 6b — ambient/secondary (optional).** `TerraformDrift` (idle ground-cover garnish), - `TerraformChunkLoader` (the deferred opt-in active force-loader — needs a chunk-force-ticket seam), - `GreenxertzAtmosphere` (assess vs the already-ported OxygenManager/field/terraformed-flag — may be superseded). - None required for terraforming to function. + - [x] **Slice 6b — `TerraformDrift` DONE (4 cells green).** `world/TerraformDrift` — idle ground-cover + garnish on settled terraformed land, near players, on a per-second budget; cross-loader `tick(MinecraftServer)` + wired into both server-tick hooks (alongside meteor + oxygen-field). Config inlined. + - [ ] **Remaining (optional, low value):** `TerraformChunkLoader` (the deferred opt-in active force-loader — + needs a chunk-force-ticket seam; off by default so the chunk-load catch-up covers it). + `world/GreenxertzAtmosphere` is **NOT terraforming** — it's the root's full oxygen-survival class, already + superseded by the ported `OxygenManager` + diffusion field + terraformed-flag. Its only unported extras + (hazard shields heat/cold, gas-tank airlock refill) are a separate **oxygen enhancement**, tracked below. ### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) — **DONE (4 cells green)** - [x] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + `ModFeatures` diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformDrift.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformDrift.java new file mode 100644 index 0000000..67be2c0 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/TerraformDrift.java @@ -0,0 +1,105 @@ +package za.co.neroland.nerospace.world; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.levelgen.Heightmap; + +/** + * Cosmetic drift (DEEPER_TERRAFORM_DESIGN.md §2.3) — the passive lane of the stage engine: settled + * terraformed land keeps sprouting sparse ground cover even while the machine idles. Pure garnish on a + * hard per-second budget (loaded-and-near-players only); no gameplay effect. + * + *

Cross-loader port note: driven per-loader through {@link #tick(MinecraftServer)} from the same + * server-tick hooks as the meteor / oxygen-field drivers (the root used a NeoForge + * {@code LevelTickEvent}); the drift config is inlined (config seam deferred).

+ */ +public final class TerraformDrift { + + // --- Inlined Config (root shipped defaults) --- + private static final boolean DRIFT_ENABLED = true; + private static final int DRIFT_PER_SECOND = 4; + + /** How close a player must be for a drift placement to bother happening. */ + private static final double PLAYER_RANGE = 64.0D; + + private TerraformDrift() { + } + + /** Runs the cosmetic drift pass on every loaded dimension (once per second, near players). */ + public static void tick(MinecraftServer server) { + if (!DRIFT_ENABLED) { + return; + } + for (ServerLevel level : server.getAllLevels()) { + tickLevel(level); + } + } + + private static void tickLevel(ServerLevel level) { + if (level.getGameTime() % 20 != 0 || level.players().isEmpty()) { + return; + } + int budget = DRIFT_PER_SECOND; + if (budget <= 0) { + return; + } + + List machines = new ArrayList<>(); + TerraformManager.get(level).forEachMachine((center, r1, r2, r3) -> { + if (r1 > 0) { + machines.add(new long[] {center.asLong(), r1, r3}); + } + }); + if (machines.isEmpty()) { + return; + } + + RandomSource rnd = level.getRandom(); + for (int i = 0; i < budget; i++) { + long[] machine = machines.get(rnd.nextInt(machines.size())); + driftOnce(level, BlockPos.of(machine[0]), (int) machine[1], (int) machine[2], rnd); + } + } + + /** One budgeted placement attempt at a random point inside the machine's rooted disc. */ + private static void driftOnce(ServerLevel level, BlockPos center, int rootedRadius, + int lifeRadius, RandomSource rnd) { + double angle = rnd.nextDouble() * Math.PI * 2.0D; + double dist = Math.sqrt(rnd.nextDouble()) * rootedRadius; // area-uniform + int x = center.getX() + (int) Math.round(Math.cos(angle) * dist); + int z = center.getZ() + (int) Math.round(Math.sin(angle) * dist); + if (!level.hasChunk(x >> 4, z >> 4)) { + return; + } + int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z); + BlockPos above = new BlockPos(x, surfaceY, z); + if (!level.hasNearbyAlivePlayer(x + 0.5D, surfaceY, z + 0.5D, PLAYER_RANGE)) { + return; + } + BlockPos ground = above.below(); + if (!level.getBlockState(ground).is(Blocks.GRASS_BLOCK) || !level.getBlockState(above).isAir()) { + return; + } + // Mostly grass tufts, sometimes a flower; on Living ground the rare extra sapling (§2.3). + double roll = rnd.nextDouble(); + long dx = x - center.getX(); + long dz = z - center.getZ(); + boolean living = lifeRadius > 0 && dx * dx + dz * dz <= (long) lifeRadius * lifeRadius; + Block plant; + if (living && roll < 0.04D) { + plant = Blocks.OAK_SAPLING; + } else if (roll < 0.25D) { + plant = rnd.nextBoolean() ? Blocks.POPPY : Blocks.DANDELION; + } else { + plant = Blocks.SHORT_GRASS; + } + level.setBlock(above, plant.defaultBlockState(), Block.UPDATE_CLIENTS); + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 31524f9..2b8218f 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -28,6 +28,7 @@ import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.world.OxygenFieldEvents; +import za.co.neroland.nerospace.world.TerraformDrift; import za.co.neroland.nerospace.world.TerraformManager; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; @@ -88,6 +89,7 @@ public void register(EntityType type, SpawnPlacementType plac server.getPlayerList().getPlayers().forEach(OxygenManager::tick); MeteorEvents.tick(server); OxygenFieldEvents.tick(server); + TerraformDrift.tick(server); }); // Terraform catch-up: convert any in-range columns on chunks that load after the frontier passed. // (Fabric's Load SAM passes a third "newly generated" flag, which we don't need.) diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 8488c4d..74edc9f 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -28,6 +28,7 @@ import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; import za.co.neroland.nerospace.world.OxygenManager; +import za.co.neroland.nerospace.world.TerraformDrift; import za.co.neroland.nerospace.world.TerraformManager; /** @@ -56,10 +57,11 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { OxygenManager.tick(serverPlayer); } }); - // Natural meteor showers + oxygen-field diffusion: tick the per-level drivers once per server tick. + // Natural meteor showers + oxygen-field diffusion + terraform drift: tick the per-level drivers once per server tick. NeoForge.EVENT_BUS.addListener((ServerTickEvent.Post event) -> { MeteorEvents.tick(event.getServer()); OxygenFieldEvents.tick(event.getServer()); + TerraformDrift.tick(event.getServer()); }); // Terraform catch-up: convert any in-range columns on chunks that load after the frontier passed. NeoForge.EVENT_BUS.addListener((ChunkEvent.Load event) -> { From 4b23ebf0301ba97bd21a221cc51f224a9ba6bcff Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:26:17 +0200 Subject: [PATCH 55/82] Add oxygen field client visuals and hazard shields Add client-side oxygen visual layer and the networking to feed it: introduce OxygenFieldSyncPayload, ClientOxygenField and ClientOxygenVisuals; register the payload in ModNetwork and push range-limited snapshots to nearby players from OxygenFieldEvents every 10 ticks. Extend OxygenManager with per-planet hazards (Cindara = HEAT, Glacira = COLD), a hazard drain multiplier, suit-variant checks (full 4-piece set required) and thematic feedback (frost vignette / smoke). Wire client tick hooks in Fabric and NeoForge to run the visuals, and update the docs checklist. Visual config values and some seams are inlined; the haze fog-tint layer remains NeoForge-only and deferred. --- docs/MULTILOADER_PORT_CHECKLIST.md | 28 ++++- .../nerospace/client/ClientOxygenField.java | 68 ++++++++++++ .../nerospace/client/ClientOxygenVisuals.java | 91 ++++++++++++++++ .../nerospace/network/ModNetwork.java | 3 + .../network/OxygenFieldSyncPayload.java | 71 +++++++++++++ .../nerospace/world/OxygenFieldEvents.java | 21 +++- .../nerospace/world/OxygenManager.java | 100 +++++++++++++++++- .../fabric/NerospaceFabricClient.java | 8 +- .../neoforge/NeoForgeClientSetup.java | 8 +- 9 files changed, 383 insertions(+), 15 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenField.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenVisuals.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/network/OxygenFieldSyncPayload.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index f79ebc9..a15d0d4 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,19 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~187 classes ported, ~77 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~190 classes ported, ~74 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — oxygen field client visuals.** All 4 cells green. Added `network/OxygenFieldSyncPayload` +> + `client/{ClientOxygenField, ClientOxygenVisuals}`; the field now syncs to nearby clients and renders as +> drifting GLOW particles + a boundary sound — the breathable volume is finally visible. 2nd networking-seam +> consumer. The haze fog-tint layer is deferred (NeoForge-only fog event; no portable Fabric counterpart). + +> **2026-06-21 update — oxygen hazard shields.** All 4 cells green. Extended `OxygenManager` with per-planet +> hazards (Cindara heat / Glacira cold → ×4 drain unless wearing the matching suit variant) + frost/smoke +> feedback. The ported thermal/cryo suit variants are now functional (previously inert). No new class — an +> in-place enhancement. Airlock refill still deferred (needs the gas-cap lookup). + > **2026-06-21 update — terraforming slice 6b: TerraformDrift (ambient cosmetic).** All 4 cells green. > `world/TerraformDrift` ticked from the shared server hook. **Terraforming is now essentially complete** > (slices 1–5 + 6a + drift); only the opt-in force-loader remains (off by default). Note: `GreenxertzAtmosphere` @@ -198,9 +208,19 @@ checked by a headless build). Wired into both server-tick hooks alongside the meteor driver; `OxygenManager.isBreathable` now reads the field; the **Oxygen Generator registers itself as a field source**, draining `EMIT_MB_PER_TICK` from its tank while sourcing (and clears on `setRemoved`). Sealed bases are now genuinely breathable. ~9 field - config keys inlined. **Deferred (cosmetic): the client visual layer** — `OxygenFieldSyncPayload` + - `ClientOxygenField` + the particle/haze/boundary overlay (gated on a visual-quality config). -- [ ] **Deferred**: terraform breathability + criteria, hazard shields/feedback, gas-tank airlock refill. + config keys inlined. +- [x] **Oxygen field client visuals DONE (4 cells green).** `network/OxygenFieldSyncPayload` (range snapshot, + long[]/byte[]) registered clientbound and pushed from `OxygenFieldEvents` every 10t to nearby players; + `client/ClientOxygenField` (data holder) + `client/ClientOxygenVisuals` (client-tick: drifting GLOW particles + in breathable cells + a boundary-crossing sound). 2nd networking-seam consumer. **Deferred: the haze fog-tint + layer** — rode a NeoForge-only `ViewportEvent.ComputeFogColor` with no portable Fabric counterpart. +- [x] **Hazard shields DONE (4 cells green).** `OxygenManager` now applies a per-planet hazard (Cindara HEAT / + Glacira COLD): ×4 oxygen drain unless a full set of the matching `HazardShield` suit variant is worn (mixed + set = no shield). Adds `hazardFor`/`hazardShield`/`pieceVariant`/`hazardDrainMultiplier` + thematic feedback + (frost vignette on cold, smoke shimmer on hot — no extra damage path). **Makes the already-ported thermal/cryo + suit variants functional.** +- [ ] **Deferred**: terraform-breathability advancement criteria, gas-tank airlock refill (needs the gas-cap + lookup; the field/pad/terraformed already refill). - **Terraforming** (signature endgame) — sliced; **slice 1 DONE (4 cells green)**, rest sequenced: - [x] **Slice 1 — per-chunk data-attachment seam.** `IPlatformHelper.is/setTerraformed` + `get/setTerraformStage(LevelChunk)` backed by NeoForge `AttachmentType` (chunk `getData`/`setData`) and diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenField.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenField.java new file mode 100644 index 0000000..d744173 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenField.java @@ -0,0 +1,68 @@ +package za.co.neroland.nerospace.client; + +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; + +import za.co.neroland.nerospace.network.OxygenFieldSyncPayload; + +/** + * Client-side mirror of the nearby oxygen field (terraform design §1.7). Fed by + * {@link OxygenFieldSyncPayload} (the clientbound handler registered in {@code ModNetwork.init()}); + * read by {@link ClientOxygenVisuals}. A plain data holder (no client-only imports) so it is safe to + * reference from common network code. + */ +public final class ClientOxygenField { + + private static volatile Long2ByteOpenHashMap field = newMap(); + + private ClientOxygenField() { + } + + private static Long2ByteOpenHashMap newMap() { + Long2ByteOpenHashMap m = new Long2ByteOpenHashMap(); + m.defaultReturnValue((byte) 0); + return m; + } + + public static void accept(OxygenFieldSyncPayload payload) { + Long2ByteOpenHashMap m = newMap(); + Long2ByteMap incoming = payload.toMap(); + for (Long2ByteMap.Entry e : incoming.long2ByteEntrySet()) { + m.put(e.getLongKey(), e.getByteValue()); + } + field = m; + } + + public static void clear() { + field = newMap(); + } + + /** @return concentration {@code 0..MAX} at {@code pos} (0 if unknown). */ + public static int concentrationAt(BlockPos pos) { + return field.get(pos.asLong()) & 0xFF; + } + + public static boolean isEmpty() { + return field.isEmpty(); + } + + public static Long2ByteMap view() { + return field; + } + + /** @return true if {@code pos} is breathable but borders a vacuum cell (a "membrane" boundary). */ + public static boolean isMembrane(BlockPos pos, int threshold) { + if (concentrationAt(pos) < threshold) { + return false; + } + for (Direction dir : Direction.values()) { + if (concentrationAt(pos.relative(dir)) < threshold) { + return true; + } + } + return false; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenVisuals.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenVisuals.java new file mode 100644 index 0000000..91bfd40 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientOxygenVisuals.java @@ -0,0 +1,91 @@ +package za.co.neroland.nerospace.client; + +import it.unimi.dsi.fastutil.longs.Long2ByteMap; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.util.RandomSource; + +/** + * Oxygen-field visual FX (terraform design §1.7) — makes the otherwise-invisible breathable field + * readable: a soft crossfade note when the player crosses the breathable boundary, and sparse drifting + * GLOW particles inside oxygenated cells near the player. Reads the client-synced + * {@link ClientOxygenField}; client-only, called once per client tick from each loader's hook. + * + *

Cross-loader port note: the haze fog-tint layer (root layer 2) is deferred — it rode a NeoForge + * {@code ViewportEvent.ComputeFogColor} with no portable Fabric counterpart. Visual config is inlined + * (quality FULL; the config seam is deferred).

+ */ +public final class ClientOxygenVisuals { + + // --- Inlined visual config (root shipped defaults; quality = FULL) --- + private static final int BREATHABLE_THRESHOLD = 6; + private static final int MAX_CONCENTRATION = 15; + private static final double PARTICLE_INTENSITY = 1.0D; + private static final double BOUNDARY_INTENSITY = 1.0D; + + /** Spawn ambient particles only every Nth tick — heavy iteration, kept sparse for performance. */ + private static final int PARTICLE_INTERVAL_TICKS = 8; + private static final long MAX_DIST_SQ = 18L * 18L; + private static final int PARTICLE_BUDGET = 2; // FULL quality + + private static boolean wasBreathable; + private static int fxTick; + + private ClientOxygenVisuals() { + } + + public static void tick() { + Minecraft mc = Minecraft.getInstance(); + ClientLevel level = mc.level; + if (level == null || mc.player == null || mc.isPaused()) { + return; + } + + BlockPos playerPos = mc.player.blockPosition(); + + // Boundary sound: crossfade an ambient note when the player crosses the breathable boundary. + boolean breathingNow = ClientOxygenField.concentrationAt(playerPos.above()) >= BREATHABLE_THRESHOLD + || ClientOxygenField.concentrationAt(playerPos) >= BREATHABLE_THRESHOLD; + if (breathingNow != wasBreathable && BOUNDARY_INTENSITY > 0.0D) { + level.playLocalSound(playerPos, SoundEvents.BUBBLE_COLUMN_UPWARDS_AMBIENT, SoundSource.AMBIENT, + 0.25F, breathingNow ? 1.3F : 0.8F, false); + } + wasBreathable = breathingNow; + + // Particles are the expensive layer — run them only every Nth tick with a tiny budget. + if (++fxTick % PARTICLE_INTERVAL_TICKS != 0 || PARTICLE_INTENSITY <= 0.0D) { + return; + } + Long2ByteMap field = ClientOxygenField.view(); + if (field.isEmpty()) { + return; + } + RandomSource rnd = level.getRandom(); + int spawned = 0; + for (Long2ByteMap.Entry e : field.long2ByteEntrySet()) { + if (spawned >= PARTICLE_BUDGET) { + break; + } + int conc = e.getByteValue() & 0xFF; + if (conc < BREATHABLE_THRESHOLD) { + continue; + } + BlockPos p = BlockPos.of(e.getLongKey()); + if (p.distSqr(playerPos) > MAX_DIST_SQ) { + continue; + } + // A single drifting ambient GLOW, rate proportional to concentration. + if (rnd.nextDouble() < PARTICLE_INTENSITY * (conc / (double) MAX_CONCENTRATION) * 0.08D) { + level.addParticle(ParticleTypes.GLOW, + p.getX() + rnd.nextDouble(), p.getY() + rnd.nextDouble(), p.getZ() + rnd.nextDouble(), + 0.0D, 0.004D, 0.0D); + spawned++; + } + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java index f3f1581..5c39052 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java @@ -81,5 +81,8 @@ public static void init() { // method reference is safe to register from common code. clientbound(MeteorSyncPayload.TYPE, MeteorSyncPayload.STREAM_CODEC, za.co.neroland.nerospace.client.ClientMeteorTracker::accept); + // Oxygen field: server → nearby clients range-limited concentration snapshot for the visual layers. + clientbound(OxygenFieldSyncPayload.TYPE, OxygenFieldSyncPayload.STREAM_CODEC, + za.co.neroland.nerospace.client.ClientOxygenField::accept); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/OxygenFieldSyncPayload.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/OxygenFieldSyncPayload.java new file mode 100644 index 0000000..31ecc81 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/OxygenFieldSyncPayload.java @@ -0,0 +1,71 @@ +package za.co.neroland.nerospace.network; + +import it.unimi.dsi.fastutil.longs.Long2ByteMap; +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * Server → client oxygen-field snapshot (terraform design §1.7). Carries the range-limited set of + * oxygenated cells around a player as packed world positions + concentrations. The client keeps a + * small local copy ({@link za.co.neroland.nerospace.client.ClientOxygenField}) for the particle / + * boundary visual layers. + */ +public record OxygenFieldSyncPayload(long[] positions, byte[] values) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "oxygen_field_sync")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(OxygenFieldSyncPayload::write, OxygenFieldSyncPayload::read); + + public static OxygenFieldSyncPayload of(Long2ByteMap field) { + long[] pos = new long[field.size()]; + byte[] val = new byte[field.size()]; + int i = 0; + for (Long2ByteMap.Entry e : field.long2ByteEntrySet()) { + pos[i] = e.getLongKey(); + val[i] = e.getByteValue(); + i++; + } + return new OxygenFieldSyncPayload(pos, val); + } + + public Long2ByteMap toMap() { + Long2ByteOpenHashMap map = new Long2ByteOpenHashMap(this.positions.length); + map.defaultReturnValue((byte) 0); + for (int i = 0; i < this.positions.length; i++) { + map.put(this.positions[i], this.values[i]); + } + return map; + } + + private static void write(RegistryFriendlyByteBuf buf, OxygenFieldSyncPayload payload) { + buf.writeVarInt(payload.positions.length); + for (int i = 0; i < payload.positions.length; i++) { + buf.writeLong(payload.positions[i]); + buf.writeByte(payload.values[i]); + } + } + + private static OxygenFieldSyncPayload read(RegistryFriendlyByteBuf buf) { + int n = buf.readVarInt(); + long[] pos = new long[n]; + byte[] val = new byte[n]; + for (int i = 0; i < n; i++) { + pos[i] = buf.readLong(); + val[i] = buf.readByte(); + } + return new OxygenFieldSyncPayload(pos, val); + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java index d5fbd68..b9491f3 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java @@ -5,8 +5,11 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.Level; +import za.co.neroland.nerospace.network.ModNetwork; +import za.co.neroland.nerospace.network.OxygenFieldSyncPayload; import za.co.neroland.nerospace.registry.ModDimensions; /** @@ -29,18 +32,30 @@ public final class OxygenFieldEvents { /** Server ticks between field relaxation passes (inlined from Config.OXYGEN_SIM_INTERVAL_TICKS). */ private static final int SIM_INTERVAL_TICKS = 5; + /** How often the range-limited field view is pushed to nearby clients (for the visual layers). */ + private static final int SYNC_INTERVAL_TICKS = 10; + /** Blocks around a player the field snapshot covers (inlined from Config.OXYGEN_SYNC_RADIUS). */ + private static final int SYNC_RADIUS = 32; private OxygenFieldEvents() { } - /** Runs one throttled field pass per eligible dimension. */ + /** Runs one throttled field pass per eligible dimension, and syncs the nearby field to clients. */ public static void tick(MinecraftServer server) { for (ServerLevel level : server.getAllLevels()) { if (!FIELD_DIMENSIONS.contains(level.dimension())) { continue; } - if (level.getGameTime() % SIM_INTERVAL_TICKS == 0) { - OxygenFieldManager.get(level).simulate(level); + long time = level.getGameTime(); + OxygenFieldManager manager = OxygenFieldManager.get(level); + if (time % SIM_INTERVAL_TICKS == 0) { + manager.simulate(level); + } + if (time % SYNC_INTERVAL_TICKS == 0) { + for (ServerPlayer player : level.players()) { + ModNetwork.sendToPlayer(player, + OxygenFieldSyncPayload.of(manager.snapshotAround(player.blockPosition(), SYNC_RADIUS))); + } } } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java index 03dec20..d244979 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java @@ -3,6 +3,7 @@ import java.util.Set; import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; import net.minecraft.network.chat.Component; import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerLevel; @@ -29,9 +30,14 @@ *

Cross-loader port note. The root drives this from a NeoForge {@code PlayerTickEvent} and a * full diffusion {@code OxygenFieldManager} (sealed rooms + client overlay, networking-synced), plus * terraform breathability, hazard shields, and gas-tank airlock refills. The multiloader ticks it from a - * per-loader server-tick hook and keeps the self-contained survival core; the diffusion field, terraform, - * hazard variants, advancement criteria, and gas-airlock refill are deferred to their own batches. Values - * are inlined (the config seam is deferred).

+ * per-loader server-tick hook and keeps the self-contained survival core. The diffusion field, terraform + * breathability, and per-planet hazard shields (heat/cold) are now wired in; advancement criteria and the + * gas-tank airlock refill remain deferred. Values are inlined (the config seam is deferred).

+ * + *

Hazards (SUIT_HAZARD_DESIGN.md). Cindara runs HOT and Glacira runs COLD: an uncountered + * hazard multiplies oxygen drain ×{@link #HAZARD_DRAIN_MULTIPLIER} (no separate damage path — lethality + * stays with zero-O₂ suffocation). A full set of the matching {@link HazardShield} suit variant negates + * it; a mixed set does not.

*/ public final class OxygenManager { @@ -49,6 +55,8 @@ public final class OxygenManager { /** Breathable-zone scan radius around the player (launch pad / oxygen generator). */ private static final int SAFE_RADIUS = 6; private static final float SUFFOCATION_DAMAGE = 1.0F; + /** Drain factor on a hazard dimension without the matching suit variant (SUIT_HAZARD_DESIGN.md §2). */ + private static final int HAZARD_DRAIN_MULTIPLIER = 4; /** Every Nerospace planet (and the vacuum of the station) is airless. */ private static final Set> PLANETS = Set.of( @@ -82,7 +90,11 @@ public static void tick(ServerPlayer player) { if (isBreathable(level, player.blockPosition())) { oxygen = max; } else { - oxygen = Math.max(0, oxygen - (suited ? SUIT_DRAIN_PER_CHECK : BARE_DRAIN_PER_CHECK)); + // An uncountered dimension hazard (Cindara heat / Glacira cold) multiplies the drain. + int drain = (suited ? SUIT_DRAIN_PER_CHECK : BARE_DRAIN_PER_CHECK) + * hazardDrainMultiplier(level, player); + oxygen = Math.max(0, oxygen - drain); + hazardFeedback(level, player); } Services.PLATFORM.setOxygen(player, oxygen); } @@ -152,4 +164,84 @@ private static boolean isSuitPiece(ItemStack worn, Item... options) { } return false; } + + // --- Hazard shields (SUIT_HAZARD_DESIGN.md) ----------------------------- + + /** Per-planet environmental hazard a suit variant must counter (NONE elsewhere). */ + public enum HazardShield { + NONE, HEAT, COLD + } + + /** The hazard a dimension carries: Cindara runs hot, Glacira runs cold. */ + private static HazardShield hazardFor(ResourceKey dimension) { + if (ModDimensions.CINDARA_LEVEL.equals(dimension)) { + return HazardShield.HEAT; + } + if (ModDimensions.GLACIRA_LEVEL.equals(dimension)) { + return HazardShield.COLD; + } + return HazardShield.NONE; + } + + /** Drain factor for the player: ×{@link #HAZARD_DRAIN_MULTIPLIER} on an uncountered hazard, else 1. */ + private static int hazardDrainMultiplier(ServerLevel level, Player player) { + HazardShield hazard = hazardFor(level.dimension()); + if (hazard == HazardShield.NONE || hazardShield(player) == hazard) { + return 1; + } + return HAZARD_DRAIN_MULTIPLIER; + } + + /** + * The worn hazard shield — requires ALL FOUR pieces of the SAME variant (a heat helmet on a cryo + * suit grants the suit's air tank but no shield), orthogonal to the base oxygen-suit set. + */ + private static HazardShield hazardShield(Player player) { + HazardShield head = pieceVariant(player.getItemBySlot(EquipmentSlot.HEAD)); + if (head == HazardShield.NONE) { + return HazardShield.NONE; + } + if (pieceVariant(player.getItemBySlot(EquipmentSlot.CHEST)) == head + && pieceVariant(player.getItemBySlot(EquipmentSlot.LEGS)) == head + && pieceVariant(player.getItemBySlot(EquipmentSlot.FEET)) == head) { + return head; + } + return HazardShield.NONE; + } + + /** Which hazard variant a single worn piece belongs to (NONE for plain/T2/non-suit items). */ + private static HazardShield pieceVariant(ItemStack worn) { + if (worn.is(ModItems.OXYGEN_SUIT_HEAT_HELMET.get()) + || worn.is(ModItems.OXYGEN_SUIT_HEAT_CHESTPLATE.get()) + || worn.is(ModItems.OXYGEN_SUIT_HEAT_LEGGINGS.get()) + || worn.is(ModItems.OXYGEN_SUIT_HEAT_BOOTS.get())) { + return HazardShield.HEAT; + } + if (worn.is(ModItems.OXYGEN_SUIT_COLD_HELMET.get()) + || worn.is(ModItems.OXYGEN_SUIT_COLD_CHESTPLATE.get()) + || worn.is(ModItems.OXYGEN_SUIT_COLD_LEGGINGS.get()) + || worn.is(ModItems.OXYGEN_SUIT_COLD_BOOTS.get())) { + return HazardShield.COLD; + } + return HazardShield.NONE; + } + + /** + * Thematic feedback for an exposed, unprotected player (no extra damage — the O₂ bar is the cost): + * a building frost vignette on the cold world (capped below fully-frozen so vanilla freeze damage + * never double-dips), sparse smoke shimmer on the hot one. + */ + private static void hazardFeedback(ServerLevel level, Player player) { + HazardShield hazard = hazardFor(level.dimension()); + if (hazard == HazardShield.NONE || hazardShield(player) == hazard) { + return; + } + if (hazard == HazardShield.COLD) { + int cap = player.getTicksRequiredToFreeze() - 2; // never "fully frozen" => no freeze damage + player.setTicksFrozen(Math.min(cap, player.getTicksFrozen() + CHECK_INTERVAL_TICKS * 2 + 15)); + } else if (player.tickCount % (CHECK_INTERVAL_TICKS * 4) == 0) { + level.sendParticles(ParticleTypes.SMOKE, + player.getX(), player.getY() + 1.2D, player.getZ(), 3, 0.25D, 0.4D, 0.25D, 0.01D); + } + } } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 0e1dd96..97e1a84 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -10,6 +10,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.client.ClientEntityRenderers; +import za.co.neroland.nerospace.client.ClientOxygenVisuals; import za.co.neroland.nerospace.client.MeteorTrackerHud; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; @@ -48,7 +49,10 @@ public void register(EntityType type, EntityRend } }); - // Meteor Tracker readout (action bar) — counterpart to NeoForge's ClientTickEvent.Post. - ClientTickEvents.END_CLIENT_TICK.register(mc -> MeteorTrackerHud.tick()); + // Meteor Tracker readout + oxygen-field visuals — counterpart to NeoForge's ClientTickEvent.Post. + ClientTickEvents.END_CLIENT_TICK.register(mc -> { + MeteorTrackerHud.tick(); + ClientOxygenVisuals.tick(); + }); } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index eef366c..2450a73 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -16,6 +16,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.client.ClientEntityRenderers; +import za.co.neroland.nerospace.client.ClientOxygenVisuals; import za.co.neroland.nerospace.client.MeteorTrackerHud; import za.co.neroland.nerospace.client.CombustionGeneratorScreen; import za.co.neroland.nerospace.client.NerosiumGrinderScreen; @@ -40,8 +41,11 @@ public static void init(IEventBus modEventBus) { modEventBus.addListener(NeoForgeClientSetup::onRegisterScreens); modEventBus.addListener(NeoForgeClientSetup::onRegisterFluidModels); modEventBus.addListener(NeoForgeClientSetup::onRegisterEntityRenderers); - // Meteor Tracker readout (action bar) — game-bus client tick (counterpart to Fabric's END_CLIENT_TICK). - NeoForge.EVENT_BUS.addListener((ClientTickEvent.Post event) -> MeteorTrackerHud.tick()); + // Meteor Tracker readout + oxygen-field visuals — game-bus client tick (counterpart to Fabric's END_CLIENT_TICK). + NeoForge.EVENT_BUS.addListener((ClientTickEvent.Post event) -> { + MeteorTrackerHud.tick(); + ClientOxygenVisuals.tick(); + }); } private static void onRegisterEntityRenderers(EntityRenderersEvent.RegisterRenderers event) { From 51d228994306022ec5b0d3c2c0336fab513d6008 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:37:36 +0200 Subject: [PATCH 56/82] Add Sentry telemetry, config and platform seams Introduce Sentry-based telemetry and a minimal cross-loader config and platform seams. - Add common telemetry classes: NerospaceTelemetry (PII-scrubbing, nerospace-only filter, de-dup, per-session cap) and SentryLogAppender. - Add NerospaceConfig to read/write config/nerospace.properties (telemetryEnabled, default ON, opt-out). - Extend IPlatformHelper with getConfigDir() and getModVersion(); implement these in FabricPlatformHelper and NeoForgePlatformHelper. - Wire NerospaceTelemetry.init() in Fabric and NeoForge bootstrap classes (disabled in dev env). - Update Gradle: compileOnly sentry in common, include/embed sentry in Fabric, jarJar embed in NeoForge. - Update MULTILOADER_PORT_CHECKLIST.md to mark telemetry work done and note runtime verification required. --- docs/MULTILOADER_PORT_CHECKLIST.md | 35 ++- multiloader/common/build.gradle | 8 + .../nerospace/config/NerospaceConfig.java | 80 +++++ .../nerospace/platform/IPlatformHelper.java | 6 + .../telemetry/NerospaceTelemetry.java | 273 ++++++++++++++++++ .../telemetry/SentryLogAppender.java | 42 +++ multiloader/fabric/build.gradle | 5 + .../nerospace/fabric/NerospaceFabric.java | 3 + .../platform/FabricPlatformHelper.java | 13 + multiloader/neoforge/build.gradle | 6 + .../nerospace/neoforge/NerospaceNeoForge.java | 3 + .../platform/NeoForgePlatformHelper.java | 14 + 12 files changed, 476 insertions(+), 12 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/NerospaceTelemetry.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/SentryLogAppender.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index a15d0d4..c0fb8ef 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,16 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~190 classes ported, ~74 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~194 classes ported, ~70 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — telemetry (Sentry) ported.** All 4 cells green; Sentry bundled per-loader (NeoForge +> jarJar + Fabric include, both tasks green). `telemetry/{NerospaceTelemetry, SentryLogAppender}` + +> `config/NerospaceConfig` (opt-out toggle, **default ON** per user decision) + `IPlatformHelper.getConfigDir/ +> getModVersion` seam. PII scrubbing + nerospace-only filter + de-dup/cap intact; production-gated (off in dev). +> ⚠️ Runtime-unverified (dev-gated + mount lag) — confirm on a shipped jar. Closes the last pre-existing pending +> task. + > **2026-06-21 update — oxygen field client visuals.** All 4 cells green. Added `network/OxygenFieldSyncPayload` > + `client/{ClientOxygenField, ClientOxygenVisuals}`; the field now syncs to nearby clients and renders as > drifting GLOW particles + a boundary sound — the breathable volume is finally visible. 2nd networking-seam @@ -394,17 +401,21 @@ checked by a headless build). --- -## 📡 Sentry / telemetry (`telemetry/` 3 + `sentry_test` block) — **POPIA/GDPR-sensitive** -- [ ] `NerospaceTelemetry` — the Sentry client: captures Nerospace exceptions/crashes, with **PII - scrubbing, de-dup, rate-limiting** and an active/opt-in gate. -- [ ] `SentryLogAppender` — Log4j2 appender that selects ERROR/FATAL events touching Nerospace code. -- [ ] `SentryTestBlock` — a debug block that forces a captured error. - -**Compliance gate (per project preference — POPIA + GDPR):** before porting, confirm telemetry is -**opt-in** (off by default), transmits **no personal data** (scrub usernames, UUIDs, IPs, file paths, -world names), documents what's collected + retention, and offers a clear off switch. Verify the Sentry -DSN/endpoint and data-processing terms meet POPIA (SA) + GDPR (EU). Re-confirm the scrubbing covers -log paths like `C:\Users\\...`. Do **not** port as-is until this is signed off. +## 📡 Sentry / telemetry (`telemetry/`) — **POPIA/GDPR-sensitive** — DONE (4 cells green) +- [x] `telemetry/NerospaceTelemetry` — the Sentry client: captures Nerospace exceptions/crashes, with + **PII scrubbing** (no IP/identity/hostname; OS-account names scrubbed from file paths via the `USER_PATH` + regex incl. `C:\Users\\...`), **nerospace-only `beforeSend` filter**, **de-dup + 10/session cap**. + Parameterised off `Services.PLATFORM` (mod version, loader name, dist) instead of FML. +- [x] `telemetry/SentryLogAppender` — Log4j2 appender selecting ERROR/FATAL events touching Nerospace code. +- [x] `config/NerospaceConfig` — minimal properties config (`config/nerospace.properties`); **`telemetryEnabled` + default ON, opt-out** (user decision 2026-06-21). Config-dir via new `IPlatformHelper.getConfigDir()` seam. +- [x] **Sentry SDK bundled per-loader** — common `compileOnly`, NeoForge `jarJar`, Fabric Loom `include` + (both bundling tasks ran green). `NerospaceTelemetry.init()` called at each loader's bootstrap; **only + initialises in a production (non-dev) environment**. +- [ ] **Deferred**: `SentryTestBlock` (debug block) — minor dev tool. +- ⚠️ **Runtime-verify on a shipped build**: the 4-cell compile + the jarJar/include tasks are green, but + Sentry initialisation, the nerospace-only filter, and path scrubbing have NOT been runtime-tested here + (dev-gated + sandbox mount lag). Confirm on a production jar before relying on it. DSN = root's EU ingest. --- diff --git a/multiloader/common/build.gradle b/multiloader/common/build.gradle index 46c4075..884891c 100644 --- a/multiloader/common/build.gradle +++ b/multiloader/common/build.gradle @@ -18,3 +18,11 @@ neoForge { accessTransformers.from(at.absolutePath) } } + +dependencies { + // Sentry error-reporting SDK (telemetry/NerospaceTelemetry). The telemetry core lives in common, so + // it compiles against the API here; each loader BUNDLES the jar (NeoForge JarJar / Fabric include). + // The core 'sentry' artifact is dependency-free. Privacy: opt-out config + nerospace-only filter + + // PII scrubbing (see PRIVACY.md). + compileOnly 'io.sentry:sentry:8.42.0' +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java new file mode 100644 index 0000000..a45307b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -0,0 +1,80 @@ +package za.co.neroland.nerospace.config; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.platform.Services; + +/** + * Minimal cross-loader config — a single {@code config/nerospace.properties} file read once at mod + * init. The full root config (NeoForge {@code ModConfigSpec}, ~50 keys) is deferred; this exists so + * the disclosed telemetry has a real, user-editable opt-out toggle (CurseForge moderation + + * POPIA/GDPR). The file is created with documented defaults on first run. + * + *

Loader-agnostic: the config directory comes through the {@link Services#PLATFORM} seam.

+ */ +public final class NerospaceConfig { + + private static final String FILE_NAME = "nerospace.properties"; + private static final String KEY_TELEMETRY = "telemetryEnabled"; + + /** Anonymous crash reporting (Sentry, EU) is ON by default; players opt out by setting this false. */ + private static volatile boolean telemetryEnabled = true; + private static volatile boolean loaded; + + private NerospaceConfig() { + } + + public static boolean isTelemetryEnabled() { + return telemetryEnabled; + } + + /** Reads (creating with defaults if absent) the config file. Safe to call once at mod init. */ + public static synchronized void load() { + if (loaded) { + return; + } + loaded = true; + Path file; + try { + file = Services.PLATFORM.getConfigDir().resolve(FILE_NAME); + } catch (RuntimeException e) { + return; // no config dir available — keep defaults + } + + Properties props = new Properties(); + if (Files.exists(file)) { + try (InputStream in = Files.newInputStream(file)) { + props.load(in); + telemetryEnabled = Boolean.parseBoolean( + props.getProperty(KEY_TELEMETRY, Boolean.toString(telemetryEnabled)).trim()); + } catch (IOException e) { + NerospaceCommon.LOGGER.warn("[Nerospace] Could not read {}; using defaults.", FILE_NAME, e); + } + } else { + write(file); + } + } + + /** Writes the default config file with an explanatory comment (best-effort). */ + private static void write(Path file) { + Properties props = new Properties(); + props.setProperty(KEY_TELEMETRY, Boolean.toString(telemetryEnabled)); + try { + Files.createDirectories(file.getParent()); + try (OutputStream out = Files.newOutputStream(file)) { + props.store(out, "Nerospace config. telemetryEnabled: send anonymous, Nerospace-only " + + "crash reports (Sentry, EU servers) — stack trace + mod/MC/loader/OS/Java " + + "versions only; no IP, username, UUID, world data or chat; file paths are " + + "scrubbed of your account name. Set to false to opt out. See PRIVACY.md."); + } + } catch (IOException e) { + NerospaceCommon.LOGGER.warn("[Nerospace] Could not write {}; using defaults.", FILE_NAME, e); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java index 892b9da..dd6ce1f 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java @@ -26,6 +26,12 @@ public interface IPlatformHelper { /** True on the physical client (renderers, screens, HUD available). */ boolean isClient(); + /** The loader config directory (NeoForge {@code FMLPaths.CONFIGDIR}, Fabric {@code getConfigDir}). */ + java.nio.file.Path getConfigDir(); + + /** This mod's version string (for telemetry release tags), or "unknown" if unavailable. */ + String getModVersion(); + // --- Per-player oxygen (data-attachment seam) --------------------------- // NeoForge backs this with an AttachmentType registered via DeferredRegister; Fabric with the // data-attachment API. The value defaults to {@code OxygenManager.OXYGEN_MAX} and persists. diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/NerospaceTelemetry.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/NerospaceTelemetry.java new file mode 100644 index 0000000..de66512 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/NerospaceTelemetry.java @@ -0,0 +1,273 @@ +package za.co.neroland.nerospace.telemetry; + +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.LogManager; + +import io.sentry.Sentry; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.protocol.Message; +import io.sentry.protocol.SentryException; +import io.sentry.protocol.SentryStackFrame; +import io.sentry.protocol.SentryStackTrace; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.config.NerospaceConfig; +import za.co.neroland.nerospace.platform.Services; + +/** + * Crash/error reporting for Nerospace via Sentry (EU ingest), built to satisfy both the CurseForge + * moderation rule (external analytics must be disclosed and opt-out-able) and POPIA/GDPR + * data-minimisation: + * + *
    + *
  • Opt-out: gated on {@code telemetryEnabled} in {@link NerospaceConfig} (default ON, + * disclosed). Set it false to stop reporting (takes effect on restart).
  • + *
  • Nerospace errors only: {@code beforeSend} drops any event whose stack trace does not + * touch {@code za.co.neroland.nerospace}.
  • + *
  • No personal data: {@code sendDefaultPii=false} (no IP), no server/host name, no user + * identity, and OS-account names are scrubbed from file paths. Remaining payload: stack trace, + * mod/MC/loader/OS/Java versions.
  • + *
  • Bounded volume: per-session de-duplication plus a hard cap of + * {@value #MAX_EVENTS_PER_SESSION} events per game session.
  • + *
+ * + *

Cross-loader port note: the root drove start/stop from NeoForge {@code ModConfigEvent} and read + * FML for version/dist; here {@link #init()} is called once per loader at bootstrap and reads loader + * facts through {@link Services#PLATFORM}. Only initialises in a production (non-dev) environment. + * Full disclosure text: {@code PRIVACY.md}.

+ */ +public final class NerospaceTelemetry { + + /** Sentry DSN — a public client key (write-only ingest), safe to ship in the jar. */ + private static final String DSN = + "https://05493ad141e6ed1526488f84618ce63d@o4511183823241216.ingest.de.sentry.io/4511509478834256"; + + /** Stack traces must contain this package for an event to be sent. */ + private static final String PACKAGE_MARKER = "za.co.neroland.nerospace"; + + /** Hard cap on events per game session (data minimisation + noise control). */ + private static final int MAX_EVENTS_PER_SESSION = 10; + + /** Masks OS-account names in Windows/macOS/Linux home-directory paths. */ + private static final Pattern USER_PATH = + Pattern.compile("(?i)(?:[A-Z]:)?[/\\\\](?:Users|home)[/\\\\][^/\\\\\\s:;,'\"]+"); + + private static volatile boolean active; + private static final AtomicInteger eventsSent = new AtomicInteger(); + private static final Set seenFingerprints = ConcurrentHashMap.newKeySet(); + private static SentryLogAppender appender; + + private NerospaceTelemetry() { + } + + /** + * Called once per loader at bootstrap. Starts reporting iff the player has not opted out and we are + * in a shipped (non-development) environment — so dev runs never report, and nothing is sent before + * the player's choice is read from {@link NerospaceConfig}. + */ + public static void init() { + NerospaceConfig.load(); + if (!NerospaceConfig.isTelemetryEnabled() || Services.PLATFORM.isDevelopmentEnvironment()) { + return; + } + start(); + } + + private static synchronized void start() { + if (active) { + return; + } + String modVersion = Services.PLATFORM.getModVersion(); + Sentry.init(options -> { + options.setDsn(DSN); + options.setRelease("nerospace@" + modVersion); + options.setEnvironment(environmentOf(modVersion)); + // POPIA/GDPR: never store the sender's IP address or identity. + options.setSendDefaultPii(false); + // The machine's hostname is identifying; never attach it. + options.setAttachServerName(false); + options.setEnableUncaughtExceptionHandler(true); + options.setBeforeSend((event, hint) -> filterAndScrub(event)); + }); + Sentry.configureScope(scope -> { + scope.setTag("loader", Services.PLATFORM.getPlatformName().toLowerCase(Locale.ROOT)); + scope.setTag("dist", Services.PLATFORM.isClient() ? "client" : "dedicated_server"); + scope.setTag("runtime", "production"); + }); + if (appender == null) { + appender = new SentryLogAppender(); + appender.start(); + ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(appender); + } + active = true; + NerospaceCommon.LOGGER.info( + "[Nerospace] Telemetry enabled (anonymous error reports, EU servers; opt out via " + + "telemetryEnabled=false in config/nerospace.properties)."); + } + + /** Maps the mod version's release channel to a Sentry environment. */ + private static String environmentOf(String version) { + String v = version.toLowerCase(Locale.ROOT); + if (v.contains("-alpha")) { + return "alpha"; + } + if (v.contains("-beta")) { + return "beta"; + } + return "production"; + } + + static boolean isActive() { + return active; + } + + /** True if any frame of the throwable (or its causes/suppressed) is Nerospace code. */ + static boolean touchesNerospace(Throwable t) { + int depth = 0; + while (t != null && depth++ < 16) { + for (StackTraceElement el : t.getStackTrace()) { + if (el.getClassName().startsWith(PACKAGE_MARKER)) { + return true; + } + } + for (Throwable s : t.getSuppressed()) { + if (touchesNerospace(s)) { + return true; + } + } + t = t.getCause(); + } + return false; + } + + /** Capture an exception already known to touch Nerospace code. */ + static void capture(Throwable t) { + if (!active || t == null) { + return; + } + Sentry.captureException(t); + } + + /** Capture a (scrubbed, truncated) FATAL log line that names Nerospace without a throwable. */ + static void captureMessage(String message) { + if (!active) { + return; + } + String scrubbed = scrub(message); + if (scrubbed.length() > 4000) { + scrubbed = scrubbed.substring(0, 4000) + "…[truncated]"; + } + SentryEvent event = new SentryEvent(); + event.setLevel(SentryLevel.FATAL); + Message msg = new Message(); + msg.setFormatted(scrubbed); + event.setMessage(msg); + Sentry.captureEvent(event); + } + + /** + * The single privacy/noise gate every outgoing event passes through: keep only Nerospace-related + * events, de-duplicate, rate-limit, and scrub PII. Returning {@code null} drops the event. + */ + private static SentryEvent filterAndScrub(SentryEvent event) { + if (!isNerospaceRelated(event)) { + return null; + } + String fingerprint = fingerprintOf(event); + if (!seenFingerprints.add(fingerprint)) { + return null; // already reported this session + } + if (eventsSent.incrementAndGet() > MAX_EVENTS_PER_SESSION) { + return null; + } + // POPIA/GDPR scrubbing: no user identity, no hostname, no OS-account names in paths. + event.setUser(null); + event.setServerName(null); + List scrubExceptions = event.getExceptions(); + if (scrubExceptions != null) { + for (SentryException ex : scrubExceptions) { + String value = ex.getValue(); + if (value != null) { + ex.setValue(scrub(value)); + } + SentryStackTrace st = ex.getStacktrace(); + List frames = st == null ? null : st.getFrames(); + if (frames != null) { + for (SentryStackFrame frame : frames) { + frame.setAbsPath(null); + } + } + } + } + Message message = event.getMessage(); + if (message != null && message.getFormatted() != null) { + message.setFormatted(scrub(message.getFormatted())); + } + return event; + } + + private static boolean isNerospaceRelated(SentryEvent event) { + Throwable t = event.getThrowable(); + if (t != null && touchesNerospace(t)) { + return true; + } + List exceptions = event.getExceptions(); + if (exceptions != null) { + for (SentryException ex : exceptions) { + SentryStackTrace st = ex.getStacktrace(); + List frames = st == null ? null : st.getFrames(); + if (frames == null) { + continue; + } + for (SentryStackFrame frame : frames) { + String module = frame.getModule(); + if (module != null && module.startsWith(PACKAGE_MARKER)) { + return true; + } + } + } + } + Message message = event.getMessage(); + String formatted = message == null ? null : message.getFormatted(); + return formatted != null && formatted.contains(PACKAGE_MARKER); + } + + private static String fingerprintOf(SentryEvent event) { + StringBuilder sb = new StringBuilder(); + List exceptions = event.getExceptions(); + Message message = event.getMessage(); + if (exceptions != null) { + for (SentryException ex : exceptions) { + sb.append(ex.getType()).append('|'); + SentryStackTrace st = ex.getStacktrace(); + List frames = st == null ? null : st.getFrames(); + if (frames != null) { + for (SentryStackFrame frame : frames) { + String module = frame.getModule(); + if (module != null && module.startsWith(PACKAGE_MARKER)) { + sb.append(module).append(':').append(frame.getLineno()).append('|'); + } + } + } + } + } else if (message != null) { + String formatted = message.getFormatted(); + if (formatted != null) { + sb.append(formatted, 0, Math.min(200, formatted.length())); + } + } + return sb.toString(); + } + + /** Replaces home-directory paths (which contain the OS account name) with a neutral marker. */ + static String scrub(String text) { + return USER_PATH.matcher(text).replaceAll("/~"); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/SentryLogAppender.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/SentryLogAppender.java new file mode 100644 index 0000000..c8fc910 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/telemetry/SentryLogAppender.java @@ -0,0 +1,42 @@ +package za.co.neroland.nerospace.telemetry; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Property; + +/** + * Log4j2 appender that feeds {@link NerospaceTelemetry}. Minecraft routes essentially every failure + * through log4j — handled errors, event-bus listener exceptions, and the crash report itself — so + * listening on the root logger catches Nerospace failures without mixins. Filtering (Nerospace-only), + * de-dup, rate-limiting and PII scrubbing all happen in {@link NerospaceTelemetry}; this only selects + * candidate log events. + */ +final class SentryLogAppender extends AbstractAppender { + + SentryLogAppender() { + super("NerospaceSentry", null, null, false, Property.EMPTY_ARRAY); + } + + @Override + public void append(LogEvent event) { + if (!NerospaceTelemetry.isActive()) { + return; + } + Level level = event.getLevel(); + if (!level.isMoreSpecificThan(Level.ERROR)) { + return; + } + Throwable thrown = event.getThrown(); + if (thrown != null) { + if (NerospaceTelemetry.touchesNerospace(thrown)) { + NerospaceTelemetry.capture(thrown); + } + } else if (level == Level.FATAL) { + String message = event.getMessage() == null ? null : event.getMessage().getFormattedMessage(); + if (message != null && message.contains("za.co.neroland.nerospace")) { + NerospaceTelemetry.captureMessage(message); + } + } + } +} diff --git a/multiloader/fabric/build.gradle b/multiloader/fabric/build.gradle index cd9b779..82c9de2 100644 --- a/multiloader/fabric/build.gradle +++ b/multiloader/fabric/build.gradle @@ -20,6 +20,11 @@ dependencies { if (fabricApi != null) { implementation "net.fabricmc.fabric-api:fabric-api:${fabricApi}" } + + // Sentry SDK (telemetry/NerospaceTelemetry): compile against it + embed it as a jar-in-jar so the + // shipped Fabric jar carries it. Counterpart to the NeoForge JarJar bundling. + implementation 'io.sentry:sentry:8.42.0' + include 'io.sentry:sentry:8.42.0' } // Expand fabric.mod.json from src/main/templates into a generated resources dir. diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 2b8218f..dd411b3 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -32,6 +32,7 @@ import za.co.neroland.nerospace.world.TerraformManager; import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; +import za.co.neroland.nerospace.telemetry.NerospaceTelemetry; import za.co.neroland.nerospace.world.OxygenManager; /** @@ -64,6 +65,8 @@ public final class NerospaceFabric implements ModInitializer { public void onInitialize() { NerospaceCommon.LOGGER.info("[Nerospace] Fabric bootstrap"); NerospaceCommon.init(); + // Anonymous, Nerospace-only crash reporting (opt-out via config/nerospace.properties; off in dev). + NerospaceTelemetry.init(); // Creative-tab contents are defined once by the cross-loader ModCreativeTab (a dedicated // Nerospace tab), so no per-loader creative-tab injection is needed here. diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java index f9af215..29ef4ca 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java @@ -5,6 +5,7 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.chunk.LevelChunk; +import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.fabric.FabricAttachments; /** @@ -33,6 +34,18 @@ public boolean isClient() { return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; } + @Override + public java.nio.file.Path getConfigDir() { + return FabricLoader.getInstance().getConfigDir(); + } + + @Override + public String getModVersion() { + return FabricLoader.getInstance().getModContainer(NerospaceCommon.MOD_ID) + .map(c -> c.getMetadata().getVersion().getFriendlyString()) + .orElse("unknown"); + } + @Override public int getOxygen(Player player) { return player.getAttachedOrCreate(FabricAttachments.OXYGEN); diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle index 8eb0fbf..312f902 100644 --- a/multiloader/neoforge/build.gradle +++ b/multiloader/neoforge/build.gradle @@ -20,6 +20,12 @@ repositories { sourceSets.main.java.srcDir rootProject.ext.commonJava sourceSets.main.resources.srcDir rootProject.ext.commonResources +dependencies { + // Sentry SDK embedded via NeoForge JarJar so players need no extra download (JarJar dedupes if + // another mod also ships Sentry). Adds it to the compile classpath AND the mod jar. + jarJar(implementation('io.sentry:sentry:8.42.0')) +} + // Expand neoforge.mods.toml from src/main/templates into a generated resources dir // (same pattern as the repo-root build). The template is NOT under src/main/resources, // so the IDE never copies a RAW (token-laden) mods.toml into bin/main; diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 74edc9f..abb1704 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -22,6 +22,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.meteor.MeteorEvents; +import za.co.neroland.nerospace.telemetry.NerospaceTelemetry; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; import za.co.neroland.nerospace.world.OxygenFieldEvents; import za.co.neroland.nerospace.registry.ModEntityAttributes; @@ -42,6 +43,8 @@ public final class NerospaceNeoForge { public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NerospaceCommon.LOGGER.info("[Nerospace] NeoForge bootstrap"); NerospaceCommon.init(); + // Anonymous, Nerospace-only crash reporting (opt-out via config/nerospace.properties; off in dev). + NerospaceTelemetry.init(); NeoForgeFluidFactory.registerFluidTypes(modEventBus); NeoForgeRegistrationFactory.registerAll(modEventBus); NeoForgeCapabilities.register(modEventBus); diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java index 3cb2e33..2db61ac 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -2,10 +2,12 @@ import net.neoforged.fml.ModList; import net.neoforged.fml.loading.FMLEnvironment; +import net.neoforged.fml.loading.FMLPaths; import net.neoforged.api.distmarker.Dist; import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.chunk.LevelChunk; +import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.neoforge.NeoForgeAttachments; /** @@ -36,6 +38,18 @@ public boolean isClient() { return FMLEnvironment.getDist() == Dist.CLIENT; } + @Override + public java.nio.file.Path getConfigDir() { + return FMLPaths.CONFIGDIR.get(); + } + + @Override + public String getModVersion() { + return ModList.get().getModContainerById(NerospaceCommon.MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()) + .orElse("unknown"); + } + @Override public int getOxygen(Player player) { return player.getData(NeoForgeAttachments.OXYGEN.get()); From c028ac56c579ac7c04d58c72b6e3339a6500d7c5 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:52:22 +0200 Subject: [PATCH 57/82] Add Fabric launch configs and VSCode workspace Add Eclipse launch configurations for Fabric (fabric_client.launch and fabric_server.launch) to run client and server dev environments, including MAIN_TYPE and VM arguments. Also add nerospace-multiloader.code-workspace to configure VS Code to treat multiloader/ as the Gradle root and enable automatic Gradle/Java import (notes Java target is JDK 25). VM argument paths in the launch files are machine-specific and may need adjustment. --- multiloader/fabric/fabric_client.launch | 20 ++++++++++++++++++++ multiloader/fabric/fabric_server.launch | 20 ++++++++++++++++++++ nerospace-multiloader.code-workspace | 18 ++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 multiloader/fabric/fabric_client.launch create mode 100644 multiloader/fabric/fabric_server.launch create mode 100644 nerospace-multiloader.code-workspace diff --git a/multiloader/fabric/fabric_client.launch b/multiloader/fabric/fabric_client.launch new file mode 100644 index 0000000..d3ef258 --- /dev/null +++ b/multiloader/fabric/fabric_client.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/multiloader/fabric/fabric_server.launch b/multiloader/fabric/fabric_server.launch new file mode 100644 index 0000000..4cf489a --- /dev/null +++ b/multiloader/fabric/fabric_server.launch @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/nerospace-multiloader.code-workspace b/nerospace-multiloader.code-workspace new file mode 100644 index 0000000..9f329be --- /dev/null +++ b/nerospace-multiloader.code-workspace @@ -0,0 +1,18 @@ +{ + "folders": [ + { + "name": "nerospace-multiloader", + "path": "multiloader" + } + ], + "settings": { + // Treat multiloader/ as the Gradle build root and let the Java extension import it + // via the module wrapper (Gradle 9.5.1), re-importing automatically when build.gradle changes. + "java.import.gradle.enabled": true, + "java.import.gradle.wrapper.enabled": true, + "java.configuration.updateBuildConfiguration": "automatic", + // The build targets Java 25 — VSCode's Java extension must run on / have a JDK 25 registered + // in "java.configuration.runtimes" (set it in your USER settings, pointing at your JDK 25 path). + "java.compile.nullAnalysis.mode": "automatic" + } +} From 81546c4e9a0dbaff3ae51e953e9a13e870d9dfac Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:54:07 +0200 Subject: [PATCH 58/82] Add eclipse plugin and clean Fabric launch files Apply the 'eclipse' plugin to all subprojects to generate .project/.classpath/.settings files (improves VSCode Java classpath resolution and avoids flaky Gradle imports). Also tidy Fabric client/server .launch files: collapse empty environmentVariables to self-closing tags, add ATTR_ATTR_USE_ARGFILE boolean, remove redundant CLASSPATH_PROVIDER entries and duplicate ATTR_ATTR_USE_ARGFILE attributes to make the launch configs more consistent. --- multiloader/build.gradle | 6 ++++++ multiloader/fabric/fabric_client.launch | 7 ++----- multiloader/fabric/fabric_server.launch | 7 ++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/multiloader/build.gradle b/multiloader/build.gradle index 3b55522..1f3339b 100644 --- a/multiloader/build.gradle +++ b/multiloader/build.gradle @@ -12,6 +12,12 @@ allprojects { subprojects { apply plugin: 'java' + // Generate static Eclipse project files (.project/.classpath/.settings) in every module via + // `./gradlew eclipse`. VSCode's Java extension reads these directly and resolves the full classpath, + // which sidesteps the flaky live Buildship/Loom Gradle import (the cause of the "cannot find + // java.lang.Object" / "not a valid java project" cascade). Fabric already applied this in its own + // script; applying here too is idempotent and adds it to common + neoforge. + apply plugin: 'eclipse' java { toolchain.languageVersion = JavaLanguageVersion.of(25) diff --git a/multiloader/fabric/fabric_client.launch b/multiloader/fabric/fabric_client.launch index d3ef258..6ca5b71 100644 --- a/multiloader/fabric/fabric_client.launch +++ b/multiloader/fabric/fabric_client.launch @@ -6,15 +6,12 @@ - - - + + - - diff --git a/multiloader/fabric/fabric_server.launch b/multiloader/fabric/fabric_server.launch index 4cf489a..87e524b 100644 --- a/multiloader/fabric/fabric_server.launch +++ b/multiloader/fabric/fabric_server.launch @@ -6,15 +6,12 @@ - - - + + - - From b25050932be49acf50e1dd8f1aa951b2b52b0c9d Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:55:38 +0200 Subject: [PATCH 59/82] Disable Buildship Gradle import for VSCode Strip Buildship nature from generated .project and disable VSCode's live Gradle import so modules are opened as static Eclipse Java projects. Adds an eclipse configuration in multiloader/build.gradle to remove Buildship natures/build commands during `./gradlew eclipse`, updates nerospace-multiloader.code-workspace to disable the Gradle import and document the recommended workflow (run `./gradlew eclipse -Pminecraft_version=26.2` to refresh classpaths and ensure a JDK 25 runtime is registered). Also includes a minor whitespace tweak in the Block-of-Glacite wiki page. --- multiloader/build.gradle | 15 +++++++++++++++ nerospace-multiloader.code-workspace | 18 ++++++++++-------- wiki/Block-of-Glacite.md | 1 + 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/multiloader/build.gradle b/multiloader/build.gradle index 1f3339b..f6adc4a 100644 --- a/multiloader/build.gradle +++ b/multiloader/build.gradle @@ -28,6 +28,21 @@ subprojects { options.release = 25 } + // Strip Loom's Buildship/Gradle nature+builder from the generated .project so VSCode imports every + // module as a plain static Eclipse Java project (via .classpath) rather than demanding a live + // Buildship/Gradle import — the import that fails here ("Missing buildship prefs" / "not a valid + // java project"). No-op for the moddev modules (they have no Buildship nature). + eclipse { + project { + file { + whenMerged { proj -> + proj.natures.removeAll { it.startsWith('org.eclipse.buildship') } + proj.buildCommands.removeAll { it.name.startsWith('org.eclipse.buildship') } + } + } + } + } + repositories { mavenCentral() maven { url 'https://maven.fabricmc.net/' } diff --git a/nerospace-multiloader.code-workspace b/nerospace-multiloader.code-workspace index 9f329be..38945ce 100644 --- a/nerospace-multiloader.code-workspace +++ b/nerospace-multiloader.code-workspace @@ -6,13 +6,15 @@ } ], "settings": { - // Treat multiloader/ as the Gradle build root and let the Java extension import it - // via the module wrapper (Gradle 9.5.1), re-importing automatically when build.gradle changes. - "java.import.gradle.enabled": true, - "java.import.gradle.wrapper.enabled": true, - "java.configuration.updateBuildConfiguration": "automatic", - // The build targets Java 25 — VSCode's Java extension must run on / have a JDK 25 registered - // in "java.configuration.runtimes" (set it in your USER settings, pointing at your JDK 25 path). - "java.compile.nullAnalysis.mode": "automatic" + // Import the three modules as the static Eclipse Java projects generated by `./gradlew eclipse` + // (each has a full .classpath). The live Buildship/Loom Gradle import is DISABLED because it + // fails here ("Missing buildship prefs" / "fabric not a valid java project" / "cannot find + // java.lang.Object"). Re-run `./gradlew eclipse -Pminecraft_version=26.2` after changing + // dependencies to refresh the classpaths. + "java.import.gradle.enabled": false, + "java.compile.nullAnalysis.mode": "automatic", + // The build targets Java 25 — VSCode's Java extension must have a JDK 25 registered in + // "java.configuration.runtimes" (set it in your USER settings, pointing at your JDK 25 path). + "java.configuration.maven.notCoveredPluginExecutionSeverity": "ignore" } } diff --git a/wiki/Block-of-Glacite.md b/wiki/Block-of-Glacite.md index 26298f4..5a75aa4 100644 --- a/wiki/Block-of-Glacite.md +++ b/wiki/Block-of-Glacite.md @@ -12,5 +12,6 @@ A pale, ice-blue storage block of the Glacira crystal. - **Unpack:** craft the block alone to get **9 Glacite** back. ## Details + - ID: `nerospace:glacite_block` - Tool: pickaxe, iron tier · Drops: itself From 45a8dbe85ef815cc086f209183c04bb18d9d5343 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 12:57:49 +0200 Subject: [PATCH 60/82] Create .gitignore --- multiloader/.gitignore | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 multiloader/.gitignore diff --git a/multiloader/.gitignore b/multiloader/.gitignore new file mode 100644 index 0000000..f2ffb8d --- /dev/null +++ b/multiloader/.gitignore @@ -0,0 +1,25 @@ +# Gradle +.gradle/ +build/ +**/build/ + +# Loom / ModDevGradle run directories +**/runs/ +**/run/ + +# Generated IDE project files — machine-specific (absolute ~/.gradle cache paths). +# Regenerate locally with: ./gradlew eclipse -Pminecraft_version=26.2 +.project +.classpath +.settings/ +**/.project +**/.classpath +**/.settings/ +bin/ +**/bin/ + +# IntelliJ +.idea/ +*.iml + +# Keep the committed VSCode run configs (.vscode/launch.json, tasks.json) — NOT ignored. From 2d71740601ed17824f6bcca2e36ded58776d744b Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:19:50 +0200 Subject: [PATCH 61/82] Add per-face pipe config, filters & upgrades Introduce per-face, per-resource pipe configuration and the pipe tool items. Adds PipeIoMode and PipeResourceType enums, ConfiguratorItem, PipeFilterItem and PipeUpgradeItem, plus item assets/models/textures and language keys. Extends UniversalPipeBlockEntity to store per-face modes, per-face item filters, speed/capacity upgrade counts, apply speed multipliers to IO, and honour canPull/canPush/OFF; adds filtered item transfer and upgrade install/uninstall (dropping items). Updates UniversalPipeBlock to pop upgrades on sneak+empty-hand, registers the new items in ModItems, and persists modes/filters/upgrades (packed long + fields). Also updates docs checklist to reflect slice A completion. --- docs/MULTILOADER_PORT_CHECKLIST.md | 36 +++- .../nerospace/item/ConfiguratorItem.java | 95 +++++++++ .../nerospace/item/PipeFilterItem.java | 78 +++++++ .../nerospace/item/PipeUpgradeItem.java | 61 ++++++ .../neroland/nerospace/pipe/PipeIoMode.java | 46 +++++ .../nerospace/pipe/PipeResourceType.java | 42 ++++ .../nerospace/pipe/UniversalPipeBlock.java | 14 +- .../pipe/UniversalPipeBlockEntity.java | 193 +++++++++++++++++- .../neroland/nerospace/registry/ModItems.java | 16 +- .../nerospace/items/capacity_upgrade.json | 6 + .../assets/nerospace/items/configurator.json | 6 + .../assets/nerospace/items/pipe_filter.json | 6 + .../assets/nerospace/items/speed_upgrade.json | 6 + .../assets/nerospace/lang/en_us.json | 28 ++- .../models/item/capacity_upgrade.json | 6 + .../nerospace/models/item/configurator.json | 6 + .../nerospace/models/item/pipe_filter.json | 6 + .../nerospace/models/item/speed_upgrade.json | 6 + .../textures/item/capacity_upgrade.png | Bin 0 -> 163 bytes .../nerospace/textures/item/configurator.png | Bin 0 -> 110 bytes .../nerospace/textures/item/pipe_filter.png | Bin 0 -> 167 bytes .../nerospace/textures/item/speed_upgrade.png | Bin 0 -> 182 bytes 22 files changed, 637 insertions(+), 20 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeFilterItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeUpgradeItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeIoMode.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeResourceType.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/capacity_upgrade.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/configurator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/pipe_filter.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/speed_upgrade.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/capacity_upgrade.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/configurator.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/pipe_filter.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/speed_upgrade.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/capacity_upgrade.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/configurator.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/pipe_filter.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_upgrade.png diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index c0fb8ef..cda5068 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,23 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~194 classes ported, ~70 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~199 classes ported, ~65 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — advanced pipes slice A (per-face configuration layer).** All 4 cells green +> (compile on 26.1.2 + 26.2; full `:neoforge:build`+`:fabric:build` on 26.2). Added `pipe/PipeIoMode` +> + `pipe/PipeResourceType` (pure-vanilla enums) and the three pipe tools — `item/ConfiguratorItem` +> (cycle selected layer + cycle a face's I/O mode), `item/PipeFilterItem` (ItemResource→vanilla +> **ItemStack** filter), `item/PipeUpgradeItem` (speed/capacity ×2). Extended `UniversalPipeBlockEntity` +> with per-face×per-type `PipeIoMode` storage (packed long), per-face item filters, speed/capacity +> upgrade counts, and rewired the energy/gas/item relay to honour `canPull`/`canPush`/`OFF` + filters + +> the speed throughput multiplier; `UniversalPipeBlock` pops upgrades on sneak-empty-hand. Registered the +> 4 items (TOOLS tab) + copied 4 textures + item models/defs + 20 lang keys. **Pipes are now configurable +> per face.** Deferred to **slice B**: the full `PipeNetwork` graph + `TravellingItem` animation + +> `UniversalPipeRenderer`/`RenderState` (cosmetic, NeoForge-transfer-coupled) and the `PipeConfigScreen` +> GUI + `SetPipeModePayload` (needs a client-screen-open seam). Note: the multiloader relay still carries +> no FLUID layer, so the stored FLUID face-mode is inert until a fluid relay lands. + > **2026-06-21 update — telemetry (Sentry) ported.** All 4 cells green; Sentry bundled per-loader (NeoForge > jarJar + Fabric include, both tasks green). `telemetry/{NerospaceTelemetry, SentryLogAppender}` + > `config/NerospaceConfig` (opt-out toggle, **default ON** per user decision) + `IPlatformHelper.getConfigDir/ @@ -316,10 +330,18 @@ checked by a headless build). - [ ] `StarGuide`, `StarGuideProgress`, `StarGuideBlock`(+BE), `StarGuideMenu` + screen, hologram BER, `StarGuideBookItem`. Progression-tracking UI. -### Pipes — advanced (`pipe/` 4 + items + payload + renderer; basic pipe already ported) -- [ ] `PipeNetwork`, `TravellingItem`, `PipeIoMode`, `PipeResourceType`, `PipeFilterItem`, - `PipeUpgradeItem`, `SetPipeModePayload`, `UniversalPipeRenderer` (streams + travelling-item visuals, - per-side I/O modes, filters). Needs networking seam. +### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** +- [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla + enums); `item/{ConfiguratorItem, PipeFilterItem (vanilla ItemStack filter), PipeUpgradeItem ×2}`. + `UniversalPipeBlockEntity` extended with per-face×per-type modes (packed long) + per-face item filters + + speed/capacity upgrades; the energy/gas/item relay honours `canPull`/`canPush`/`OFF` + filters + speed + throughput; `UniversalPipeBlock` sneak-empty-hand pops upgrades. Items registered (TOOLS tab) + assets + + 20 lang keys. +- [ ] **Slice B (deferred) — graph + visuals + GUI.** `PipeNetwork` (591-line graph; NeoForge-transfer- + coupled), `TravellingItem` (animated stacks; ItemResource→ItemStack), `UniversalPipeRenderer` + + `UniversalPipeRenderState` (stream + travelling-item visuals), `PipeConfigScreen` + `PipeConfigOpenHandler` + + `network/SetPipeModePayload` (the per-face×per-type config GUI — needs a cross-loader client-screen-open + seam). Cosmetic / convenience; the slice-A in-world cycling already configures pipes fully. ### Machine modules / upgrades (`module/` 3) — **DONE (4 cells green)** - [x] `ModuleType`, `UpgradeModuleItem` (4 items: speed / efficiency / fortune / silk-touch) + `MachineModules` @@ -344,8 +366,8 @@ checked by a headless build). summon-only). Lazy `EntityType` supplier (vanilla `SpawnEggItem` binds too early); SPAWN_EGGS tab. - [x] `DestinationCompassItem` (×4: station/greenxertz/cindara/glacira) + `GreenxertzNavigatorItem` — creative-only travel devices; TOOLS_AND_UTILITIES tab. Assets + 17 lang keys copied. -- [ ] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` (depend on **advanced pipes**), - `StarGuideBookItem` (depends on **star guide**). +- [x] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` — DONE (advanced-pipes slice A; TOOLS tab). +- [ ] `StarGuideBookItem` (depends on **star guide**). - [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. ### Cross-cutting registries (`registry/`) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java new file mode 100644 index 0000000..8488800 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java @@ -0,0 +1,95 @@ +package za.co.neroland.nerospace.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.pipe.PipeIoMode; +import za.co.neroland.nerospace.pipe.PipeResourceType; +import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; +import za.co.neroland.nerospace.registry.ModDataComponents; + +/** + * The Configurator — the pipe network tool. It edits one {@link PipeResourceType} layer at a time (the + * "selected type", stored on the stack via {@link ModDataComponents#SELECTED_PIPE_TYPE}): + *
    + *
  • Sneak + right-click (in air or on a non-pipe block): cycle the selected resource type + * (energy → fluid → gas → item).
  • + *
  • Right-click a Universal Pipe face: cycle that face's I/O mode for the selected type + * (auto → in → out → off).
  • + *
+ * + *

Cross-loader port: already loader-agnostic in the standalone mod (vanilla item + data component); + * copied verbatim. The full per-face × per-type config GUI is the deferred client slice.

+ */ +public class ConfiguratorItem extends Item { + + public ConfiguratorItem(Properties properties) { + super(properties); + } + + private static PipeResourceType selectedType(ItemStack stack) { + int ordinal = stack.getOrDefault(ModDataComponents.SELECTED_PIPE_TYPE.get(), 0); + return PipeResourceType.VALUES[Math.floorMod(ordinal, PipeResourceType.VALUES.length)]; + } + + private static PipeResourceType cycleSelectedType(ItemStack stack) { + int next = Math.floorMod(stack.getOrDefault(ModDataComponents.SELECTED_PIPE_TYPE.get(), 0) + 1, + PipeResourceType.VALUES.length); + stack.set(ModDataComponents.SELECTED_PIPE_TYPE.get(), next); + return PipeResourceType.VALUES[next]; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + Player player = context.getPlayer(); + + if (player != null && player.isShiftKeyDown()) { + if (level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity) { + // Reserved for the config GUI (client slice); only cycle when sneaking on other blocks. + return InteractionResult.SUCCESS; + } + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + PipeResourceType type = cycleSelectedType(context.getItemInHand()); + serverPlayer.sendSystemMessage(Component.translatable( + "item.nerospace.configurator.selected", type.label())); + } + return InteractionResult.SUCCESS; + } + + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer + && level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity pipe) { + Direction face = context.getClickedFace(); + PipeResourceType type = selectedType(context.getItemInHand()); + PipeIoMode mode = pipe.cycleMode(face, type); + serverPlayer.sendSystemMessage(Component.translatable( + "item.nerospace.configurator.face", type.label(), face.getName(), + Component.translatable("pipe.nerospace.mode." + mode.getSerializedName()))); + return InteractionResult.SUCCESS; + } + return level.isClientSide() ? InteractionResult.SUCCESS : InteractionResult.PASS; + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (player.isShiftKeyDown()) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + PipeResourceType type = cycleSelectedType(player.getItemInHand(hand)); + serverPlayer.sendSystemMessage(Component.translatable( + "item.nerospace.configurator.selected", type.label())); + } + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeFilterItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeFilterItem.java new file mode 100644 index 0000000..64bd63c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeFilterItem.java @@ -0,0 +1,78 @@ +package za.co.neroland.nerospace.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; +import za.co.neroland.nerospace.registry.ModDataComponents; + +/** + * Pipe Filter — restricts a pipe face's item layer to one item: + *
    + *
  • Right-click (air) holding the filter in one hand and the item to filter in the other: + * sets the filter (empty other hand clears it).
  • + *
  • Right-click a Universal Pipe face: applies the filter to that face — only the + * configured item is pulled or pushed through it. Apply an empty filter to remove.
  • + *
+ * + *

Cross-loader port: the standalone mod stored the filter as a NeoForge-transfer {@code ItemResource}; + * the multiloader stores a vanilla {@link ItemStack} (the {@link ModDataComponents#FILTER_ITEM} component + * and {@link UniversalPipeBlockEntity#setFilter} are both ItemStack-based here).

+ */ +public class PipeFilterItem extends Item { + + public PipeFilterItem(Properties properties) { + super(properties); + } + + /** The filter set on this stack (EMPTY = unset). */ + public static ItemStack configured(ItemStack stack) { + return stack.getOrDefault(ModDataComponents.FILTER_ITEM.get(), ItemStack.EMPTY); + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + if (!level.isClientSide() && context.getPlayer() instanceof ServerPlayer player + && level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity pipe) { + Direction face = context.getClickedFace(); + ItemStack filter = configured(context.getItemInHand()); + pipe.setFilter(face, filter); + player.sendSystemMessage(filter.isEmpty() + ? Component.translatable("item.nerospace.pipe_filter.cleared_face", face.getName()) + : Component.translatable("item.nerospace.pipe_filter.applied", + filter.getHoverName(), face.getName())); + return InteractionResult.SUCCESS; + } + return level.isClientSide() ? InteractionResult.SUCCESS : InteractionResult.PASS; + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + ItemStack self = player.getItemInHand(hand); + ItemStack other = player.getItemInHand(hand == InteractionHand.MAIN_HAND + ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND); + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + if (other.isEmpty()) { + self.remove(ModDataComponents.FILTER_ITEM.get()); + serverPlayer.sendSystemMessage(Component.translatable("item.nerospace.pipe_filter.cleared")); + } else { + ItemStack resource = other.copyWithCount(1); + self.set(ModDataComponents.FILTER_ITEM.get(), resource); + serverPlayer.sendSystemMessage(Component.translatable( + "item.nerospace.pipe_filter.set", resource.getHoverName())); + } + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeUpgradeItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeUpgradeItem.java new file mode 100644 index 0000000..d941995 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/PipeUpgradeItem.java @@ -0,0 +1,61 @@ +package za.co.neroland.nerospace.item; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; + +/** + * A pipe upgrade module — right-click a Universal Pipe to install (consumed, up to + * {@link UniversalPipeBlockEntity#MAX_UPGRADES} of each kind per segment): + *
    + *
  • Speed: multiplies the segment's per-face throughput (energy/gas) and item move rate.
  • + *
  • Capacity: buffer multiplier (reserved for the fluid/gas tanks + in-transit cap in the + * graph slice).
  • + *
+ * Sneak-right-click the pipe with an empty hand to pop all upgrades back out. + * + *

Cross-loader port: pure vanilla item; copied verbatim from the standalone mod.

+ */ +public class PipeUpgradeItem extends Item { + + public enum Kind { + SPEED, + CAPACITY + } + + private final Kind kind; + + public PipeUpgradeItem(Properties properties, Kind kind) { + super(properties); + this.kind = kind; + } + + public Kind kind() { + return this.kind; + } + + @Override + public InteractionResult useOn(UseOnContext context) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + if (!level.isClientSide() && context.getPlayer() instanceof ServerPlayer player + && level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity pipe) { + if (pipe.installUpgrade(this.kind)) { + context.getItemInHand().shrink(1); + player.sendSystemMessage(Component.translatable("item.nerospace.pipe_upgrade.installed", + context.getItemInHand().getHoverName(), + pipe.upgradeCount(this.kind), UniversalPipeBlockEntity.MAX_UPGRADES)); + } else { + player.sendSystemMessage(Component.translatable("item.nerospace.pipe_upgrade.full")); + } + return InteractionResult.SUCCESS; + } + return level.isClientSide() ? InteractionResult.SUCCESS : InteractionResult.PASS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeIoMode.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeIoMode.java new file mode 100644 index 0000000..7a852e7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeIoMode.java @@ -0,0 +1,46 @@ +package za.co.neroland.nerospace.pipe; + +import net.minecraft.util.StringRepresentable; + +/** + * Per-face, per-resource-type input/output mode of a Universal Pipe. Every face holds one of these for + * each {@link PipeResourceType}, so a single face can (e.g.) take fluid IN while sending energy OUT. + * {@code AUTO} both pulls from providers and pushes to receivers. + * + *

Cross-loader port: pure vanilla ({@link StringRepresentable}); identical to the standalone mod.

+ */ +public enum PipeIoMode implements StringRepresentable { + AUTO("auto"), + IN("in"), + OUT("out"), + OFF("off"); + + public static final PipeIoMode[] VALUES = values(); + + private final String name; + + PipeIoMode(String name) { + this.name = name; + } + + public boolean canPull() { + return this == AUTO || this == IN; + } + + public boolean canPush() { + return this == AUTO || this == OUT; + } + + public boolean isConnected() { + return this != OFF; + } + + public PipeIoMode next() { + return VALUES[(ordinal() + 1) % VALUES.length]; + } + + @Override + public String getSerializedName() { + return this.name; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeResourceType.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeResourceType.java new file mode 100644 index 0000000..c3d3f91 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/PipeResourceType.java @@ -0,0 +1,42 @@ +package za.co.neroland.nerospace.pipe; + +import net.minecraft.network.chat.Component; +import net.minecraft.util.StringRepresentable; + +/** + * The four resource layers a Universal Pipe carries simultaneously over its one connection graph. + * Each has a display colour (ARGB) used by the streams in the pipe renderer and the Configurator UI. + * + *

Cross-loader port: pure vanilla ({@link StringRepresentable}/{@link Component}); identical to the + * standalone mod. Note: the multiloader relay currently moves energy, gas and items — the {@code FLUID} + * layer is reserved (its per-face mode is stored but inert until the fluid relay lands).

+ */ +public enum PipeResourceType implements StringRepresentable { + ENERGY("energy", 0xFFE0506A), // red — FE + FLUID("fluid", 0xFF3C78F0), // blue + GAS("gas", 0xFF78D2F0), // O₂ cyan + ITEM("item", 0xFFE8E8F4); // white — items render as themselves + + public static final PipeResourceType[] VALUES = values(); + + private final String name; + private final int color; + + PipeResourceType(String name, int color) { + this.name = name; + this.color = color; + } + + public int color() { + return this.color; + } + + public Component label() { + return Component.translatable("pipe.nerospace.type." + this.name); + } + + @Override + public String getSerializedName() { + return this.name; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java index bf22740..abe97a4 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java @@ -3,6 +3,8 @@ import com.mojang.serialization.MapCodec; import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.RenderShape; @@ -10,12 +12,13 @@ import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; import org.jetbrains.annotations.Nullable; import za.co.neroland.nerospace.registry.ModBlockEntities; -/** Universal Pipe block — ticks its {@link UniversalPipeBlockEntity} energy relay. */ +/** Universal Pipe block — ticks its {@link UniversalPipeBlockEntity} relay; sneak-empty-hand pops upgrades. */ public class UniversalPipeBlock extends BaseEntityBlock { public static final MapCodec CODEC = simpleCodec(UniversalPipeBlock::new); @@ -34,6 +37,15 @@ protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; } + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (player.isShiftKeyDown() && level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity pipe + && pipe.uninstallUpgrades() > 0) { + return InteractionResult.SUCCESS; + } + return InteractionResult.PASS; + } + @Nullable @Override public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java index 55e33ae..867ddb1 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -3,7 +3,9 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.NonNullList; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Container; +import net.minecraft.world.Containers; import net.minecraft.world.WorldlyContainer; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; @@ -20,9 +22,11 @@ import za.co.neroland.nerospace.gas.GasResource; import za.co.neroland.nerospace.gas.GasTank; import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.item.PipeUpgradeItem; import za.co.neroland.nerospace.platform.EnergyLookup; import za.co.neroland.nerospace.platform.GasLookup; import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; /** * Universal Pipe — relays energy, gas AND items between adjacent storages. Energy/gas use the @@ -42,12 +46,28 @@ public class UniversalPipeBlockEntity extends BlockEntity implements WorldlyCont private static final int[] ALL_SLOTS = {0, 1, 2}; + /** Max installed upgrades of each kind per segment. */ + public static final int MAX_UPGRADES = 3; + private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_IO, MAX_IO, this::setChanged); private final GasTank gas = new GasTank(GAS_CAPACITY, this::setChanged); private final NonNullList items = NonNullList.withSize(ITEM_SLOTS, ItemStack.EMPTY); + /** Per-face (6) × per-resource-type (4) I/O mode, set with the Configurator. Default AUTO. */ + private final PipeIoMode[][] faceModes = new PipeIoMode[6][PipeResourceType.VALUES.length]; + /** Per-face item-layer filter (EMPTY = unfiltered), indexed by {@code Direction.get3DDataValue()}. */ + private final ItemStack[] faceFilters = new ItemStack[6]; + private int speedUpgrades; + private int capacityUpgrades; + public UniversalPipeBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.UNIVERSAL_PIPE.get(), pos, state); + for (int f = 0; f < 6; f++) { + for (int t = 0; t < PipeResourceType.VALUES.length; t++) { + this.faceModes[f][t] = PipeIoMode.AUTO; + } + this.faceFilters[f] = ItemStack.EMPTY; + } } public NerospaceEnergyStorage getEnergy() { @@ -58,6 +78,94 @@ public NerospaceGasStorage getGas() { return this.gas; } + // --- Per-face I/O modes (Configurator) ----------------------------------- + + public PipeIoMode mode(Direction dir, PipeResourceType type) { + return this.faceModes[dir.get3DDataValue()][type.ordinal()]; + } + + /** Cycle a face's mode for one resource type (Configurator). @return the new mode. */ + public PipeIoMode cycleMode(Direction dir, PipeResourceType type) { + PipeIoMode next = mode(dir, type).next(); + this.faceModes[dir.get3DDataValue()][type.ordinal()] = next; + setChanged(); + return next; + } + + /** Directly set a face's mode for one resource type (Configurator GUI / commands). */ + public void setMode(Direction dir, PipeResourceType type, PipeIoMode mode) { + this.faceModes[dir.get3DDataValue()][type.ordinal()] = mode; + setChanged(); + } + + // --- Per-face item filter (Pipe Filter) ---------------------------------- + + public ItemStack filter(Direction dir) { + return this.faceFilters[dir.get3DDataValue()]; + } + + public void setFilter(Direction dir, ItemStack filter) { + this.faceFilters[dir.get3DDataValue()] = filter == null ? ItemStack.EMPTY : filter; + setChanged(); + } + + private boolean passesFilter(Direction dir, ItemStack candidate) { + ItemStack f = this.faceFilters[dir.get3DDataValue()]; + return f.isEmpty() || ItemStack.isSameItemSameComponents(f, candidate); + } + + // --- Upgrades (Speed / Capacity) ----------------------------------------- + + public boolean installUpgrade(PipeUpgradeItem.Kind kind) { + if (upgradeCount(kind) >= MAX_UPGRADES) { + return false; + } + if (kind == PipeUpgradeItem.Kind.SPEED) { + this.speedUpgrades++; + } else { + this.capacityUpgrades++; + } + setChanged(); + return true; + } + + public int upgradeCount(PipeUpgradeItem.Kind kind) { + return kind == PipeUpgradeItem.Kind.SPEED ? this.speedUpgrades : this.capacityUpgrades; + } + + /** Pops all installed upgrades back out (sneak-right-click with an empty hand). @return count. */ + public int uninstallUpgrades() { + int total = this.speedUpgrades + this.capacityUpgrades; + if (total > 0 && this.level instanceof ServerLevel serverLevel) { + BlockPos pos = getBlockPos(); + double x = pos.getX() + 0.5; + double y = pos.getY() + 0.5; + double z = pos.getZ() + 0.5; + if (this.speedUpgrades > 0) { + Containers.dropItemStack(serverLevel, x, y, z, + new ItemStack(ModItems.SPEED_UPGRADE.get(), this.speedUpgrades)); + } + if (this.capacityUpgrades > 0) { + Containers.dropItemStack(serverLevel, x, y, z, + new ItemStack(ModItems.CAPACITY_UPGRADE.get(), this.capacityUpgrades)); + } + this.speedUpgrades = 0; + this.capacityUpgrades = 0; + setChanged(); + } + return total; + } + + /** Throughput multiplier for the energy/gas layers (and item move rate). */ + public int speedMultiplier() { + return 1 + this.speedUpgrades; + } + + /** Buffer multiplier — reserved for the fluid/gas tanks + in-transit cap in the graph slice. */ + public int capacityMultiplier() { + return 1 + this.capacityUpgrades; + } + public void tick(Level level, BlockPos pos, BlockState state) { if (level.isClientSide()) { return; @@ -68,14 +176,18 @@ public void tick(Level level, BlockPos pos, BlockState state) { } private void relayEnergy(Level level, BlockPos pos) { + long io = (long) MAX_IO * speedMultiplier(); for (Direction dir : Direction.values()) { + if (!mode(dir, PipeResourceType.ENERGY).canPull()) { + continue; + } NerospaceEnergyStorage neighbour = EnergyLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); if (neighbour == null) { continue; } long room = this.energy.getCapacity() - this.energy.getAmount(); if (room > 0) { - long moved = neighbour.extract(Math.min(room, MAX_IO), false); + long moved = neighbour.extract(Math.min(room, io), false); if (moved > 0) { this.energy.insert(moved, false); } @@ -85,11 +197,14 @@ private void relayEnergy(Level level, BlockPos pos) { if (this.energy.getAmount() <= 0) { break; } + if (!mode(dir, PipeResourceType.ENERGY).canPush()) { + continue; + } NerospaceEnergyStorage neighbour = EnergyLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); if (neighbour == null) { continue; } - long offered = this.energy.extract(Math.min(this.energy.getAmount(), MAX_IO), true); + long offered = this.energy.extract(Math.min(this.energy.getAmount(), io), true); long accepted = neighbour.insert(offered, false); if (accepted > 0) { this.energy.extract(accepted, false); @@ -98,11 +213,15 @@ private void relayEnergy(Level level, BlockPos pos) { } private void relayGas(Level level, BlockPos pos) { + long io = (long) GAS_MAX_IO * speedMultiplier(); for (Direction dir : Direction.values()) { long room = this.gas.getCapacity() - this.gas.getAmount(); if (room <= 0) { break; } + if (!mode(dir, PipeResourceType.GAS).canPull()) { + continue; + } NerospaceGasStorage neighbour = GasLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); if (neighbour == null) { continue; @@ -111,7 +230,7 @@ private void relayGas(Level level, BlockPos pos) { if (ngas.isEmpty() || (!this.gas.getGas().isEmpty() && this.gas.getGas() != ngas)) { continue; } - long available = neighbour.drain(Math.min(room, GAS_MAX_IO), true); + long available = neighbour.drain(Math.min(room, io), true); long moved = this.gas.fill(ngas, available, false); if (moved > 0) { neighbour.drain(moved, false); @@ -121,12 +240,15 @@ private void relayGas(Level level, BlockPos pos) { if (this.gas.getAmount() <= 0) { break; } + if (!mode(dir, PipeResourceType.GAS).canPush()) { + continue; + } NerospaceGasStorage neighbour = GasLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); if (neighbour == null) { continue; } GasResource g = this.gas.getGas(); - long offered = this.gas.drain(Math.min(this.gas.getAmount(), GAS_MAX_IO), true); + long offered = this.gas.drain(Math.min(this.gas.getAmount(), io), true); long accepted = neighbour.fill(g, offered, false); if (accepted > 0) { this.gas.drain(accepted, false); @@ -135,18 +257,34 @@ private void relayGas(Level level, BlockPos pos) { } private void relayItems(Level level, BlockPos pos) { - // Pull one item per tick from each non-pipe neighbour container into the buffer. + int perTick = speedMultiplier(); + // Pull from each non-pipe neighbour whose face ITEM mode allows pulling; the pipe's face filter + // (if any) restricts what enters. Sources feed the line — pipes are never pulled from. for (Direction dir : Direction.values()) { + if (!mode(dir, PipeResourceType.ITEM).canPull()) { + continue; + } BlockEntity be = level.getBlockEntity(pos.relative(dir)); if (be instanceof Container src && !(be instanceof UniversalPipeBlockEntity)) { - moveOne(src, dir.getOpposite(), this, dir); + for (int i = 0; i < perTick; i++) { + if (!moveOneFiltered(src, dir.getOpposite(), this, dir, dir)) { + break; + } + } } } - // Push one item per tick from the buffer into each neighbour container (incl. other pipes). + // Push into each neighbour (incl. other pipes) whose face ITEM mode allows pushing. for (Direction dir : Direction.values()) { + if (!mode(dir, PipeResourceType.ITEM).canPush()) { + continue; + } BlockEntity be = level.getBlockEntity(pos.relative(dir)); if (be instanceof Container dst) { - moveOne(this, dir, dst, dir.getOpposite()); + for (int i = 0; i < perTick; i++) { + if (!moveOneFiltered(this, dir, dst, dir.getOpposite(), dir)) { + break; + } + } } } } @@ -169,13 +307,20 @@ private static boolean placeable(Container into, int slot, ItemStack stack, Dire return !(into instanceof WorldlyContainer wc) || wc.canPlaceItemThroughFace(slot, stack, face); } - /** Move a single item from {@code from} (extracted through {@code fromFace}) into {@code into}. */ - private static boolean moveOne(Container from, Direction fromFace, Container into, Direction intoFace) { + /** + * Move a single item from {@code from} (extracted through {@code fromFace}) into {@code into}, + * honouring this pipe's item filter on {@code filterFace} (the face the item passes through here). + */ + private boolean moveOneFiltered(Container from, Direction fromFace, Container into, Direction intoFace, + Direction filterFace) { for (int fs : slotsFor(from, fromFace)) { ItemStack stack = from.getItem(fs); if (stack.isEmpty()) { continue; } + if (!passesFilter(filterFace, stack)) { + continue; + } if (from instanceof WorldlyContainer wc && !wc.canTakeItemThroughFace(fs, stack, fromFace)) { continue; } @@ -221,6 +366,22 @@ protected void saveAdditional(ValueOutput output) { output.store("Item" + i, ItemStack.OPTIONAL_CODEC, this.items.get(i)); } } + // Per-face × per-type I/O modes packed two bits each (6 faces × 4 types = 48 bits). + long packed = 0L; + int types = PipeResourceType.VALUES.length; + for (int f = 0; f < 6; f++) { + for (int t = 0; t < types; t++) { + packed |= ((long) (this.faceModes[f][t].ordinal() & 0x3)) << ((f * types + t) * 2); + } + } + output.putLong("Faces", packed); + for (int f = 0; f < 6; f++) { + if (!this.faceFilters[f].isEmpty()) { + output.store("Filter" + f, ItemStack.OPTIONAL_CODEC, this.faceFilters[f]); + } + } + output.putInt("SpeedUpgrades", this.speedUpgrades); + output.putInt("CapacityUpgrades", this.capacityUpgrades); } @Override @@ -231,6 +392,18 @@ protected void loadAdditional(ValueInput input) { for (int i = 0; i < ITEM_SLOTS; i++) { this.items.set(i, input.read("Item" + i, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); } + long packed = input.getLongOr("Faces", 0L); + int types = PipeResourceType.VALUES.length; + for (int f = 0; f < 6; f++) { + for (int t = 0; t < types; t++) { + this.faceModes[f][t] = PipeIoMode.VALUES[(int) ((packed >> ((f * types + t) * 2)) & 0x3L)]; + } + } + for (int f = 0; f < 6; f++) { + this.faceFilters[f] = input.read("Filter" + f, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY); + } + this.speedUpgrades = input.getIntOr("SpeedUpgrades", 0); + this.capacityUpgrades = input.getIntOr("CapacityUpgrades", 0); } // --- WorldlyContainer (item buffer) ------------------------------------- diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index 7532f7d..c86996e 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -29,9 +29,12 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.item.ConfiguratorItem; import za.co.neroland.nerospace.item.DestinationCompassItem; import za.co.neroland.nerospace.item.GreenxertzNavigatorItem; import za.co.neroland.nerospace.item.NerospaceSpawnEggItem; +import za.co.neroland.nerospace.item.PipeFilterItem; +import za.co.neroland.nerospace.item.PipeUpgradeItem; import za.co.neroland.nerospace.meteor.MeteorCallerItem; import za.co.neroland.nerospace.module.ModuleType; import za.co.neroland.nerospace.module.UpgradeModuleItem; @@ -119,6 +122,16 @@ public final class ModItems { /** Trade-only Artificer gear; ported as a plain item (its custom gear behaviour is deferred). */ public static final RegistryEntry XERTZ_RESONATOR = item("xertz_resonator"); + // --- Universal Pipe tools (per-face I/O modes, item filters, throughput upgrades) ---- + public static final RegistryEntry CONFIGURATOR = ITEMS.register("configurator", + key -> new ConfiguratorItem(new Item.Properties().stacksTo(1).setId(key))); + public static final RegistryEntry PIPE_FILTER = ITEMS.register("pipe_filter", + key -> new PipeFilterItem(new Item.Properties().stacksTo(16).setId(key))); + public static final RegistryEntry SPEED_UPGRADE = ITEMS.register("speed_upgrade", + key -> new PipeUpgradeItem(new Item.Properties().setId(key), PipeUpgradeItem.Kind.SPEED)); + public static final RegistryEntry CAPACITY_UPGRADE = ITEMS.register("capacity_upgrade", + key -> new PipeUpgradeItem(new Item.Properties().setId(key), PipeUpgradeItem.Kind.CAPACITY)); + // --- Machine upgrade modules (the quarry is the first consumer) ---------- public static final RegistryEntry SPEED_MODULE = module("speed_module", ModuleType.SPEED); public static final RegistryEntry EFFICIENCY_MODULE = module("efficiency_module", ModuleType.EFFICIENCY); @@ -266,7 +279,8 @@ public static Map, List> creativeTabItems List.of(NEROSIUM_PICKAXE.get(), ROCKET_FUEL_BUCKET.get(), XERTZ_RESONATOR.get(), ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get(), GREENXERTZ_NAVIGATOR.get(), STATION_COMPASS.get(), GREENXERTZ_COMPASS.get(), - CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get(), METEOR_TRACKER.get()), + CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get(), METEOR_TRACKER.get(), + CONFIGURATOR.get(), PIPE_FILTER.get(), SPEED_UPGRADE.get(), CAPACITY_UPGRADE.get()), CreativeModeTabs.SPAWN_EGGS, List.of(XERTZ_STALKER_SPAWN_EGG.get(), QUARTZ_CRAWLER_SPAWN_EGG.get(), GREENLING_SPAWN_EGG.get(), ALIEN_VILLAGER_SPAWN_EGG.get(), CINDER_STALKER_SPAWN_EGG.get(), diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/capacity_upgrade.json b/multiloader/common/src/main/resources/assets/nerospace/items/capacity_upgrade.json new file mode 100644 index 0000000..603e411 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/capacity_upgrade.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/capacity_upgrade" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/configurator.json b/multiloader/common/src/main/resources/assets/nerospace/items/configurator.json new file mode 100644 index 0000000..232b13b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/configurator.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/configurator" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/pipe_filter.json b/multiloader/common/src/main/resources/assets/nerospace/items/pipe_filter.json new file mode 100644 index 0000000..77df951 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/pipe_filter.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/pipe_filter" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/speed_upgrade.json b/multiloader/common/src/main/resources/assets/nerospace/items/speed_upgrade.json new file mode 100644 index 0000000..a308ae8 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/speed_upgrade.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/speed_upgrade" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 8614419..1fba814 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -112,9 +112,13 @@ "item.nerospace.alien_fragment": "Alien Fragment", "item.nerospace.alien_tech_scrap": "Alien Tech Scrap", "item.nerospace.alien_villager_spawn_egg": "Alien Villager Spawn Egg", + "item.nerospace.capacity_upgrade": "Capacity Upgrade", "item.nerospace.cindara_compass": "Cindara Compass", "item.nerospace.cinder_stalker_spawn_egg": "Cinder Stalker Spawn Egg", "item.nerospace.cindrite": "Cindrite", + "item.nerospace.configurator": "Configurator", + "item.nerospace.configurator.face": "%1$s — %2$s face: %3$s", + "item.nerospace.configurator.selected": "Configuring: %s", "item.nerospace.destination_compass.travel": "Travelling to %s", "item.nerospace.drift_fleece": "Drift Fleece", "item.nerospace.efficiency_module": "Efficiency Module", @@ -159,6 +163,13 @@ "item.nerospace.oxygen_suit_t2_chestplate": "Tier 2 Oxygen Suit Chestplate", "item.nerospace.oxygen_suit_t2_helmet": "Tier 2 Oxygen Suit Helmet", "item.nerospace.oxygen_suit_t2_leggings": "Tier 2 Oxygen Suit Leggings", + "item.nerospace.pipe_filter": "Pipe Filter", + "item.nerospace.pipe_filter.applied": "Face filter applied: %s on the %s face", + "item.nerospace.pipe_filter.cleared": "Filter cleared", + "item.nerospace.pipe_filter.cleared_face": "Face filter removed from the %s face", + "item.nerospace.pipe_filter.set": "Filter set: %s", + "item.nerospace.pipe_upgrade.full": "This pipe segment has no room for that upgrade", + "item.nerospace.pipe_upgrade.installed": "%s installed (%s/%s)", "item.nerospace.quartz_crawler_spawn_egg": "Quartz Crawler Spawn Egg", "item.nerospace.raw_nerosium": "Raw Nerosium", "item.nerospace.raw_nerosteel": "Raw Nerosteel", @@ -175,11 +186,26 @@ "item.nerospace.rocket_tier_4": "Tier 4 Rocket", "item.nerospace.silk_touch_module": "Silk Touch Module", "item.nerospace.speed_module": "Speed Module", + "item.nerospace.speed_upgrade": "Speed Upgrade", "item.nerospace.station_compass": "Station Compass", "item.nerospace.woolly_drift_spawn_egg": "Woolly Drift Spawn Egg", "item.nerospace.xertz_quartz": "Xertz Quartz", "item.nerospace.xertz_resonator": "Xertz Resonator", "item.nerospace.xertz_stalker_spawn_egg": "Xertz Stalker Spawn Egg", "itemGroup.nerospace": "Nerospace", - "message.nerospace.greenxertz.no_air": "You are out of oxygen — reach a launch pad or an Oxygen Generator!" + "message.nerospace.greenxertz.no_air": "You are out of oxygen — reach a launch pad or an Oxygen Generator!", + "pipe.nerospace.face.down": "Bottom", + "pipe.nerospace.face.east": "East", + "pipe.nerospace.face.north": "North", + "pipe.nerospace.face.south": "South", + "pipe.nerospace.face.up": "Top", + "pipe.nerospace.face.west": "West", + "pipe.nerospace.mode.auto": "Auto", + "pipe.nerospace.mode.in": "In", + "pipe.nerospace.mode.off": "Off", + "pipe.nerospace.mode.out": "Out", + "pipe.nerospace.type.energy": "Energy", + "pipe.nerospace.type.fluid": "Fluid", + "pipe.nerospace.type.gas": "Gas", + "pipe.nerospace.type.item": "Items" } diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/capacity_upgrade.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/capacity_upgrade.json new file mode 100644 index 0000000..d41ce98 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/capacity_upgrade.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/capacity_upgrade" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/configurator.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/configurator.json new file mode 100644 index 0000000..6484e70 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/configurator.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/configurator" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/pipe_filter.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/pipe_filter.json new file mode 100644 index 0000000..61f82ce --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/pipe_filter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/pipe_filter" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/speed_upgrade.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/speed_upgrade.json new file mode 100644 index 0000000..251392b --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/speed_upgrade.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/speed_upgrade" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/capacity_upgrade.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/capacity_upgrade.png new file mode 100644 index 0000000000000000000000000000000000000000..d311223367e37bf5e05f937d22eae4227b7f8e8a GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`S)MMAAr*6y6C^SYa3t^e+44u- z_3=90YzavT35kdatf?s}9hd$*IVo&0yHg~3BcoVO)BK{k#LzS5t-Th^8#IrlYQ*W+ zoch@stQ^QDz+6z(*vPnhey3T(gv%eL9D2?gINUXG@Jn=NXJFX(QbKF4^X6!vtqh*7 KelF{r5}E)Z%r|-f literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/configurator.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/configurator.png new file mode 100644 index 0000000000000000000000000000000000000000..7212122931fdaff5f89d50f97fa8c9e2905f0ca3 GIT binary patch literal 110 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`rk*a2Ar*6y6Be+|*p_wcf9C3C zf0v(NNU2JQY0L?5a^y;7>yHjRT4j#si%oBK(PBS=d6Mo9%WY{=KXkxGgGc&WZ6p&zE dbwPoJ!FH>r(B9NicA!HTJYD@<);T3K0RReiJedFh literal 0 HcmV?d00001 From 2da5feffffbe0000f63918308606907b186d0eb6 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 13:29:08 +0200 Subject: [PATCH 62/82] Add fluid lookup and pipe fluid relay Introduce a fluid query seam and pipe fluid handling across loaders. Adds a common platform FluidLookup (Services-resolved) and Fabric/NeoForge implementations with META-INF service entries. Registers the FLUID block-api/capability for the Universal Pipe on Fabric and NeoForge. UniversalPipeBlockEntity gains a FluidTank (capacity/max IO constants), getFluidTank(), relayFluid() logic (pull/push honoring per-face PipeIoMode and speed), tick wiring, and NBT persistence/serialization for fluid (uses BuiltInRegistries/Identifier). Updates docs checklist to reflect the completed fluid relay work. --- docs/MULTILOADER_PORT_CHECKLIST.md | 16 ++++- .../pipe/UniversalPipeBlockEntity.java | 63 +++++++++++++++++++ .../nerospace/platform/FluidLookup.java | 22 +++++++ .../nerospace/fabric/NerospaceFabric.java | 3 + .../nerospace/platform/FabricFluidLookup.java | 20 ++++++ ...co.neroland.nerospace.platform.FluidLookup | 1 + .../neoforge/NeoForgeCapabilities.java | 4 ++ .../platform/NeoForgeFluidLookup.java | 20 ++++++ ...co.neroland.nerospace.platform.FluidLookup | 1 + 9 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidLookup.java create mode 100644 multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidLookup.java create mode 100644 multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup create mode 100644 multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidLookup.java create mode 100644 multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index cda5068..e3f39a0 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,19 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~199 classes ported, ~65 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~202 classes ported, ~62 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — pipe fluid relay (closes the slice-A FLUID gap).** All 4 cells green (compile on +> 26.2; full `:neoforge:build`+`:fabric:build` on 26.1.2). Added a `platform/FluidLookup` query seam +> (mirrors `EnergyLookup`/`GasLookup`: common interface via `Services.load` + `NeoForgeFluidLookup` +> [`level.getCapability`] + `FabricFluidLookup` [`BlockApiLookup.find`] + both `META-INF/services` files). +> `UniversalPipeBlockEntity` gained a `FluidTank` + `getFluidTank()`, a `relayFluid()` mirroring the gas +> relay (honours the FLUID face-mode + speed throughput), tick wiring, and NBT persistence; the pipe's +> fluid handler is now exposed as the FLUID capability on both loaders. **The Universal Pipe now genuinely +> carries all four layers (energy/fluid/gas/item)** — e.g. piping `rocket_fuel` from a Refinery to a Fuel +> Tank — and the slice-A FLUID face-mode is now functional (no longer inert). + > **2026-06-21 update — advanced pipes slice A (per-face configuration layer).** All 4 cells green > (compile on 26.1.2 + 26.2; full `:neoforge:build`+`:fabric:build` on 26.2). Added `pipe/PipeIoMode` > + `pipe/PipeResourceType` (pure-vanilla enums) and the three pipe tools — `item/ConfiguratorItem` @@ -337,6 +347,10 @@ checked by a headless build). speed/capacity upgrades; the energy/gas/item relay honours `canPull`/`canPush`/`OFF` + filters + speed throughput; `UniversalPipeBlock` sneak-empty-hand pops upgrades. Items registered (TOOLS tab) + assets + 20 lang keys. +- [x] **Fluid relay** — added the `platform/FluidLookup` query seam (common + both loaders + services) and a + `FluidTank` + `relayFluid()` to the pipe BE (honours the FLUID face-mode + speed); the pipe's fluid handler + is exposed as the FLUID cap on both loaders. The pipe now carries all four layers; the FLUID face-mode is + live (e.g. Refinery → pipe → Fuel Tank). - [ ] **Slice B (deferred) — graph + visuals + GUI.** `PipeNetwork` (591-line graph; NeoForge-transfer- coupled), `TravellingItem` (animated stacks; ItemResource→ItemStack), `UniversalPipeRenderer` + `UniversalPipeRenderState` (stream + travelling-item visuals), `PipeConfigScreen` + `PipeConfigOpenHandler` diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java index 867ddb1..2b73271 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -3,6 +3,8 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.NonNullList; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Container; import net.minecraft.world.Containers; @@ -12,6 +14,8 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; @@ -19,11 +23,14 @@ import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.fluid.FluidTank; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; import za.co.neroland.nerospace.gas.GasResource; import za.co.neroland.nerospace.gas.GasTank; import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.item.PipeUpgradeItem; import za.co.neroland.nerospace.platform.EnergyLookup; +import za.co.neroland.nerospace.platform.FluidLookup; import za.co.neroland.nerospace.platform.GasLookup; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModItems; @@ -42,6 +49,8 @@ public class UniversalPipeBlockEntity extends BlockEntity implements WorldlyCont public static final int MAX_IO = 1_000; public static final int GAS_CAPACITY = 8_000; public static final int GAS_MAX_IO = 1_000; + public static final int FLUID_CAPACITY = 8_000; + public static final int FLUID_MAX_IO = 1_000; public static final int ITEM_SLOTS = 3; private static final int[] ALL_SLOTS = {0, 1, 2}; @@ -51,6 +60,7 @@ public class UniversalPipeBlockEntity extends BlockEntity implements WorldlyCont private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, MAX_IO, MAX_IO, this::setChanged); private final GasTank gas = new GasTank(GAS_CAPACITY, this::setChanged); + private final FluidTank fluid = new FluidTank(FLUID_CAPACITY, this::setChanged); private final NonNullList items = NonNullList.withSize(ITEM_SLOTS, ItemStack.EMPTY); /** Per-face (6) × per-resource-type (4) I/O mode, set with the Configurator. Default AUTO. */ @@ -78,6 +88,10 @@ public NerospaceGasStorage getGas() { return this.gas; } + public NerospaceFluidStorage getFluidTank() { + return this.fluid; + } + // --- Per-face I/O modes (Configurator) ----------------------------------- public PipeIoMode mode(Direction dir, PipeResourceType type) { @@ -172,6 +186,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { } relayEnergy(level, pos); relayGas(level, pos); + relayFluid(level, pos); relayItems(level, pos); } @@ -256,6 +271,50 @@ private void relayGas(Level level, BlockPos pos) { } } + private void relayFluid(Level level, BlockPos pos) { + long io = (long) FLUID_MAX_IO * speedMultiplier(); + for (Direction dir : Direction.values()) { + long room = this.fluid.getCapacity() - this.fluid.getAmount(); + if (room <= 0) { + break; + } + if (!mode(dir, PipeResourceType.FLUID).canPull()) { + continue; + } + NerospaceFluidStorage neighbour = FluidLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); + if (neighbour == null) { + continue; + } + Fluid nfluid = neighbour.getFluid(); + if (nfluid == Fluids.EMPTY || (this.fluid.getFluid() != Fluids.EMPTY && this.fluid.getFluid() != nfluid)) { + continue; + } + long available = neighbour.drain(Math.min(room, io), true); + long moved = this.fluid.fill(nfluid, available, false); + if (moved > 0) { + neighbour.drain(moved, false); + } + } + for (Direction dir : Direction.values()) { + if (this.fluid.getAmount() <= 0) { + break; + } + if (!mode(dir, PipeResourceType.FLUID).canPush()) { + continue; + } + NerospaceFluidStorage neighbour = FluidLookup.INSTANCE.find(level, pos.relative(dir), dir.getOpposite()); + if (neighbour == null) { + continue; + } + Fluid f = this.fluid.getFluid(); + long offered = this.fluid.drain(Math.min(this.fluid.getAmount(), io), true); + long accepted = neighbour.fill(f, offered, false); + if (accepted > 0) { + this.fluid.drain(accepted, false); + } + } + } + private void relayItems(Level level, BlockPos pos) { int perTick = speedMultiplier(); // Pull from each non-pipe neighbour whose face ITEM mode allows pulling; the pipe's face filter @@ -361,6 +420,8 @@ protected void saveAdditional(ValueOutput output) { output.putInt("Energy", this.energy.getRaw()); output.putString("Gas", this.gas.getRawGas().getSerializedName()); output.putInt("GasAmount", this.gas.getRawAmount()); + output.putString("Fluid", BuiltInRegistries.FLUID.getKey(this.fluid.getRawFluid()).toString()); + output.putInt("FluidAmount", this.fluid.getRawAmount()); for (int i = 0; i < ITEM_SLOTS; i++) { if (!this.items.get(i).isEmpty()) { output.store("Item" + i, ItemStack.OPTIONAL_CODEC, this.items.get(i)); @@ -389,6 +450,8 @@ protected void loadAdditional(ValueInput input) { super.loadAdditional(input); this.energy.setRaw(input.getIntOr("Energy", 0)); this.gas.setRaw(GasResource.byName(input.getStringOr("Gas", "empty")), input.getIntOr("GasAmount", 0)); + Fluid storedFluid = BuiltInRegistries.FLUID.getValue(Identifier.parse(input.getStringOr("Fluid", "minecraft:empty"))); + this.fluid.setRaw(storedFluid, input.getIntOr("FluidAmount", 0)); for (int i = 0; i < ITEM_SLOTS; i++) { this.items.set(i, input.read("Item" + i, ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY)); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidLookup.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidLookup.java new file mode 100644 index 0000000..7374624 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/FluidLookup.java @@ -0,0 +1,22 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; + +/** + * Query side of the fluid seam: find the fluid storage exposed by the block at {@code pos} on + * {@code side}. Mirrors {@link EnergyLookup}/{@link GasLookup} — NeoForge implements it over + * {@code Level.getCapability}, Fabric over {@code BlockApiLookup.find}. Resolved via {@link Services}. + */ +public interface FluidLookup { + + FluidLookup INSTANCE = Services.load(FluidLookup.class); + + @Nullable + NerospaceFluidStorage find(Level level, BlockPos pos, @Nullable Direction side); +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index dd411b3..4d0fd89 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -140,6 +140,9 @@ public void register(EntityType type, SpawnPlacementType plac GAS.registerForBlockEntity( (be, direction) -> be.getGas(), ModBlockEntities.UNIVERSAL_PIPE.get()); + FLUID.registerForBlockEntity( + (be, direction) -> be.getFluidTank(), + ModBlockEntities.UNIVERSAL_PIPE.get()); ItemStorage.SIDED.registerForBlockEntity( (be, direction) -> ContainerStorage.of(be, direction), ModBlockEntities.UNIVERSAL_PIPE.get()); diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidLookup.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidLookup.java new file mode 100644 index 0000000..873b468 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricFluidLookup.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.fabric.NerospaceFabric; +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; + +/** Fabric query of the mod's fluid block-api lookup. */ +public final class FabricFluidLookup implements FluidLookup { + + @Nullable + @Override + public NerospaceFluidStorage find(Level level, BlockPos pos, @Nullable Direction side) { + return NerospaceFabric.FLUID.find(level, pos, side); + } +} diff --git a/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup new file mode 100644 index 0000000..edaef5f --- /dev/null +++ b/multiloader/fabric/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.FabricFluidLookup diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java index b295d04..6e50509 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -110,6 +110,10 @@ private static void onRegisterCapabilities(RegisterCapabilitiesEvent event) { GAS, ModBlockEntities.UNIVERSAL_PIPE.get(), (be, side) -> be.getGas()); + event.registerBlockEntity( + FLUID, + ModBlockEntities.UNIVERSAL_PIPE.get(), + (be, side) -> be.getFluidTank()); event.registerBlockEntity( Capabilities.Item.BLOCK, ModBlockEntities.UNIVERSAL_PIPE.get(), diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidLookup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidLookup.java new file mode 100644 index 0000000..d9ace8f --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgeFluidLookup.java @@ -0,0 +1,20 @@ +package za.co.neroland.nerospace.platform; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.neoforge.NeoForgeCapabilities; + +/** NeoForge query of the mod's fluid capability. */ +public final class NeoForgeFluidLookup implements FluidLookup { + + @Nullable + @Override + public NerospaceFluidStorage find(Level level, BlockPos pos, @Nullable Direction side) { + return level.getCapability(NeoForgeCapabilities.FLUID, pos, side); + } +} diff --git a/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup new file mode 100644 index 0000000..2e4bfc0 --- /dev/null +++ b/multiloader/neoforge/src/main/resources/META-INF/services/za.co.neroland.nerospace.platform.FluidLookup @@ -0,0 +1 @@ +za.co.neroland.nerospace.platform.NeoForgeFluidLookup From 5c9e7912303fe73867b00a7afd9497f14ea849ef Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:46:48 +0200 Subject: [PATCH 63/82] Add Star Guide progression (book, pedestal, UI) Port the first slice of the Star Guide progression: adds browsable guide UI, pedestal block/BE, book item, menu and server-side progression queries. New classes: StarGuideScreen, StarGuideBookItem, StarGuide, StarGuideBlock, StarGuideBlockEntity, StarGuideMenu, StarGuideProgress. Register block, block-item, book item, BE and menu (ModBlocks, ModItems, ModBlockEntities, ModMenuTypes) and add models, textures, blockstate, loot table and ~98 lang keys. BlockEntity computes & syncs a per-player next-step hologram icon (server-side); the client BER renderer and the "seen" player-attachment are deferred to a later slice. Menu reads advancement completion live (no ModCriteria dependency); two steps use stand-in icons until related content is ported. Also update MULTILOADER_PORT_CHECKLIST.md to reflect the slice and build status. --- docs/MULTILOADER_PORT_CHECKLIST.md | 31 ++- .../nerospace/client/StarGuideScreen.java | 184 ++++++++++++++++++ .../nerospace/item/StarGuideBookItem.java | 37 ++++ .../nerospace/progression/StarGuide.java | 124 ++++++++++++ .../nerospace/progression/StarGuideBlock.java | 121 ++++++++++++ .../progression/StarGuideBlockEntity.java | 162 +++++++++++++++ .../nerospace/progression/StarGuideMenu.java | 90 +++++++++ .../progression/StarGuideProgress.java | 56 ++++++ .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 6 + .../neroland/nerospace/registry/ModItems.java | 12 +- .../nerospace/registry/ModMenuTypes.java | 5 + .../nerospace/blockstates/star_guide.json | 7 + .../assets/nerospace/items/star_guide.json | 6 + .../nerospace/items/star_guide_book.json | 6 + .../assets/nerospace/lang/en_us.json | 98 ++++++++++ .../nerospace/models/block/star_guide.json | 104 ++++++++++ .../models/item/star_guide_book.json | 6 + .../nerospace/textures/block/star_guide.png | Bin 0 -> 501 bytes .../nerospace/textures/gui/star_guide.png | Bin 0 -> 6781 bytes .../textures/item/star_guide_book.png | Bin 0 -> 189 bytes .../loot_table/blocks/star_guide.json | 21 ++ .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeClientSetup.java | 2 + 24 files changed, 1079 insertions(+), 6 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/StarGuideBookItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideProgress.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/star_guide.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/star_guide.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/star_guide_book.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/star_guide.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/star_guide_book.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/star_guide.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/gui/star_guide.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/star_guide_book.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/star_guide.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index e3f39a0..cdb5242 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,25 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~202 classes ported, ~62 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~209 classes ported, ~55 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — Star Guide slice 1 (the browsable progression guide).** All 4 cells green (full +> `:neoforge:build`+`:fabric:build` on 26.2; compile on 26.1.2). Ported `progression/{StarGuide (9-chapter +> ×40-step content table), StarGuideProgress (reads advancements), StarGuideBlock (lectern pedestal), +> StarGuideBlockEntity (MenuProvider + next-step hologram compute/sync), StarGuideMenu}` + +> `item/StarGuideBookItem` + `client/StarGuideScreen` (built on the existing `TexturedContainerScreen` + +> `SpaceButton` — near-verbatim since the root already uses the 26.x submission model). Registered block + +> block-item + book item + BE + menu + per-loader screen; copied block/GUI/book assets + models + blockstate +> + loot table + **98 lang keys** (full chapter/step text). The guide opens from the **Star Guide Book** (in +> hand) or a **Star Guide pedestal** (install the book). **No `ModCriteria` needed** — the guide just reads +> advancement completion (missing advancements read as incomplete), sidestepping the 26.1↔26.2 criterion +> package split. Two steps (station_charter / new_life) use stand-in icons for the not-yet-ported +> STATION_CHARTER / LOPER_HAUNCH. **Deferred (slice 2):** the advancement DATA (so steps actually tick +> complete — the guide currently browses fully but tracks no completion until advancements land), the +> hologram BER (cosmetic; the BE already computes+syncs the stack), and the "seen-pulse" (needs a +> `STAR_GUIDE_SEEN` player-attachment seam). + > **2026-06-21 update — pipe fluid relay (closes the slice-A FLUID gap).** All 4 cells green (compile on > 26.2; full `:neoforge:build`+`:fabric:build` on 26.1.2). Added a `platform/FluidLookup` query seam > (mirrors `EnergyLookup`/`GasLookup`: common interface via `Services.load` + `NeoForgeFluidLookup` @@ -336,9 +352,16 @@ checked by a headless build). `Gui.setOverlayMessage(Component, boolean)` (the standalone mod's call) is gone from vanilla `Gui` — use `Player.sendOverlayMessage(Component)`** (probed). Proves the networking seam end-to-end. All 4 cells green. -### Star Guide / progression (`progression/` 5 + client + item) -- [ ] `StarGuide`, `StarGuideProgress`, `StarGuideBlock`(+BE), `StarGuideMenu` + screen, hologram BER, - `StarGuideBookItem`. Progression-tracking UI. +### Star Guide / progression (`progression/` 5 + client + item) — **slice 1 DONE (4 cells green)** +- [x] **Slice 1 — browsable guide.** `progression/{StarGuide, StarGuideProgress, StarGuideBlock, + StarGuideBlockEntity, StarGuideMenu}` + `item/StarGuideBookItem` + `client/StarGuideScreen`. Registered + block/block-item/book/BE/menu + per-loader screen + assets + 98 lang keys. Opens from the book (in hand) + or the pedestal (install the book). Reads advancement completion — **no `ModCriteria` dependency**. +- [ ] **Slice 2 (deferred).** Advancement DATA (so steps tick complete — currently the guide browses but + tracks no completion); the hologram BER (`StarGuideHologramRenderer`/`RenderState` — cosmetic; the BE + already computes + syncs the next-step stack); the "seen-pulse" (needs a `STAR_GUIDE_SEEN` player + attachment seam). Also the stand-in icons for station_charter / new_life resolve once STATION_CHARTER / + LOPER_HAUNCH are ported. ### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** - [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java new file mode 100644 index 0000000..c0eb65a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java @@ -0,0 +1,184 @@ +package za.co.neroland.nerospace.client; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.util.FormattedCharSequence; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.progression.StarGuide; +import za.co.neroland.nerospace.progression.StarGuideMenu; + +/** + * The Star Guide screen: a chapter rail on the left, the selected chapter's step nodes connected by a + * dotted trajectory line on the right (rocket-UI styling), and a guide-text panel underneath. + * Completed steps light up. + * + *

Cross-loader port: built on the multiloader {@link TexturedContainerScreen} + {@link SpaceButton} + * (same 26.x submission-model rendering as the standalone mod). Slice 1 shows completion only — the + * "seen pulse" rode the deferred {@code STAR_GUIDE_SEEN} attachment.

+ */ +public class StarGuideScreen extends TexturedContainerScreen { + + private static final Identifier TEXTURE = + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "textures/gui/star_guide.png"); + /** Star Guide accent: nerosium purple (the mod's signpost block). */ + private static final int ACCENT = 0xFFB05AE0; + private static final int DONE = 0xFF58D08A; + + private final List chapterButtons = new ArrayList<>(); + private final List stepButtons = new ArrayList<>(); + private int selectedChapter; + private int selectedStep; + + public StarGuideScreen(StarGuideMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, TEXTURE, ACCENT, 240, 200); + this.titleLabelX = 10; + this.inventoryLabelY = 10_000; // no player inventory on this panel + } + + @Override + protected void init() { + super.init(); + this.chapterButtons.clear(); + for (int i = 0; i < StarGuide.CHAPTER_COUNT; i++) { + final int chapter = i; + SpaceButton button = new SpaceButton(this.leftPos + 8, this.topPos + 22 + i * 17, 66, 14, + Component.translatable(StarGuide.CHAPTERS.get(i).titleKey()), ACCENT, + b -> selectChapter(chapter)); + this.addRenderableWidget(button); + this.chapterButtons.add(button); + } + rebuildStepButtons(); + } + + private void selectChapter(int chapter) { + this.selectedChapter = chapter; + this.selectedStep = 0; + rebuildStepButtons(); + } + + private void rebuildStepButtons() { + this.stepButtons.forEach(this::removeWidget); + this.stepButtons.clear(); + List steps = StarGuide.CHAPTERS.get(this.selectedChapter).steps(); + for (int i = 0; i < steps.size(); i++) { + final int step = i; + SpaceButton node = new SpaceButton(stepX(i), stepY(i), 42, 14, + Component.literal(String.valueOf(i + 1)), ACCENT, b -> selectStep(step)); + this.addRenderableWidget(node); + this.stepButtons.add(node); + } + } + + /** Serpentine layout: odd rows run right-to-left so the path snakes without crossing nodes. */ + private int stepX(int index) { + int col = index % 3; + if ((index / 3) % 2 == 1) { + col = 2 - col; + } + return this.leftPos + 82 + col * 52; + } + + private int stepY(int index) { + return this.topPos + 26 + (index / 3) * rowSpacing(); + } + + /** + * Vertical pitch between node rows, compressed for long chapters so every row stays inside the + * step canvas. + */ + private int rowSpacing() { + int steps = StarGuide.CHAPTERS.get(this.selectedChapter).steps().size(); + int rows = Math.max(1, (steps + 2) / 3); + return rows <= 1 ? 32 : Math.min(32, 54 / (rows - 1)); + } + + private void selectStep(int step) { + this.selectedStep = step; + } + + @Override + protected void extractForeground(GuiGraphicsExtractor g) { + List steps = StarGuide.CHAPTERS.get(this.selectedChapter).steps(); + + // Chapter rail: light fully-completed chapters. + for (int i = 0; i < this.chapterButtons.size(); i++) { + int total = StarGuide.CHAPTERS.get(i).steps().size(); + boolean allDone = Integer.bitCount(this.menu.completionMask(i)) >= total; + this.chapterButtons.get(i).setSelected(i == this.selectedChapter || allDone); + } + + // Dotted trajectory line linking the chapter's nodes in order (the progression path). + for (int i = 0; i < steps.size() - 1; i++) { + boolean done = this.menu.isStepComplete(this.selectedChapter, i); + int color = done ? DONE : 0xFF31506B; + if (i / 3 == (i + 1) / 3) { + int xa = Math.min(stepX(i), stepX(i + 1)) + 44; + int xb = Math.max(stepX(i), stepX(i + 1)) - 2; + dottedLine(g, xa, stepY(i) + 7, xb, stepY(i) + 7, color); + } else { + int cx = stepX(i) + 21; // same column as the next node (serpentine turn) + dottedLine(g, cx, stepY(i) + 15, cx, stepY(i + 1) - 1, color); + } + } + + // Step nodes: completed steps lit, with a completion pip. + for (int i = 0; i < this.stepButtons.size(); i++) { + boolean done = this.menu.isStepComplete(this.selectedChapter, i); + SpaceButton node = this.stepButtons.get(i); + node.setSelected(done); + if (done) { + g.fill(node.getX() + node.getWidth() - 5, node.getY() + 2, // completion pip + node.getX() + node.getWidth() - 2, node.getY() + 5, DONE); + } + } + + // Guide-text panel for the selected step. + StarGuide.Step step = steps.get(Math.min(this.selectedStep, steps.size() - 1)); + boolean stepDone = this.menu.isStepComplete(this.selectedChapter, this.selectedStep); + Component title = Component.translatable(step.titleKey()); + label(g, title, 82, 100, stepDone ? DONE : 0xFFE6D2FF); + if (stepDone) { + Component complete = Component.translatable("gui.nerospace.star_guide.complete"); + int tagX = 230 - this.font.width(complete); + if (tagX >= 82 + this.font.width(title) + 6) { + label(g, complete, tagX, 100, DONE); + } + } + // Description: wrapped to the text panel. + List lines = this.font.split( + Component.translatable(step.textKey()), 146); + int lineHeight = lines.size() > 8 ? 9 : 10; + int y = 112; + for (FormattedCharSequence line : lines) { + if (y + this.font.lineHeight > 193) { + break; + } + g.text(this.font, line, this.leftPos + 82, this.topPos + y, 0xFFB6C6D8, false); + y += lineHeight; + } + } + + /** A dotted 2px line between two points (axis-aligned or otherwise), ~5px pitch. */ + private static void dottedLine(GuiGraphicsExtractor g, int x0, int y0, int x1, int y1, int color) { + int steps = Math.max(1, (int) (Math.hypot(x1 - x0, y1 - y0) / 5.0D)); + for (int s = 0; s <= steps; s++) { + float t = s / (float) steps; + int ax = Math.round(Mth.lerp(t, x0, x1)); + int ay = Math.round(Mth.lerp(t, y0, y1)); + g.fill(ax, ay, ax + 2, ay + 2, color); + } + } + + @Override + protected void extractLabels(GuiGraphicsExtractor extractor, int mouseX, int mouseY) { + extractor.text(this.font, this.title, this.titleLabelX, this.titleLabelY, TITLE, false); + // No inventory label: the panel has no player slots. + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/StarGuideBookItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/StarGuideBookItem.java new file mode 100644 index 0000000..9af410e --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/StarGuideBookItem.java @@ -0,0 +1,37 @@ +package za.co.neroland.nerospace.item; + +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.progression.StarGuideMenu; + +/** + * The Star Guide Book: the key to the Star Guide pedestal, and a working copy of the guide on its own + * — used in hand it opens the same live progression tree. The menu is player-progress-backed, so no + * block is needed; the pedestal remains the in-world anchor with the hologram. + * + *

Cross-loader port: vanilla {@code SimpleMenuProvider} + {@code openMenu}; identical to the + * standalone mod.

+ */ +public class StarGuideBookItem extends Item { + + public StarGuideBookItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(new SimpleMenuProvider( + (id, inventory, p) -> new StarGuideMenu(id, inventory, p), + Component.translatable("container.nerospace.star_guide"))); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java new file mode 100644 index 0000000..4d020fe --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java @@ -0,0 +1,124 @@ +package za.co.neroland.nerospace.progression; + +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.resources.Identifier; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.ItemLike; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The Star Guide content table (chapters → steps, in code). Completion is advancement-driven — each + * step names the advancement that completes it, and the menu packs per-chapter completion bitmasks + * from {@code ServerPlayer.getAdvancements()}. Icons are suppliers because the table is built before + * registry objects exist. + * + *

Cross-loader port note: two steps reference content not yet ported (the station-founding + * {@code STATION_CHARTER} and the meadow-loper {@code LOPER_HAUNCH}); their icons are substituted with + * ported stand-ins (Station Floor / Drift Fleece). Those steps' advancements stay unresolved, so they + * read as incomplete — forward-looking guidance until those systems land.

+ */ +public final class StarGuide { + + /** One step of a chapter: icon + lang keys + the advancement that completes it. */ + public record Step(String id, Supplier icon, Identifier advancement) { + + public String titleKey() { + return "gui.nerospace.star_guide.step." + this.id; + } + + public String textKey() { + return "gui.nerospace.star_guide.step." + this.id + ".text"; + } + + public ItemStack iconStack() { + return new ItemStack(this.icon.get()); + } + } + + /** A chapter: lang key + ordered steps (≤ 16 so the completion bitmask fits a data slot). */ + public record Chapter(String id, List steps) { + + public String titleKey() { + return "gui.nerospace.star_guide.chapter." + this.id; + } + } + + private static Identifier adv(String path) { + return Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, path); + } + + private static Step step(String id, Supplier icon, String advancementPath) { + return new Step(id, icon, adv(advancementPath)); + } + + /** The chapters (order = chapter index used by menu completion bitmasks). */ + public static final List CHAPTERS = List.of( + new Chapter("nerosium", List.of( + step("raw_nerosium", () -> ModItems.RAW_NEROSIUM.get(), "guide/raw_nerosium"), + step("nerosium_ingot", () -> ModItems.NEROSIUM_INGOT.get(), "root"), + step("nerosium_pickaxe", () -> ModItems.NEROSIUM_PICKAXE.get(), "guide/nerosium_pickaxe"))), + new Chapter("machines", List.of( + step("nerosium_grinder", () -> ModBlocks.NEROSIUM_GRINDER.get(), "nerosium_grinder"), + step("nerosium_dust", () -> ModItems.NEROSIUM_DUST.get(), "guide/nerosium_dust"), + step("combustion_generator", () -> ModBlocks.COMBUSTION_GENERATOR.get(), "guide/combustion_generator"))), + new Chapter("power_grid", List.of( + step("universal_pipe", () -> ModBlocks.UNIVERSAL_PIPE.get(), "guide/universal_pipe"), + step("battery", () -> ModBlocks.BATTERY.get(), "guide/battery"), + step("passive_generator", () -> ModBlocks.PASSIVE_GENERATOR.get(), "guide/passive_generator"), + step("configurator", () -> ModItems.CONFIGURATOR.get(), "guide/configurator"))), + new Chapter("rocketry", List.of( + step("rocket_fuel_canister", () -> ModItems.ROCKET_FUEL_CANISTER.get(), "guide/rocket_fuel_canister"), + step("rocket_launch_pad", () -> ModBlocks.ROCKET_LAUNCH_PAD.get(), "guide/rocket_launch_pad"), + step("rocket_tier_1", () -> ModItems.ROCKET_TIER_1.get(), "rocket"), + step("station", () -> ModBlocks.STATION_FLOOR.get(), "station"), + // STATION_CHARTER not yet ported — substitute the Station Floor icon. + step("station_charter", () -> ModBlocks.STATION_FLOOR.get(), "guide/station_charter"))), + new Chapter("new_worlds", List.of( + step("rocket_tier_2", () -> ModItems.ROCKET_TIER_2.get(), "guide/rocket_tier_2"), + step("greenxertz", () -> ModItems.NEROSTEEL_INGOT.get(), "greenxertz"), + step("nerosteel_ingot", () -> ModItems.RAW_NEROSTEEL.get(), "guide/nerosteel_ingot"), + step("rocket_tier_3", () -> ModItems.ROCKET_TIER_3.get(), "guide/rocket_tier_3"), + step("cindara", () -> ModItems.CINDRITE.get(), "cindara"), + step("cindrite", () -> ModBlocks.CINDRITE_BLOCK.get(), "guide/cindrite"), + step("rocket_tier_4", () -> ModItems.ROCKET_TIER_4.get(), "guide/rocket_tier_4"), + step("glacira", () -> ModItems.GLACITE.get(), "glacira"), + step("glacite", () -> ModBlocks.GLACITE_BLOCK.get(), "guide/glacite"))), + new Chapter("mining", List.of( + step("quarry_landmark", () -> ModBlocks.QUARRY_LANDMARK.get(), "guide/quarry_landmark"), + step("frame_casing", () -> ModItems.FRAME_CASING.get(), "guide/frame_casing"), + step("quarry_controller", () -> ModBlocks.QUARRY_CONTROLLER.get(), "guide/quarry_controller"), + step("upgrade_module", () -> ModItems.SPEED_MODULE.get(), "guide/upgrade_module"))), + new Chapter("vacuum", List.of( + step("oxygen_generator", () -> ModBlocks.OXYGEN_GENERATOR.get(), "guide/oxygen_generator"), + step("gas_tank", () -> ModBlocks.GAS_TANK.get(), "guide/gas_tank"), + step("oxygen_suit", () -> ModItems.OXYGEN_SUIT_HELMET.get(), "guide/oxygen_suit"), + step("oxygen_suit_t2", () -> ModItems.OXYGEN_SUIT_T2_HELMET.get(), "guide/oxygen_suit_t2"), + step("thermal_suit", () -> ModItems.OXYGEN_SUIT_HEAT_HELMET.get(), "guide/thermal_suit"), + step("cryo_suit", () -> ModItems.OXYGEN_SUIT_COLD_HELMET.get(), "guide/cryo_suit"))), + new Chapter("terraforming", List.of( + step("terraformer", () -> ModBlocks.TERRAFORMER.get(), "guide/terraformer"), + step("terraformed_ground", () -> ModBlocks.TERRAFORMER.get(), "guide/terraformed_ground"), + step("hydration_module", () -> ModBlocks.HYDRATION_MODULE.get(), "guide/hydration_module"), + step("living_world", () -> ModItems.MEADOW_LOPER_SPAWN_EGG.get(), "guide/living_world"), + // LOPER_HAUNCH not yet ported — substitute the Drift Fleece icon. + step("new_life", () -> ModItems.DRIFT_FLEECE.get(), "guide/new_life"))), + new Chapter("meteor_events", List.of( + step("meteor_site", () -> ModItems.ALIEN_FRAGMENT.get(), "guide/alien_fragment"), + step("alien_tech", () -> ModItems.ALIEN_TECH_SCRAP.get(), "guide/alien_tech_scrap"), + step("alien_core", () -> ModItems.ALIEN_CORE.get(), "guide/alien_core")))); + + public static final int CHAPTER_COUNT = CHAPTERS.size(); + + private StarGuide() { + } + + /** Total step count across all chapters (sanity bound for menu button ids). */ + public static int totalSteps() { + return CHAPTERS.stream().mapToInt(c -> c.steps().size()).sum(); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlock.java new file mode 100644 index 0000000..7cc4731 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlock.java @@ -0,0 +1,121 @@ +package za.co.neroland.nerospace.progression; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The Star Guide pedestal block. Lectern-style: install a Star Guide Book to load it (right-click + * opens the progression tree); sneak-right-click returns the book; breaking a loaded pedestal drops + * both. + * + *

Cross-loader port: vanilla block interactions ({@code useItemOn}/{@code useWithoutItem} + + * {@code openMenu}); identical to the standalone mod.

+ */ +public class StarGuideBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(StarGuideBlock::new); + + public StarGuideBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new StarGuideBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.STAR_GUIDE.get(), + (lvl, pos, st, be) -> be.tick(lvl, pos, st)); + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + // Install a Star Guide Book on the bare pedestal. + if (stack.is(ModItems.STAR_GUIDE_BOOK.get()) + && level.getBlockEntity(pos) instanceof StarGuideBlockEntity guide && !guide.hasBook()) { + if (!level.isClientSide() && guide.installBook(stack)) { + level.playSound(null, pos, SoundEvents.BOOK_PUT, SoundSource.BLOCKS, 1.0F, 1.0F); + } + return InteractionResult.SUCCESS; + } + return super.useItemOn(stack, state, level, pos, player, hand, hit); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (!(level.getBlockEntity(pos) instanceof StarGuideBlockEntity guide)) { + return InteractionResult.PASS; + } + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (!guide.hasBook()) { + player.sendSystemMessage(Component.translatable("message.nerospace.star_guide.empty")); + return InteractionResult.SUCCESS; + } + if (player.isShiftKeyDown()) { + // Return the installed book. + ItemStack book = guide.removeBook(); + if (!book.isEmpty() && !player.addItem(book)) { + player.drop(book, false); + } + level.playSound(null, pos, SoundEvents.BOOK_PUT, SoundSource.BLOCKS, 1.0F, 0.8F); + return InteractionResult.SUCCESS; + } + if (player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(guide); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof StarGuideBlockEntity guide ? guide.comparatorSignal() : 0; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlockEntity.java new file mode 100644 index 0000000..bf00a64 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideBlockEntity.java @@ -0,0 +1,162 @@ +package za.co.neroland.nerospace.progression; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientGamePacketListener; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Containers; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * The Star Guide pedestal: holds the installed Star Guide Book and, while loaded, projects a hologram + * of the nearest player's NEXT incomplete progression step (their personal "you are here" marker). The + * hologram icon is computed server-side on a slow tick and synced via the vanilla block-entity update + * packet. + * + *

Cross-loader port: vanilla MenuProvider + value-IO + block-entity sync; identical to the + * standalone mod. (The client hologram renderer is the deferred cosmetic follow-up — the synced + * hologram stack is harmless until a BER draws it.)

+ */ +public class StarGuideBlockEntity extends BlockEntity implements MenuProvider { + + /** Server ticks between hologram refreshes (1s — progression changes are slow). */ + private static final int HOLOGRAM_INTERVAL = 20; + /** Players within this radius drive the hologram's next-step lookup. */ + private static final double HOLOGRAM_PLAYER_RANGE = 12.0D; + + private ItemStack book = ItemStack.EMPTY; + /** Icon of the nearest player's next incomplete step (client-synced; EMPTY = show the book). */ + private ItemStack hologram = ItemStack.EMPTY; + + public StarGuideBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.STAR_GUIDE.get(), pos, state); + } + + public boolean hasBook() { + return !this.book.isEmpty(); + } + + /** Installs one book item (lectern-style). @return true if the pedestal accepted it. */ + public boolean installBook(ItemStack stack) { + if (hasBook() || stack.isEmpty()) { + return false; + } + this.book = stack.split(1); + markChangedAndSync(); + return true; + } + + /** Removes and returns the installed book (EMPTY when the pedestal is bare). */ + public ItemStack removeBook() { + ItemStack removed = this.book; + this.book = ItemStack.EMPTY; + this.hologram = ItemStack.EMPTY; + markChangedAndSync(); + return removed; + } + + public ItemStack getBook() { + return this.book; + } + + /** The hologram stack the client renderer floats above the pedestal. */ + public ItemStack getHologram() { + return this.hologram; + } + + public int comparatorSignal() { + return hasBook() ? 15 : 0; + } + + // --- Ticking (server): refresh the hologram from the nearest player's progress ----------- + + public void tick(Level level, BlockPos pos, BlockState state) { + if (!(level instanceof ServerLevel serverLevel) || !hasBook() + || level.getGameTime() % HOLOGRAM_INTERVAL != 0L) { + return; + } + Player nearest = serverLevel.getNearestPlayer( + pos.getX() + 0.5D, pos.getY() + 0.5D, pos.getZ() + 0.5D, HOLOGRAM_PLAYER_RANGE, false); + ItemStack next = nearest instanceof ServerPlayer serverPlayer + ? StarGuideProgress.nextStepIcon(serverPlayer) + : ItemStack.EMPTY; + if (!ItemStack.isSameItemSameComponents(next, this.hologram)) { + this.hologram = next; + markChangedAndSync(); + } + } + + private void markChangedAndSync() { + setChanged(); + if (this.level != null && !this.level.isClientSide()) { + this.level.sendBlockUpdated(this.worldPosition, getBlockState(), getBlockState(), 3); + } + } + + /** Breaking a loaded pedestal pops the installed book (the block itself drops via loot). */ + @Override + public void preRemoveSideEffects(BlockPos pos, BlockState state) { + super.preRemoveSideEffects(pos, state); + if (this.level instanceof ServerLevel && hasBook()) { + Containers.dropItemStack(this.level, + pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, removeBook()); + } + } + + // --- Persistence + client sync ----------------------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.store("Book", ItemStack.OPTIONAL_CODEC, this.book); + output.store("Hologram", ItemStack.OPTIONAL_CODEC, this.hologram); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.book = input.read("Book", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY); + this.hologram = input.read("Hologram", ItemStack.OPTIONAL_CODEC).orElse(ItemStack.EMPTY); + } + + @Override + public Packet getUpdatePacket() { + return ClientboundBlockEntityDataPacket.create(this); + } + + @Override + public CompoundTag getUpdateTag(HolderLookup.Provider registries) { + return saveCustomOnly(registries); + } + + // --- MenuProvider -------------------------------------------------------------------------- + + @Override + public Component getDisplayName() { + return Component.translatable("container.nerospace.star_guide"); + } + + @Nullable + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory playerInventory, Player player) { + return new StarGuideMenu(containerId, playerInventory, player); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java new file mode 100644 index 0000000..e804538 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java @@ -0,0 +1,90 @@ +package za.co.neroland.nerospace.progression; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * Star Guide menu: no slots — just synced per-chapter completion bitmasks read live from the player's + * advancements (bit i = step i of that chapter). Data slots sync as shorts, so masks are safe while + * chapters stay ≤ 16 steps (enforced by {@link StarGuide}'s table shape). + * + *

Cross-loader port note (slice 1): the standalone mod also tracks a "seen" mask via a + * {@code STAR_GUIDE_SEEN} player attachment (completed-but-unseen steps pulse). That attachment is a + * separate cross-loader seam and is deferred — the multiloader menu syncs completion only.

+ */ +public class StarGuideMenu extends AbstractContainerMenu { + + public static final int DATA_COUNT = StarGuide.CHAPTER_COUNT; + + private final ContainerData data; + + /** Client constructor (referenced by the {@code MenuType}). */ + public StarGuideMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, playerInventory.player); + } + + /** Server constructor: data reads live from the player's advancements. */ + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public StarGuideMenu(int containerId, Inventory playerInventory, Player player) { + super(ModMenuTypes.STAR_GUIDE.get(), containerId); + this.data = player instanceof ServerPlayer serverPlayer + ? new ProgressData(serverPlayer) + : new SimpleContainerData(DATA_COUNT); + checkContainerDataCount(this.data, DATA_COUNT); + this.addDataSlots(this.data); + } + + @Override + public boolean stillValid(Player player) { + return true; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; // no slots + } + + // --- Screen helpers ------------------------------------------------------------------------ + + public int completionMask(int chapter) { + return this.data.get(chapter); + } + + public boolean isStepComplete(int chapter, int step) { + return (completionMask(chapter) & (1 << step)) != 0; + } + + /** Live server-side view: per-chapter completion masks from the player's advancements. */ + private static final class ProgressData implements ContainerData { + + private final ServerPlayer player; + + ProgressData(ServerPlayer player) { + this.player = player; + } + + @Override + public int get(int index) { + return index >= 0 && index < StarGuide.CHAPTER_COUNT + ? StarGuideProgress.chapterMask(this.player, index) + : 0; + } + + @Override + public void set(int index, int value) { + // Read-only from the client. + } + + @Override + public int getCount() { + return DATA_COUNT; + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideProgress.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideProgress.java new file mode 100644 index 0000000..cda3897 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideProgress.java @@ -0,0 +1,56 @@ +package za.co.neroland.nerospace.progression; + +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.server.ServerAdvancementManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.progression.StarGuide.Chapter; +import za.co.neroland.nerospace.progression.StarGuide.Step; + +/** + * Server-side progression queries: completion is read straight from the player's advancements — the + * guide never keeps its own completion state. Unresolvable advancement ids (a step whose advancement + * is missing from the datapack) count as incomplete. + * + *

Cross-loader port: pure vanilla (advancement manager); identical to the standalone mod.

+ */ +public final class StarGuideProgress { + + private StarGuideProgress() { + } + + /** Whether {@code step} is complete for {@code player}. */ + public static boolean isComplete(ServerPlayer player, Step step) { + ServerAdvancementManager manager = player.level().getServer().getAdvancements(); + AdvancementHolder holder = manager.get(step.advancement()); + return holder != null && player.getAdvancements().getOrStartProgress(holder).isDone(); + } + + /** Per-chapter completion bitmask (bit i = step i complete). */ + public static int chapterMask(ServerPlayer player, int chapterIndex) { + Chapter chapter = StarGuide.CHAPTERS.get(chapterIndex); + int mask = 0; + for (int i = 0; i < chapter.steps().size(); i++) { + if (isComplete(player, chapter.steps().get(i))) { + mask |= 1 << i; + } + } + return mask; + } + + /** + * The icon of the player's FIRST incomplete step in chapter order — the pedestal hologram's + * "you are here" marker. EMPTY once everything is complete (the hologram falls back to the book). + */ + public static ItemStack nextStepIcon(ServerPlayer player) { + for (Chapter chapter : StarGuide.CHAPTERS) { + for (Step step : chapter.steps()) { + if (!isComplete(player, step)) { + return step.iconStack(); + } + } + } + return ItemStack.EMPTY; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 03a42ea..5cdc9d9 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -19,6 +19,7 @@ import za.co.neroland.nerospace.machine.TerraformerBlockEntity; import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; +import za.co.neroland.nerospace.progression.StarGuideBlockEntity; import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; import za.co.neroland.nerospace.storage.CreativeFluidTankBlockEntity; import za.co.neroland.nerospace.storage.CreativeGasTankBlockEntity; @@ -131,6 +132,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("terraform_monitor", key -> new BlockEntityType<>(TerraformMonitorBlockEntity::new, java.util.Set.of(ModBlocks.TERRAFORM_MONITOR.get()))); + public static final RegistryEntry> STAR_GUIDE = + BLOCK_ENTITIES.register("star_guide", + key -> new BlockEntityType<>(StarGuideBlockEntity::new, java.util.Set.of(ModBlocks.STAR_GUIDE.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 2f67423..848ac5c 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -29,6 +29,7 @@ import za.co.neroland.nerospace.machine.quarry.QuarryLandmarkBlock; import za.co.neroland.nerospace.meteor.MeteorCoreBlock; import za.co.neroland.nerospace.pipe.UniversalPipeBlock; +import za.co.neroland.nerospace.progression.StarGuideBlock; import za.co.neroland.nerospace.rocket.LaunchGantryBlock; import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; import za.co.neroland.nerospace.storage.CreativeBatteryBlock; @@ -216,6 +217,11 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_GREEN).strength(3.0F, 6.0F) .requiresCorrectToolForDrops().lightLevel(s -> 7).sound(SoundType.METAL))); + public static final RegistryEntry STAR_GUIDE = BLOCKS.register("star_guide", + key -> new StarGuideBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_PURPLE).strength(3.0F, 6.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 7).sound(SoundType.METAL))); + public static final RegistryEntry SOLAR_PANEL = BLOCKS.register("solar_panel", key -> new SolarPanelBlock(BlockBehaviour.Properties.of() .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index c86996e..d95a51b 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -35,6 +35,7 @@ import za.co.neroland.nerospace.item.NerospaceSpawnEggItem; import za.co.neroland.nerospace.item.PipeFilterItem; import za.co.neroland.nerospace.item.PipeUpgradeItem; +import za.co.neroland.nerospace.item.StarGuideBookItem; import za.co.neroland.nerospace.meteor.MeteorCallerItem; import za.co.neroland.nerospace.module.ModuleType; import za.co.neroland.nerospace.module.UpgradeModuleItem; @@ -132,6 +133,11 @@ public final class ModItems { public static final RegistryEntry CAPACITY_UPGRADE = ITEMS.register("capacity_upgrade", key -> new PipeUpgradeItem(new Item.Properties().setId(key), PipeUpgradeItem.Kind.CAPACITY)); + // --- Star Guide (progression pedestal + book) --------------------------- + public static final RegistryEntry STAR_GUIDE_ITEM = blockItem("star_guide", ModBlocks.STAR_GUIDE); + public static final RegistryEntry STAR_GUIDE_BOOK = ITEMS.register("star_guide_book", + key -> new StarGuideBookItem(new Item.Properties().stacksTo(1).setId(key))); + // --- Machine upgrade modules (the quarry is the first consumer) ---------- public static final RegistryEntry SPEED_MODULE = module("speed_module", ModuleType.SPEED); public static final RegistryEntry EFFICIENCY_MODULE = module("efficiency_module", ModuleType.EFFICIENCY); @@ -280,7 +286,8 @@ public static Map, List> creativeTabItems ROCKET_TIER_1.get(), ROCKET_TIER_2.get(), ROCKET_TIER_3.get(), ROCKET_TIER_4.get(), GREENXERTZ_NAVIGATOR.get(), STATION_COMPASS.get(), GREENXERTZ_COMPASS.get(), CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get(), METEOR_TRACKER.get(), - CONFIGURATOR.get(), PIPE_FILTER.get(), SPEED_UPGRADE.get(), CAPACITY_UPGRADE.get()), + CONFIGURATOR.get(), PIPE_FILTER.get(), SPEED_UPGRADE.get(), CAPACITY_UPGRADE.get(), + STAR_GUIDE_BOOK.get()), CreativeModeTabs.SPAWN_EGGS, List.of(XERTZ_STALKER_SPAWN_EGG.get(), QUARTZ_CRAWLER_SPAWN_EGG.get(), GREENLING_SPAWN_EGG.get(), ALIEN_VILLAGER_SPAWN_EGG.get(), CINDER_STALKER_SPAWN_EGG.get(), @@ -295,7 +302,8 @@ public static Map, List> creativeTabItems CreativeModeTabs.FUNCTIONAL_BLOCKS, List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), HYDRATION_MODULE_ITEM.get(), TERRAFORM_MONITOR_ITEM.get(), SPEED_MODULE.get(), EFFICIENCY_MODULE.get(), FORTUNE_MODULE.get(), SILK_TOUCH_MODULE.get(), - CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get())); + CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get(), + STAR_GUIDE_ITEM.get())); } /** diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 5627a50..2239475 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -14,6 +14,7 @@ import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; import za.co.neroland.nerospace.menu.TerraformMonitorMenu; import za.co.neroland.nerospace.menu.TerraformerMenu; +import za.co.neroland.nerospace.progression.StarGuideMenu; import za.co.neroland.nerospace.rocket.RocketMenu; import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; @@ -63,6 +64,10 @@ public final class ModMenuTypes { MENUS.register("terraform_monitor", key -> new MenuType<>(TerraformMonitorMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> STAR_GUIDE = + MENUS.register("star_guide", + key -> new MenuType<>(StarGuideMenu::new, FeatureFlags.VANILLA_SET)); + private ModMenuTypes() { } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/star_guide.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/star_guide.json new file mode 100644 index 0000000..a7eb2b3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/star_guide.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/star_guide" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/star_guide.json b/multiloader/common/src/main/resources/assets/nerospace/items/star_guide.json new file mode 100644 index 0000000..f9f2172 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/star_guide.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/star_guide" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/star_guide_book.json b/multiloader/common/src/main/resources/assets/nerospace/items/star_guide_book.json new file mode 100644 index 0000000..d214c67 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/star_guide_book.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/star_guide_book" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 1fba814..76bc39e 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -50,6 +50,7 @@ "block.nerospace.rocket_launch_pad.report.t3_not_ready": "Tier 3 needs a Station Wall ring or a Heavy Launch Complex", "block.nerospace.rocket_launch_pad.report.t3_ready": "Tier 3 ready: Station Wall ring or Heavy complex present", "block.nerospace.solar_panel": "Solar Panel", + "block.nerospace.star_guide": "Star Guide", "block.nerospace.station_floor": "Station Floor", "block.nerospace.station_wall": "Station Wall", "block.nerospace.terraform_monitor": "Terraform Monitor", @@ -67,6 +68,7 @@ "container.nerospace.passive_generator": "Passive Generator", "container.nerospace.quarry_controller": "Quarry Controller", "container.nerospace.rocket": "Rocket", + "container.nerospace.star_guide": "Star Guide", "container.nerospace.terraform_monitor": "Terraform Monitor", "container.nerospace.terraformer": "Terraformer", "entity.nerospace.alien_villager": "Alien Villager", @@ -94,6 +96,100 @@ "gui.nerospace.quarry.state.mining": "Mining", "gui.nerospace.quarry.state.paused": "Paused", "gui.nerospace.rocket.launch": "Launch", + "gui.nerospace.star_guide.chapter.machines": "Machines", + "gui.nerospace.star_guide.chapter.meteor_events": "Meteor Events", + "gui.nerospace.star_guide.chapter.mining": "Mining", + "gui.nerospace.star_guide.chapter.nerosium": "Nerosium", + "gui.nerospace.star_guide.chapter.new_worlds": "New Worlds", + "gui.nerospace.star_guide.chapter.power_grid": "Power Grid", + "gui.nerospace.star_guide.chapter.rocketry": "Rocketry", + "gui.nerospace.star_guide.chapter.terraforming": "Terraforming", + "gui.nerospace.star_guide.chapter.vacuum": "Vacuum", + "gui.nerospace.star_guide.complete": "COMPLETE", + "gui.nerospace.star_guide.step.alien_core": "Alien Core", + "gui.nerospace.star_guide.step.alien_core.text": "The rarest meteor prize. An intact Alien Core is the key to the deepest alien technology — the heart of a future upgrade tree.", + "gui.nerospace.star_guide.step.alien_tech": "Salvaged Tech", + "gui.nerospace.star_guide.step.alien_tech.text": "Rarer meteors carry Alien Tech Scrap — the raw material of the scanners and upgrades to come. Hoard it.", + "gui.nerospace.star_guide.step.battery": "Stored Potential", + "gui.nerospace.star_guide.step.battery.text": "Batteries buffer the grid: generators fill them, machines drain them through the pipe network. Build one before your first rocket launch window.", + "gui.nerospace.star_guide.step.cindara": "Into the Fire", + "gui.nerospace.star_guide.step.cindara.text": "Cindara is a volcanic moon: fire-immune stalkers, lava fields and cindrite. Heat-graded gear recommended.", + "gui.nerospace.star_guide.step.cindrite": "Heart of the Volcano", + "gui.nerospace.star_guide.step.cindrite.text": "Cindrite crystals upgrade your oxygen suit to Tier 2 and gate the deepest progression. Mine them from Cindara's stone.", + "gui.nerospace.star_guide.step.combustion_generator": "Burning Bright", + "gui.nerospace.star_guide.step.combustion_generator.text": "The Combustion Generator burns coal, charcoal or fuel canisters into energy. It is your first power source — pipe its output to your machines.", + "gui.nerospace.star_guide.step.configurator": "Fine Tuning", + "gui.nerospace.star_guide.step.configurator.text": "The Configurator sets per-face pipe modes (in / out / off) so you can route exactly what flows where.", + "gui.nerospace.star_guide.step.cryo_suit": "Dressed for the Deep Cold", + "gui.nerospace.star_guide.step.cryo_suit.text": "Glacira's cold drains an unprotected suit four times faster and frosts your visor. The glacite-lined Cryo Suit keeps you warm — all four pieces, or no shield.", + "gui.nerospace.star_guide.step.frame_casing": "Frameworks", + "gui.nerospace.star_guide.step.frame_casing.text": "Frame Casing is a hollow ring of nerosteel. The quarry spends one casing per frame block as it materialises its glowing structural frame around the region.", + "gui.nerospace.star_guide.step.gas_tank": "Bottled Air", + "gui.nerospace.star_guide.step.gas_tank.text": "Gas Tanks store oxygen piped from a generator. A tank by your base door acts as an airlock — suits refill from it automatically.", + "gui.nerospace.star_guide.step.glacira": "Into the Cold", + "gui.nerospace.star_guide.step.glacira.text": "Glacira is an ice moon: pale frozen plains, stilt-legged striders and glacite. The last stop before you turn a world green.", + "gui.nerospace.star_guide.step.glacite": "Heart of the Glacier", + "gui.nerospace.star_guide.step.glacite.text": "Glacite is crystallised water-ice — the key to cold-weather suits and, one day, to giving a terraformed world its water. Mine it from Glacira's stone.", + "gui.nerospace.star_guide.step.greenxertz": "A Whole New World", + "gui.nerospace.star_guide.step.greenxertz.text": "Greenxertz: a green-steel world of nerosteel and xertz quartz — and the creatures that guard them. The air is thin; bring a suit.", + "gui.nerospace.star_guide.step.hydration_module": "Meltwater", + "gui.nerospace.star_guide.step.hydration_module.text": "A Hydration Module touching your Terraformer melts glacite into water for the Hydrated stage — basins fill into lakes behind the green frontier.", + "gui.nerospace.star_guide.step.living_world": "World Awake", + "gui.nerospace.star_guide.step.living_world.text": "Behind the water, the land matures: natural colour, trees, rain — and the first herds. Stand on Living ground and watch a world breathe on its own.", + "gui.nerospace.star_guide.step.meteor_site": "Visitor from Beyond", + "gui.nerospace.star_guide.step.meteor_site.text": "Meteors crash on the Overworld and the planets, leaving a small crater around a glowing Meteor Core. Break the core to claim Alien Fragments and a jump-start of off-world ores — hold a Meteor Tracker to find the way.", + "gui.nerospace.star_guide.step.nerosium_dust": "Finely Ground", + "gui.nerospace.star_guide.step.nerosium_dust.text": "Grind raw nerosium into dust, then smelt the dust into ingots — two for one.", + "gui.nerospace.star_guide.step.nerosium_grinder": "Industrial Revolution", + "gui.nerospace.star_guide.step.nerosium_grinder.text": "The Nerosium Grinder doubles your ore yield by grinding raw nerosium into dust. It needs power from the grid — see the Power Grid chapter.", + "gui.nerospace.star_guide.step.nerosium_ingot": "First Smelt", + "gui.nerospace.star_guide.step.nerosium_ingot.text": "Smelt raw nerosium in any furnace. The ingot is your basic crafting metal for machines, rockets and tools.", + "gui.nerospace.star_guide.step.nerosium_pickaxe": "Tools of the Trade", + "gui.nerospace.star_guide.step.nerosium_pickaxe.text": "Nerosium tools sit at iron tier with better durability. You will be mining a lot — craft the pickaxe.", + "gui.nerospace.star_guide.step.nerosteel_ingot": "Alien Alloy", + "gui.nerospace.star_guide.step.nerosteel_ingot.text": "Nerosteel is Greenxertz's primary metal — mine the ore and smelt it. Higher-tier rockets and suits are built from it.", + "gui.nerospace.star_guide.step.new_life": "New Life", + "gui.nerospace.star_guide.step.new_life.text": "Each planet wakes its own livestock: the Meadow Loper, the Ember Strutter, the Woolly Drift. Feed a pair their favourite crop and breed the first generation born off Earth.", + "gui.nerospace.star_guide.step.oxygen_generator": "Something to Breathe", + "gui.nerospace.star_guide.step.oxygen_generator.text": "The Oxygen Generator electrolyses grid power into oxygen and pressurises the space around it. Sealed rooms fill completely; open air only holds a bubble.", + "gui.nerospace.star_guide.step.oxygen_suit": "Suit Up", + "gui.nerospace.star_guide.step.oxygen_suit.text": "A full four-piece Oxygen Suit is portable life support: a finite air tank that drains slowly off safe zones and refills at airlocks.", + "gui.nerospace.star_guide.step.oxygen_suit_t2": "Ember-Proof", + "gui.nerospace.star_guide.step.oxygen_suit_t2.text": "The cindrite-upgraded Tier 2 suit doubles your air and refills twice as fast. A mixed set counts as Tier 1 — wear all four pieces.", + "gui.nerospace.star_guide.step.passive_generator": "Slow and Steady", + "gui.nerospace.star_guide.step.passive_generator.text": "The Passive Generator trickles energy from a nerosium core for a long time — weaker than combustion but completely hands-off.", + "gui.nerospace.star_guide.step.quarry_controller": "Strip Miner", + "gui.nerospace.star_guide.step.quarry_controller.text": "Place the Controller beside the landmarks, load it with Frame Casing and feed it power. It builds the frame, then a gantry-mounted drill excavates the interior layer by layer down to bedrock — sucking up liquids, skipping tile-entity columns, and auto-ejecting drops into adjacent storage or pipes.", + "gui.nerospace.star_guide.step.quarry_landmark": "Stake a Claim", + "gui.nerospace.star_guide.step.quarry_landmark.text": "Place three Quarry Landmarks in an L to mark a rectangle — they project guide lines along the ground. Their reference height is the top of the dig.", + "gui.nerospace.star_guide.step.raw_nerosium": "Strange Red Rock", + "gui.nerospace.star_guide.step.raw_nerosium.text": "Nerosium ore veins thread the overworld's stone. Mine them with an iron pickaxe or better — the raw red metal is the seed of everything that follows.", + "gui.nerospace.star_guide.step.rocket_fuel_canister": "Highly Flammable", + "gui.nerospace.star_guide.step.rocket_fuel_canister.text": "Rockets burn rocket fuel. Craft canisters and fill them — or pump fuel straight into a pad-side rocket from a Fuel Tank.", + "gui.nerospace.star_guide.step.rocket_launch_pad": "Ground Control", + "gui.nerospace.star_guide.step.rocket_launch_pad.text": "A rocket deploys onto a full 3x3 of Launch Pad blocks. A Fuel Tank next to the pad auto-fuels the rocket — four times faster on the full 3x3.", + "gui.nerospace.star_guide.step.rocket_tier_1": "We Have Liftoff", + "gui.nerospace.star_guide.step.rocket_tier_1.text": "The Tier 1 rocket reaches the Orbital Station. Deploy it on the pad, board, fuel up and hit launch.", + "gui.nerospace.star_guide.step.rocket_tier_2": "Bigger Boosters", + "gui.nerospace.star_guide.step.rocket_tier_2.text": "Tier 2 adds Greenxertz to your destinations. It needs station materials, so establish yourself in orbit first.", + "gui.nerospace.star_guide.step.rocket_tier_3": "To the Fire Moon", + "gui.nerospace.star_guide.step.rocket_tier_3.text": "Tier 3 reaches Cindara. Its launch pad must be ringed with Station Wall — the blast would melt anything less.", + "gui.nerospace.star_guide.step.rocket_tier_4": "To the Ice Moon", + "gui.nerospace.star_guide.step.rocket_tier_4.text": "Tier 4 reaches Glacira, the frozen edge of the system. Built around a cindrite core, it launches only from a Heavy Launch Complex (5x5 pad + gantry).", + "gui.nerospace.star_guide.step.station": "Orbital", + "gui.nerospace.star_guide.step.station.text": "The Orbital Station is your first destination — vacuum-cold and airless. Stay near the pad's safe zone until you have oxygen gear.", + "gui.nerospace.star_guide.step.station_charter": "Homestead in Orbit", + "gui.nerospace.star_guide.step.station_charter.text": "Craft a Station Charter (rename it in an anvil to name your station), pick the FOUND node in any rocket and launch. The Station Core anchors it — break the Core to unregister and reclaim the charter.", + "gui.nerospace.star_guide.step.terraformed_ground": "Green Again", + "gui.nerospace.star_guide.step.terraformed_ground.text": "Terraformed ground is permanently breathable. Stand on land your machine reclaimed and breathe without a suit — the end of the beginning.", + "gui.nerospace.star_guide.step.terraformer": "World Engine", + "gui.nerospace.star_guide.step.terraformer.text": "The Terraformer converts dead ground into living, breathable terrain in an expanding circle. Energy is the throttle — feed it well.", + "gui.nerospace.star_guide.step.thermal_suit": "Forged for the Fire", + "gui.nerospace.star_guide.step.thermal_suit.text": "Cindara's heat makes any other suit burn air four times faster. The Thermal Suit (Tier 2 piece + cindrite) shrugs it off — all four pieces, or no shield.", + "gui.nerospace.star_guide.step.universal_pipe": "Connect Everything", + "gui.nerospace.star_guide.step.universal_pipe.text": "One pipe carries everything: energy, fluids, gas and items. Pipes form networks that balance their contents and feed adjacent machines.", + "gui.nerospace.star_guide.step.upgrade_module": "Tune It Up", + "gui.nerospace.star_guide.step.upgrade_module.text": "Slot cross-machine upgrade modules into the Controller: Speed digs faster, Efficiency cuts power use, and Fortune or Silk Touch change what the dig drops — just like enchanting your pickaxe.", "gui.nerospace.terraform_monitor.hydration": "Water: %s", "gui.nerospace.terraform_monitor.no_link": "No Terraformer within 32 blocks", "gui.nerospace.terraform_monitor.radii": "Radii: %s / %s / %s", @@ -187,6 +283,7 @@ "item.nerospace.silk_touch_module": "Silk Touch Module", "item.nerospace.speed_module": "Speed Module", "item.nerospace.speed_upgrade": "Speed Upgrade", + "item.nerospace.star_guide_book": "Star Guide Book", "item.nerospace.station_compass": "Station Compass", "item.nerospace.woolly_drift_spawn_egg": "Woolly Drift Spawn Egg", "item.nerospace.xertz_quartz": "Xertz Quartz", @@ -194,6 +291,7 @@ "item.nerospace.xertz_stalker_spawn_egg": "Xertz Stalker Spawn Egg", "itemGroup.nerospace": "Nerospace", "message.nerospace.greenxertz.no_air": "You are out of oxygen — reach a launch pad or an Oxygen Generator!", + "message.nerospace.star_guide.empty": "Place a Star Guide Book on the pedestal to open the guide", "pipe.nerospace.face.down": "Bottom", "pipe.nerospace.face.east": "East", "pipe.nerospace.face.north": "North", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/star_guide.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/star_guide.json new file mode 100644 index 0000000..dbb0216 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/star_guide.json @@ -0,0 +1,104 @@ +{ + "elements": [ + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 1, + 0, + 1 + ], + "to": [ + 15, + 3, + 15 + ] + }, + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 5, + 3, + 5 + ], + "to": [ + 11, + 10, + 11 + ] + }, + { + "faces": { + "down": { + "texture": "#all" + }, + "east": { + "texture": "#all" + }, + "north": { + "texture": "#all" + }, + "south": { + "texture": "#all" + }, + "up": { + "texture": "#all" + }, + "west": { + "texture": "#all" + } + }, + "from": [ + 2, + 10, + 2 + ], + "to": [ + 14, + 13, + 14 + ] + } + ], + "textures": { + "all": "nerospace:block/star_guide", + "particle": "nerospace:block/star_guide" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/star_guide_book.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/star_guide_book.json new file mode 100644 index 0000000..eeb0f66 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/star_guide_book.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/star_guide_book" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/star_guide.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/star_guide.png new file mode 100644 index 0000000000000000000000000000000000000000..444639e05be8e6da42499e82c53f3fff3672a600 GIT binary patch literal 501 zcmVjTBZf}{X1b{&t zA|9nQGnJr|G#*?%%=t42$3Q&nU5|+8soSBGG=jpMcYOpbiyMFu5N_U_uTV;pD9xb-8{<326^$OcIWO>OT4yAN7IzChn9WhmnZJY6y`!RJ> z)#xOR>34hk0tl@d#9`ZpZg0u*lBaIR;ktJ{-t`d@0ikVwKmNc}H6lK$F1FWk^B@kL zq_GPT5RyN=y@<7ncxEbzs@>j_7l!qG8P_N8AW(y@QhE|>fW&|S9-j?X4|B4-#8fqd rIG(!Q0}n6Rl@G=)O0%=|q6C)7zvsp!@-ozo+MV5eA-Xc3FMD%yr9`eJ(e_=zFhf z5U&KX7TNuG6D+m5mOj1T5+Nub*#WCum%-i|YN79U(EjL}ZkF`~)Mcjur}{`+W_cO4 zvs~l2x(zd0H+_0U|NKM8p3_D1^<(fE(j~u(QdF)stE_z6wyL}}?dr&KX^l&OX-#2q zokwO#_qL~uK71SfXWx0v0)5%6{jMQk{P)6P)%LkhNROK3+I`lo;B^n^zWkzSu8wXq z$OH>@C(=WM|22^Y=SEy#{XQc1>S$DX!sI$ke+I}Cy&!rb_3b(=OqZ}lydofehj!gX z+T-Oub4oK=Qh-)^Sr46yI#aM!NO-f(=heB1fS!=k=MNA1PQPkOoS|+dv_{jjc@uJ5 zn@d{WgmKy?HMALeS?I@ZCn|5Vo#``9%!mJH5KE1eID@1qWp-zo96sK!lvNG3(R@rX zn5$dz3ETyyoqpl+w1NWg6CT?^(M^hT_|}%trfUuHl+&JP@1tFqr$^ZYBkNgWShErg zC`JjYuxwF_*K1A63G*OG^z$3IWE{>gH6k$hPhcX2$K{*Tlh+AiU!o7^62`6HLq`-R z{9viLW}-{T#9;?T0?hF4xJt=oaPQUWG?a> znWe)j#HORlYh8-F>c1PF073WxivltDR1&kDPMt&dBkyx%zCvbl2Y>&u|v*Z@8*z!H* ziLJIZJhSyQ;j^#;3+E&94NJd*kN0ty=>$LFyStS+H`!#X@&aXNuTLYho0i&-E%(+7 zYRedJ4iHP((>ya%*ZXAQbT!U|pt_H#dEbAP%q~hYI!k9aaH16)=ZO6^ z%x`N&l>NhyUG8#HgtT_fYM2REdbm=)g?H|tHE)DYp%PSE^ZQ2qp0ew`=qQ4R2P}0h zmdcswQ}Y(p(`jxHnbCLfE*EjL-i&x&8HD&kZvwoA&aoPaFu9d*$rn(}!W)qh~*S2M=CMqrS_z%W}apQVnzyJg%D8tpu7 z^Z-?CZ_2Ilg==yt{N)%?WPc;YuTeH~?G#d9v;D7Ok#)nZ97zJJbByd-^YRI;H{Jlpj^ z$+U}lE3`CwT@Y6;??=feOb<_#niBI;d}H3QJZqEo5@&AaoY`Tk7cm0u)zkU258}n2 zmnmm!#_<7PA=&{7t3qLYZx`}6zn-4AuZrnOf;|Qc0pvEP|rZM_VyyxLflh5tA7~wA5Zi>?zIt%j2H5|+<@8#6TJV=^;^&{6nv>m213I8Y>#xuC zW7C_HTJ<2_E-X2(kU#QCy@m>LzHr7H&y9|8H|2dRMcH34KSO!diV zU>-W|tAq#7NDOHYX$M!HdE4nYRq~Y;fyW?$qfc%>C-99F4INDO+xIuPZ3_I?llk*- zyD;Hm7VKSBJ|OyEQ2hM54f&D8<01j012ZSi!1Uz|glq6?{m#qMhH{Uh?c|29_=Yws zU(oDm!~vo@OZy$mi${3L$}afP2nEx_V&Z}!zOATX-ttQV4M zUy2`@pkI+2yS(TGr)TJ(UllU0kc|^mtzKq%NuoYKsF^+Y?HiUazUVAXZz)D7 zRD-iD-gwYavL#-uwx(P(^ZR)TY9HupVuj!JH)(hkQwp{iIVjqCr0@d0n1rm2hl@)yyktQI{`I?A~%&Q6cYH zt$WL?^bgcZ(>&wAo#W)-l)5}sqfa|;Moxc?d3YwGTG~YR)KYwJrsKR9$yd!@4`nC! zkO)LQzZt%r$_NnIk1s9tm~N|{E3g$rZVgVg0Zh&x*A7b*!uYYl9Cp6A>~oF@IlVWl zwf$Ezx-l0zQu&_umzdM9nETnP`O~p&0}ERG3%a?!Zw_mfxQq<98@ahGf8*L=P&0j`W0b<(x?8@l7Gcm`s}OFq zo_bLtd)r>oQ(oHxvw_)8j^(D@&tWFcA}^2pg24}?$+1;mx1b0?4t4Z6zLSP@_mh{-^1$u6~hhG|{oJ$(CPyLm>H$2{? zJ3f}n{J*;O_XreWggM;Od-UxnN3{2;aEZvYoP5tihk$9*bJvxmhtu;q-n0M7cZvcv zMaskJybk-E9*R8_+};++8n3(U7-v2%t8UIm<>VbFF(jCaSX4jtec5FL<8N5BT#XT{ z5|l>~CZKX;8K;tP+MfJsv#6*1J1q zA{v!nvxKhfX>;|iJ8C}~l;^)E#TMVV;HoKdVC=FD49i}=wd<)3uA(zh(vK~2336)7 zqpjtC@Otxk|9Z2WF{^Iiq!;?z(Z29QF{eBwR_eev_t#9530E( zaTTf3rvQ?7rk-pJeyG8PdR$OHf|E6faio@d0~_EZzk^~}?4Pm_A%ECVO_;)a!FB2O zoG^L-{-|=%A8(f5_8h(1oApO^gV4oSq~?Si#3B|}ktch8TaCxAwBlYu+P(T0_f=D+8AS(Dgm)E%X`Usa4C`TTYIc+lV@Q5{R(Y99<}|LGG?D z#sn&q=H6#a=}M-UY91XvVJei=@wUD(qg#@yf4l@2N~!1WFg`XOL^cX35)vfB^X#Jq zWa|Wo?>bt1B4H*pf`db9e-gB*$hEyrCsoux#GK6FL5j;g0(B%X6SSrdOiRR&lELQ7 z&%tCBgpA+C+WN-l9 zBKxbyrJ>=;3)#RrRpgHl9V)LG*OFb0fNu}s+;cT6p?ug%vd!$pSp76R!IovpH(!?) zcfu-oxj6}8^q<)LG@-Pewf>Y$r$)w# z^zWmJG70$>0FjfI{-Ic7{*VE^v!8#8_|o21zIE*$4TYf|DL6?=5kqKbecx0yN{!lU{A0a^{+31Bft0@Cc!BRw%D<;Boc~ z($EuODpzQEoWLxog+{0vkPa8G&VuIbZQT@Xogb^P2G@+HLBl6>g=oXI*AJN!cQook z1mUNNZw16zO>r6-g@1|i0;MRJH(WlOp)Q~L1LJ`JS$@$q(ciLA^Q90;W@67H0G1$? zr|n~=A&z==0A~g24{WA5BJsrKsEGVnQblHa(4RbLj!JS3in9sArq4&HR@d5V8NJxw z#_YrGe0|SM+D&<-4cC74YjOVyqD@uz;Hqt%kU#8T=g*!!q@7kM_=XJ4)-t1J-FmeIkcn2r8zvb6t2yE0R+yPB-O zYbu@$h)xD9N07LFVMs8adG&t38PbV=>;+ zsNyaR&<|+Au2oMMsLE&rZa=I-J0=0W1VNVlzgP0EWT6XWF}!u8d>q-Ta`l~!%Q>C| z(cvdeco1P8p`Ha&DzlhAurpWo&x+DpQc~9EQ6p5mQ8_WymjRP`M{uGGG#m$-lN_Wx zZw|QB1dO*zI7qM}RGiQS0*bV_@Y+>MPa4G20mz$nD|5gf{Dpsfr{M*}tz$6~({E(; z++Gq25X%Po;ctAPjq;Nba;Ke}8n=zG#$e_Z=3Cr!&E$LN)Z-74hQ~`?$7lbIAELyr zOPu-+?@rN%nR>pWddT(WMshKmahbcCd5M>U+q|T%!8Wos@1sLub1AMpGnibnE6XqC z=mQTbkKg-l3eI+Jp3QP@)w-}dlf~SzP z&tKX!9c}rZZi#?lDC}~a9=5M4S*iS?VIZ)Jf(LhUTujG6D+g2yPVI1$YPMaYkaG?Z zBzfrg8LwyNfY4r(=$zyatvjziOb8KOiSyw^=j^4$6o|Is9-eEqkx6j( zx^LDy>e0g%$%mu@QMkMSNO9`k;0i^UEpwDUk`PSX$N=^b55zrq(?(WLlbukL%k0Cd z@hniDAe#K-f+0jKn*c!U=~3b~1XL3_*f}1+k0*=Ab?(D8xzsZVQ$3dM}Om!a_53BMbfE2{-h@y^3ze zYn+%)dbdJR{T*Qh^_Uwg#AWy~u^gp-BpyG~JBiP05x>{) zTAz#<=K$^mStdNfLWOa?5XX(h|6sF7_VU8i96GtjKl$PnpuT~;c#V<{fMBoT6f8x) zg6#;i2IBtVqR_|BjuMaqy#l6Bc@n=HIca+U`HYDiCl0A+62eLmo51dyvQP&u923x6 z8_&F=rw-zmo^Wa|oFB;n9WbS~!06907RiYcV)Sok$d&qm;*%A8aA)?lnb2rY7TnD5 zH1n`JdVoA$Ukd&_nLS7rA8OIx_6G*=sHgGj$B!BRxHc$)YppzrDWJRE)K;Xx0|td8 z`$#OavRXwW@iVc~4mi9@?}RitP94X!V5w8N`oa_^^LsHXKrbgAm#iLOTvdd374;C% zIPo)h=m#qTgDOFh^8>7s+qweVDWFI2&R!g-#Y|PNdOmTG^y-}4AfNXtp&rnlPFOtD zPIF!s>(3i-Ujljn0 z*yHRyad?_?p)@(R;av3^0kCG9ZBzoFIzZRGegvUax~*|>-F!m995jZF!K7d6(XsdA z)h!^LGNB|%@-~!3x{heEJTWU>9kB)crUt@(gHp^WDb9 z>+A&iGEfT>Rn!{gA;-oG;n#xDHF6DBFQ$S_xc27Qm_s0L=9%DVG*(_o#1LWEQqS`T z&{YrA+fhX!(v0SUttJaXs9q;%P8;t2Dw%>9XJBJje4ns9AN^yz zHc;_LAf%}GhD{iHtL#L6Ztbv+hfsNQe4u&GAhe0?M$1Ci4xIH`t*tc96!`q5es*qu zt+BEZVHD$JB2|>Y>jk%|Vhc&?Zm1P7i2=YAef72qJ0rIYLY{aXA*VFGYd}*Pgi0*| zC_v0Sn-LpMzxG74HMJ#Sl*cZnI4L1B06;1i=PnjN_GmU5)$6F3onV>g*n(S#pF4CV zZ!QQFNjP0xR@PIO(Xt0&$C+fnbh*U3anmh>u!5GWn&-v!c_S*}^?-N_@{N7!xM&I) znq;@$W6ByDtUmY5Yp3~FZwG%3eY8gbB#7nlVB0k?rjxKEs>vgd1qw0|#(ILH^xwwz z3xtCeeQ`yogK4a3aRIQzEDIDO*r4$iGk-JX*emSO>TuGht-SW`h+rm3NywvBN%OnA znl(kk0{M_M6kUX8&-tW!?CKeyeC5x(v^0Cp^5s0X)Ak+nlZB0c+sQZkw>CH1wGpdg zjk_@gd5SW<&VJPT3JVj%gbu~P@RmLdG!%y zq)Yq5amSbDEw|d!Gd}xnlK~c;wHZ#|{u-!f#9rrTtluf#L39Fe3YD7W*9vm9Zw2&6 z`xHP&haQzT11MQNkd^~($C>n>6i+;kl@+oo z#o;$F=lCFnsoAc)UVi_o{0L9;qc(0|KkF|x=ShR5yCI#*u1kgl(UF6ccHY~xJ#I3w zLgp5N<{_Uyd@N`K|cn=gHzdzKiT96*epZsDx696&ZBK^o;Jj#5j& znPprh*vi$vb*EaNO!0}GCbN~Pcp9Ta7)|NQF;yj!jX*FimT@=ZU4-)|@ni+Tgi|?S zh!|twu&1-B!)VGIv*KD0c9Hh0B;J8){-%QW_>OL>erbu_HFU&C#iu7nTA^bxQfzF zx7}9|eT-hSd}QTvcW1SKjV$=XKN3FO{wBBNH$)GuHgnUxzt&0?s<2)aIp4-pnm%tA kwk&3V{k8M|dO-OxAm=Z-hLQzi&i`Jlsj90|qii4jKMckap8x;= literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/star_guide_book.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/star_guide_book.png new file mode 100644 index 0000000000000000000000000000000000000000..5995cd5d1a7a7c2102e34479564f7949eaae22df GIT binary patch literal 189 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EuJopAr*6y6C^SYbex&^_`+}h zo@MU#dI!I5T;T1-G4b&;>E3Z%%!LI2SXgQRd$Ysaf3vIAe)Anb%#NT zk;9Fry6zGnux-w-!wQQReExs3S-Z!Aw?Xq=%7(_xi3g7Nv??;Ln%UTRt+6A|`XpPx i8>S?Nqn?!y7#Y?i$y>dDzRw)!9tKZWKbLh*2~7Yy#7BAn literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/star_guide.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/star_guide.json new file mode 100644 index 0000000..91819a9 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/star_guide.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:star_guide" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/star_guide" +} \ No newline at end of file diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 97e1a84..c21e340 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -20,6 +20,7 @@ import za.co.neroland.nerospace.client.HydrationModuleScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; +import za.co.neroland.nerospace.client.StarGuideScreen; import za.co.neroland.nerospace.client.TerraformMonitorScreen; import za.co.neroland.nerospace.client.TerraformerScreen; import za.co.neroland.nerospace.registry.ModMenuTypes; @@ -41,6 +42,7 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.TERRAFORMER.get(), TerraformerScreen::new); MenuScreens.register(ModMenuTypes.HYDRATION_MODULE.get(), HydrationModuleScreen::new); MenuScreens.register(ModMenuTypes.TERRAFORM_MONITOR.get(), TerraformMonitorScreen::new); + MenuScreens.register(ModMenuTypes.STAR_GUIDE.get(), StarGuideScreen::new); ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { @Override diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 2450a73..5170835 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -26,6 +26,7 @@ import za.co.neroland.nerospace.client.HydrationModuleScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; +import za.co.neroland.nerospace.client.StarGuideScreen; import za.co.neroland.nerospace.client.TerraformMonitorScreen; import za.co.neroland.nerospace.client.TerraformerScreen; import za.co.neroland.nerospace.fluid.ModFluids; @@ -68,6 +69,7 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.TERRAFORMER.get(), TerraformerScreen::new); event.register(ModMenuTypes.HYDRATION_MODULE.get(), HydrationModuleScreen::new); event.register(ModMenuTypes.TERRAFORM_MONITOR.get(), TerraformMonitorScreen::new); + event.register(ModMenuTypes.STAR_GUIDE.get(), StarGuideScreen::new); } /** Rocket fuel renders as itself (amber still/flow) instead of the default missing art. */ From fe5d0f7c8ca455385f274ede0cceb062ef7f8b8d Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 17:53:23 +0200 Subject: [PATCH 64/82] Add Nerospace advancements and guide entries Add a large set of advancement JSONs for the Nerospace mod, including the root, rocket progression, station/dimension goals (Cindara, Glacira, Greenxertz) and many guide/item-based advancements (crafting, suits, machinery, modules, terraforming, etc.). These files provide in-game progression hooks and telemetry for items, armor sets, machines and breeding/terraform milestones under multiloader/common/src/main/resources/data/nerospace/advancement. --- docs/MULTILOADER_PORT_CHECKLIST.md | 24 +++++-- .../data/nerospace/advancement/cindara.json | 25 ++++++++ .../data/nerospace/advancement/glacira.json | 25 ++++++++ .../nerospace/advancement/greenxertz.json | 25 ++++++++ .../advancement/guide/alien_core.json | 28 +++++++++ .../advancement/guide/alien_fragment.json | 28 +++++++++ .../advancement/guide/alien_tech_scrap.json | 28 +++++++++ .../nerospace/advancement/guide/battery.json | 28 +++++++++ .../nerospace/advancement/guide/cindrite.json | 28 +++++++++ .../guide/combustion_generator.json | 28 +++++++++ .../advancement/guide/configurator.json | 28 +++++++++ .../advancement/guide/cryo_suit.json | 38 +++++++++++ .../advancement/guide/frame_casing.json | 28 +++++++++ .../nerospace/advancement/guide/gas_tank.json | 28 +++++++++ .../nerospace/advancement/guide/glacite.json | 28 +++++++++ .../advancement/guide/hydration_module.json | 28 +++++++++ .../advancement/guide/living_world.json | 12 ++++ .../advancement/guide/nerosium_dust.json | 28 +++++++++ .../advancement/guide/nerosium_pickaxe.json | 28 +++++++++ .../advancement/guide/nerosteel_ingot.json | 28 +++++++++ .../nerospace/advancement/guide/new_life.json | 63 +++++++++++++++++++ .../advancement/guide/oxygen_generator.json | 28 +++++++++ .../advancement/guide/oxygen_suit.json | 38 +++++++++++ .../advancement/guide/oxygen_suit_t2.json | 38 +++++++++++ .../advancement/guide/passive_generator.json | 28 +++++++++ .../advancement/guide/quarry_controller.json | 28 +++++++++ .../advancement/guide/quarry_landmark.json | 28 +++++++++ .../advancement/guide/raw_nerosium.json | 28 +++++++++ .../guide/rocket_fuel_canister.json | 28 +++++++++ .../advancement/guide/rocket_launch_pad.json | 28 +++++++++ .../advancement/guide/rocket_tier_2.json | 28 +++++++++ .../advancement/guide/rocket_tier_3.json | 28 +++++++++ .../advancement/guide/rocket_tier_4.json | 28 +++++++++ .../advancement/guide/station_charter.json | 12 ++++ .../advancement/guide/terraformed_ground.json | 12 ++++ .../advancement/guide/terraformer.json | 28 +++++++++ .../advancement/guide/thermal_suit.json | 38 +++++++++++ .../advancement/guide/universal_pipe.json | 28 +++++++++ .../advancement/guide/upgrade_module.json | 61 ++++++++++++++++++ .../advancement/nerosium_grinder.json | 28 +++++++++ .../data/nerospace/advancement/rocket.json | 28 +++++++++ .../data/nerospace/advancement/root.json | 30 +++++++++ .../data/nerospace/advancement/station.json | 25 ++++++++ 43 files changed, 1245 insertions(+), 5 deletions(-) create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/cindara.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/glacira.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/greenxertz.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_core.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_fragment.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_tech_scrap.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/battery.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/cindrite.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/combustion_generator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/configurator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/cryo_suit.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/frame_casing.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/gas_tank.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/glacite.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/hydration_module.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/living_world.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_dust.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_pickaxe.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosteel_ingot.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/new_life.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_generator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit_t2.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/passive_generator.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_controller.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_landmark.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/raw_nerosium.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_fuel_canister.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_launch_pad.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_2.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_3.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_4.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/station_charter.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformed_ground.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformer.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/thermal_suit.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/universal_pipe.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/guide/upgrade_module.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/nerosium_grinder.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/rocket.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/root.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/advancement/station.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index cdb5242..93dc1ea 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -4,6 +4,12 @@ Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still n cross-loader `multiloader/` project. As of this audit: **~209 classes ported, ~55 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — Star Guide slice 2a (advancement data — the guide now tracks progress).** All 4 +> cells green (full `:neoforge:build`+`:fabric:build` on 26.2; no Java changed — pure data). Copied all 42 +> nerospace advancements into common; 39 use vanilla triggers and track real completion, the 3 custom-trigger +> ones were converted to `minecraft:impossible` (load + parent chain intact, inert until granted), and 2 +> display icons were repointed off unported items. The Star Guide steps now light up as the player progresses. + > **2026-06-21 update — Star Guide slice 1 (the browsable progression guide).** All 4 cells green (full > `:neoforge:build`+`:fabric:build` on 26.2; compile on 26.1.2). Ported `progression/{StarGuide (9-chapter > ×40-step content table), StarGuideProgress (reads advancements), StarGuideBlock (lectern pedestal), @@ -357,11 +363,19 @@ checked by a headless build). StarGuideBlockEntity, StarGuideMenu}` + `item/StarGuideBookItem` + `client/StarGuideScreen`. Registered block/block-item/book/BE/menu + per-loader screen + assets + 98 lang keys. Opens from the book (in hand) or the pedestal (install the book). Reads advancement completion — **no `ModCriteria` dependency**. -- [ ] **Slice 2 (deferred).** Advancement DATA (so steps tick complete — currently the guide browses but - tracks no completion); the hologram BER (`StarGuideHologramRenderer`/`RenderState` — cosmetic; the BE - already computes + syncs the next-step stack); the "seen-pulse" (needs a `STAR_GUIDE_SEEN` player - attachment seam). Also the stand-in icons for station_charter / new_life resolve once STATION_CHARTER / - LOPER_HAUNCH are ported. +- [x] **Slice 2a — advancement DATA DONE (4 cells green).** Copied all 42 nerospace advancements; **39 use + pure vanilla triggers** (`inventory_changed` / `changed_dimension` / `bred_animals`) and track real + completion immediately. The **3 custom-trigger ones** (`terraformed_ground`/`living_world`/`station_charter`, + which need the deferred `ModCriteria` whose `PlayerTrigger` base moved packages 26.1↔26.2) were rewritten to + `minecraft:impossible` so they load and keep the parent chain intact (children `hydration_module`/`new_life` + are not orphaned) — they display but stay incomplete until granted. Repointed 2 display icons off unported + items (`station_charter`→`station_floor`, `new_life`→`meadow_loper_spawn_egg`). **The guide now tracks live + progress.** All JSON parse-validated; item predicates + the 4 `changed_dimension` targets all resolve. +- [ ] **Slice 2b (deferred).** The hologram BER (`StarGuideHologramRenderer`/`RenderState` — cosmetic; the BE + already computes + syncs the next-step stack); the "seen-pulse" (needs a `STAR_GUIDE_SEEN` player attachment + seam); converting the 3 `impossible` advancements back to real completion (needs `ModCriteria` via reflection + /version-split, or code-granting from the terraform/station-founding events). The station_charter / new_life + guide-step icons resolve once STATION_CHARTER / LOPER_HAUNCH are ported. ### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** - [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/cindara.json b/multiloader/common/src/main/resources/data/nerospace/advancement/cindara.json new file mode 100644 index 0000000..ef75d7e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/cindara.json @@ -0,0 +1,25 @@ +{ + "parent": "nerospace:guide/rocket_tier_3", + "criteria": { + "reached_cindara": { + "conditions": { + "to": "nerospace:cindara" + }, + "trigger": "minecraft:changed_dimension" + } + }, + "display": { + "description": "Travel to the volcanic moon Cindara", + "frame": "goal", + "icon": { + "id": "nerospace:cindrite" + }, + "title": "Into the Fire" + }, + "requirements": [ + [ + "reached_cindara" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/glacira.json b/multiloader/common/src/main/resources/data/nerospace/advancement/glacira.json new file mode 100644 index 0000000..5bd9470 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/glacira.json @@ -0,0 +1,25 @@ +{ + "parent": "nerospace:guide/rocket_tier_4", + "criteria": { + "reached_glacira": { + "conditions": { + "to": "nerospace:glacira" + }, + "trigger": "minecraft:changed_dimension" + } + }, + "display": { + "description": "Travel to the frozen moon Glacira", + "frame": "goal", + "icon": { + "id": "nerospace:glacite" + }, + "title": "Into the Cold" + }, + "requirements": [ + [ + "reached_glacira" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/greenxertz.json b/multiloader/common/src/main/resources/data/nerospace/advancement/greenxertz.json new file mode 100644 index 0000000..5b7f652 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/greenxertz.json @@ -0,0 +1,25 @@ +{ + "parent": "nerospace:guide/rocket_tier_2", + "criteria": { + "reached_greenxertz": { + "conditions": { + "to": "nerospace:greenxertz" + }, + "trigger": "minecraft:changed_dimension" + } + }, + "display": { + "description": "Travel to Greenxertz", + "frame": "goal", + "icon": { + "id": "nerospace:nerosteel_ingot" + }, + "title": "A Whole New World" + }, + "requirements": [ + [ + "reached_greenxertz" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_core.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_core.json new file mode 100644 index 0000000..6a8b172 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_core.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/alien_tech_scrap", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:alien_core" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Find the rare Alien Core inside a meteor", + "icon": { + "id": "nerospace:alien_core" + }, + "title": "Alien Core" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_fragment.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_fragment.json new file mode 100644 index 0000000..95eae67 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_fragment.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:root", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:alien_fragment" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Loot a fallen meteor for an Alien Fragment", + "icon": { + "id": "nerospace:alien_fragment" + }, + "title": "Visitor from Beyond" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_tech_scrap.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_tech_scrap.json new file mode 100644 index 0000000..e0e9715 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/alien_tech_scrap.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/alien_fragment", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:alien_tech_scrap" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Recover Alien Tech Scrap from a meteor", + "icon": { + "id": "nerospace:alien_tech_scrap" + }, + "title": "Salvaged Tech" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/battery.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/battery.json new file mode 100644 index 0000000..ccb6d6a --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/battery.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/universal_pipe", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:battery" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Battery", + "icon": { + "id": "nerospace:battery" + }, + "title": "Stored Potential" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/cindrite.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/cindrite.json new file mode 100644 index 0000000..52adf71 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/cindrite.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:cindara", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:cindrite" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Mine cindrite on Cindara", + "icon": { + "id": "nerospace:cindrite" + }, + "title": "Heart of the Volcano" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/combustion_generator.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/combustion_generator.json new file mode 100644 index 0000000..21c9b89 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/combustion_generator.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:nerosium_grinder", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:combustion_generator" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build a Combustion Generator", + "icon": { + "id": "nerospace:combustion_generator" + }, + "title": "Burning Bright" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/configurator.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/configurator.json new file mode 100644 index 0000000..08a913d --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/configurator.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/universal_pipe", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:configurator" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Configurator", + "icon": { + "id": "nerospace:configurator" + }, + "title": "Fine Tuning" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/cryo_suit.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/cryo_suit.json new file mode 100644 index 0000000..0aa415f --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/cryo_suit.json @@ -0,0 +1,38 @@ +{ + "parent": "nerospace:guide/oxygen_suit_t2", + "criteria": { + "has_cryo_suit": { + "conditions": { + "items": [ + { + "items": "nerospace:oxygen_suit_cold_helmet" + }, + { + "items": "nerospace:oxygen_suit_cold_chestplate" + }, + { + "items": "nerospace:oxygen_suit_cold_leggings" + }, + { + "items": "nerospace:oxygen_suit_cold_boots" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Assemble a full Cryo Suit — Glacira's cold stops draining your air", + "frame": "goal", + "icon": { + "id": "nerospace:oxygen_suit_cold_helmet" + }, + "title": "Dressed for the Deep Cold" + }, + "requirements": [ + [ + "has_cryo_suit" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/frame_casing.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/frame_casing.json new file mode 100644 index 0000000..c6b0d59 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/frame_casing.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:greenxertz", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:frame_casing" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft Frame Casing from nerosteel — the quarry builds its frame from these", + "icon": { + "id": "nerospace:frame_casing" + }, + "title": "Frameworks" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/gas_tank.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/gas_tank.json new file mode 100644 index 0000000..c83aeb2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/gas_tank.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/oxygen_generator", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:gas_tank" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Gas Tank for oxygen storage", + "icon": { + "id": "nerospace:gas_tank" + }, + "title": "Bottled Air" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/glacite.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/glacite.json new file mode 100644 index 0000000..8672b69 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/glacite.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:glacira", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:glacite" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Mine glacite on Glacira", + "icon": { + "id": "nerospace:glacite" + }, + "title": "Heart of the Glacier" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/hydration_module.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/hydration_module.json new file mode 100644 index 0000000..7aeceef --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/hydration_module.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/terraformed_ground", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:hydration_module" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build a Hydration Module and feed your Terraformer glacite", + "icon": { + "id": "nerospace:hydration_module" + }, + "title": "Meltwater" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/living_world.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/living_world.json new file mode 100644 index 0000000..b60ce11 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/living_world.json @@ -0,0 +1,12 @@ +{ + "parent": "nerospace:guide/hydration_module", + "criteria": { "impossible": { "trigger": "minecraft:impossible" } }, + "display": { + "description": "Stand on land your Terraformer brought fully to life", + "frame": "challenge", + "icon": { "id": "nerospace:meadow_loper_spawn_egg" }, + "title": "World Awake" + }, + "requirements": [ [ "impossible" ] ], + "sends_telemetry_event": true +} diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_dust.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_dust.json new file mode 100644 index 0000000..7bad4c6 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_dust.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:nerosium_grinder", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:nerosium_dust" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Grind nerosium into dust", + "icon": { + "id": "nerospace:nerosium_dust" + }, + "title": "Finely Ground" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_pickaxe.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_pickaxe.json new file mode 100644 index 0000000..d56b434 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosium_pickaxe.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:root", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:nerosium_pickaxe" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a nerosium pickaxe", + "icon": { + "id": "nerospace:nerosium_pickaxe" + }, + "title": "Tools of the Trade" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosteel_ingot.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosteel_ingot.json new file mode 100644 index 0000000..6c7ab7e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/nerosteel_ingot.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:greenxertz", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:nerosteel_ingot" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Smelt a nerosteel ingot from Greenxertz ore", + "icon": { + "id": "nerospace:nerosteel_ingot" + }, + "title": "Alien Alloy" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/new_life.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/new_life.json new file mode 100644 index 0000000..605cd18 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/new_life.json @@ -0,0 +1,63 @@ +{ + "parent": "nerospace:guide/living_world", + "criteria": { + "bred_ember_strutter": { + "conditions": { + "child": [ + { + "condition": "minecraft:entity_properties", + "entity": "this", + "predicate": { + "type": "nerospace:ember_strutter" + } + } + ] + }, + "trigger": "minecraft:bred_animals" + }, + "bred_meadow_loper": { + "conditions": { + "child": [ + { + "condition": "minecraft:entity_properties", + "entity": "this", + "predicate": { + "type": "nerospace:meadow_loper" + } + } + ] + }, + "trigger": "minecraft:bred_animals" + }, + "bred_woolly_drift": { + "conditions": { + "child": [ + { + "condition": "minecraft:entity_properties", + "entity": "this", + "predicate": { + "type": "nerospace:woolly_drift" + } + } + ] + }, + "trigger": "minecraft:bred_animals" + } + }, + "display": { + "description": "Breed a creature born of a terraformed world", + "frame": "goal", + "icon": { + "id": "nerospace:meadow_loper_spawn_egg" + }, + "title": "New Life" + }, + "requirements": [ + [ + "bred_meadow_loper", + "bred_ember_strutter", + "bred_woolly_drift" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_generator.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_generator.json new file mode 100644 index 0000000..2d936e1 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_generator.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:station", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:oxygen_generator" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build an Oxygen Generator", + "icon": { + "id": "nerospace:oxygen_generator" + }, + "title": "Something to Breathe" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit.json new file mode 100644 index 0000000..8082d00 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit.json @@ -0,0 +1,38 @@ +{ + "parent": "nerospace:guide/oxygen_generator", + "criteria": { + "has_suit": { + "conditions": { + "items": [ + { + "items": "nerospace:oxygen_suit_helmet" + }, + { + "items": "nerospace:oxygen_suit_chestplate" + }, + { + "items": "nerospace:oxygen_suit_leggings" + }, + { + "items": "nerospace:oxygen_suit_boots" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Assemble a full Oxygen Suit", + "frame": "goal", + "icon": { + "id": "nerospace:oxygen_suit_helmet" + }, + "title": "Suit Up" + }, + "requirements": [ + [ + "has_suit" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit_t2.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit_t2.json new file mode 100644 index 0000000..8cdc22b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/oxygen_suit_t2.json @@ -0,0 +1,38 @@ +{ + "parent": "nerospace:guide/oxygen_suit", + "criteria": { + "has_suit_t2": { + "conditions": { + "items": [ + { + "items": "nerospace:oxygen_suit_t2_helmet" + }, + { + "items": "nerospace:oxygen_suit_t2_chestplate" + }, + { + "items": "nerospace:oxygen_suit_t2_leggings" + }, + { + "items": "nerospace:oxygen_suit_t2_boots" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Assemble a full Tier 2 (cindrite) Oxygen Suit", + "frame": "goal", + "icon": { + "id": "nerospace:oxygen_suit_t2_helmet" + }, + "title": "Ember-Proof" + }, + "requirements": [ + [ + "has_suit_t2" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/passive_generator.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/passive_generator.json new file mode 100644 index 0000000..eb07ba9 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/passive_generator.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/universal_pipe", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:passive_generator" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build a Passive Generator", + "icon": { + "id": "nerospace:passive_generator" + }, + "title": "Slow and Steady" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_controller.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_controller.json new file mode 100644 index 0000000..de09a7f --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_controller.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/quarry_landmark", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:quarry_controller" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build a Quarry Controller and automate your digging", + "icon": { + "id": "nerospace:quarry_controller" + }, + "title": "Strip Miner" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_landmark.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_landmark.json new file mode 100644 index 0000000..e7aaed4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/quarry_landmark.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/frame_casing", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:quarry_landmark" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft Quarry Landmarks to mark out a rectangular mining region", + "icon": { + "id": "nerospace:quarry_landmark" + }, + "title": "Stake a Claim" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/raw_nerosium.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/raw_nerosium.json new file mode 100644 index 0000000..6cccad8 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/raw_nerosium.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:root", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:raw_nerosium" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Mine raw nerosium from nerosium ore", + "icon": { + "id": "nerospace:raw_nerosium" + }, + "title": "Strange Red Rock" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_fuel_canister.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_fuel_canister.json new file mode 100644 index 0000000..bda4517 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_fuel_canister.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/combustion_generator", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:rocket_fuel_canister" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Fill a Rocket Fuel Canister", + "icon": { + "id": "nerospace:rocket_fuel_canister" + }, + "title": "Highly Flammable" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_launch_pad.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_launch_pad.json new file mode 100644 index 0000000..05a7165 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_launch_pad.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/rocket_fuel_canister", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:rocket_launch_pad" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Rocket Launch Pad (you'll want a 3x3)", + "icon": { + "id": "nerospace:rocket_launch_pad" + }, + "title": "Ground Control" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_2.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_2.json new file mode 100644 index 0000000..4fa9556 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_2.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:station", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:rocket_tier_2" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Tier 2 Rocket", + "icon": { + "id": "nerospace:rocket_tier_2" + }, + "title": "Bigger Boosters" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_3.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_3.json new file mode 100644 index 0000000..063c4b5 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_3.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:greenxertz", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:rocket_tier_3" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Tier 3 Rocket (its pad needs a Station Wall ring)", + "icon": { + "id": "nerospace:rocket_tier_3" + }, + "title": "To the Fire Moon" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_4.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_4.json new file mode 100644 index 0000000..c7588be --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/rocket_tier_4.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:cindara", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:rocket_tier_4" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Tier 4 Rocket (it launches only from a Heavy Launch Complex)", + "icon": { + "id": "nerospace:rocket_tier_4" + }, + "title": "To the Ice Moon" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/station_charter.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/station_charter.json new file mode 100644 index 0000000..48f10ac --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/station_charter.json @@ -0,0 +1,12 @@ +{ + "parent": "nerospace:station", + "criteria": { "impossible": { "trigger": "minecraft:impossible" } }, + "display": { + "description": "Found your own station with a Station Charter", + "frame": "goal", + "icon": { "id": "nerospace:station_floor" }, + "title": "Homestead in Orbit" + }, + "requirements": [ [ "impossible" ] ], + "sends_telemetry_event": true +} diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformed_ground.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformed_ground.json new file mode 100644 index 0000000..c3de716 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformed_ground.json @@ -0,0 +1,12 @@ +{ + "parent": "nerospace:guide/terraformer", + "criteria": { "impossible": { "trigger": "minecraft:impossible" } }, + "display": { + "description": "Stand on ground your Terraformer made breathable", + "frame": "challenge", + "icon": { "id": "nerospace:terraformer" }, + "title": "Green Again" + }, + "requirements": [ [ "impossible" ] ], + "sends_telemetry_event": true +} diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformer.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformer.json new file mode 100644 index 0000000..419de98 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/terraformer.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:cindara", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:terraformer" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build a Terraformer", + "icon": { + "id": "nerospace:terraformer" + }, + "title": "World Engine" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/thermal_suit.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/thermal_suit.json new file mode 100644 index 0000000..7947736 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/thermal_suit.json @@ -0,0 +1,38 @@ +{ + "parent": "nerospace:guide/oxygen_suit_t2", + "criteria": { + "has_thermal_suit": { + "conditions": { + "items": [ + { + "items": "nerospace:oxygen_suit_heat_helmet" + }, + { + "items": "nerospace:oxygen_suit_heat_chestplate" + }, + { + "items": "nerospace:oxygen_suit_heat_leggings" + }, + { + "items": "nerospace:oxygen_suit_heat_boots" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Assemble a full Thermal Suit — Cindara's heat stops draining your air", + "frame": "goal", + "icon": { + "id": "nerospace:oxygen_suit_heat_helmet" + }, + "title": "Forged for the Fire" + }, + "requirements": [ + [ + "has_thermal_suit" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/universal_pipe.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/universal_pipe.json new file mode 100644 index 0000000..2541b10 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/universal_pipe.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/combustion_generator", + "criteria": { + "has_item": { + "conditions": { + "items": [ + { + "items": "nerospace:universal_pipe" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Universal Pipe", + "icon": { + "id": "nerospace:universal_pipe" + }, + "title": "Connect Everything" + }, + "requirements": [ + [ + "has_item" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/guide/upgrade_module.json b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/upgrade_module.json new file mode 100644 index 0000000..0c94c77 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/guide/upgrade_module.json @@ -0,0 +1,61 @@ +{ + "parent": "nerospace:guide/quarry_controller", + "criteria": { + "efficiency": { + "conditions": { + "items": [ + { + "items": "nerospace:efficiency_module" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "fortune": { + "conditions": { + "items": [ + { + "items": "nerospace:fortune_module" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "silk_touch": { + "conditions": { + "items": [ + { + "items": "nerospace:silk_touch_module" + } + ] + }, + "trigger": "minecraft:inventory_changed" + }, + "speed": { + "conditions": { + "items": [ + { + "items": "nerospace:speed_module" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft an upgrade module — speed, efficiency, fortune or silk touch", + "icon": { + "id": "nerospace:speed_module" + }, + "title": "Tune It Up" + }, + "requirements": [ + [ + "speed", + "efficiency", + "fortune", + "silk_touch" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/nerosium_grinder.json b/multiloader/common/src/main/resources/data/nerospace/advancement/nerosium_grinder.json new file mode 100644 index 0000000..4b9bc64 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/nerosium_grinder.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:root", + "criteria": { + "has_grinder": { + "conditions": { + "items": [ + { + "items": "nerospace:nerosium_grinder" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Build a Nerosium Grinder", + "icon": { + "id": "nerospace:nerosium_grinder" + }, + "title": "Industrial Revolution" + }, + "requirements": [ + [ + "has_grinder" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/rocket.json b/multiloader/common/src/main/resources/data/nerospace/advancement/rocket.json new file mode 100644 index 0000000..760bd52 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/rocket.json @@ -0,0 +1,28 @@ +{ + "parent": "nerospace:guide/rocket_launch_pad", + "criteria": { + "has_rocket": { + "conditions": { + "items": [ + { + "items": "nerospace:rocket_tier_1" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "description": "Craft a Tier 1 Rocket", + "icon": { + "id": "nerospace:rocket_tier_1" + }, + "title": "We Have Liftoff" + }, + "requirements": [ + [ + "has_rocket" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/root.json b/multiloader/common/src/main/resources/data/nerospace/advancement/root.json new file mode 100644 index 0000000..62fb0e5 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/root.json @@ -0,0 +1,30 @@ +{ + "criteria": { + "has_nerosium": { + "conditions": { + "items": [ + { + "items": "nerospace:nerosium_ingot" + } + ] + }, + "trigger": "minecraft:inventory_changed" + } + }, + "display": { + "announce_to_chat": false, + "background": "minecraft:textures/gui/advancements/backgrounds/stone.png", + "description": "Mine nerosium and reach for the stars", + "icon": { + "id": "nerospace:nerosium_ingot" + }, + "show_toast": false, + "title": "Nerospace" + }, + "requirements": [ + [ + "has_nerosium" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/advancement/station.json b/multiloader/common/src/main/resources/data/nerospace/advancement/station.json new file mode 100644 index 0000000..3ab6f35 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/advancement/station.json @@ -0,0 +1,25 @@ +{ + "parent": "nerospace:rocket", + "criteria": { + "reached_station": { + "conditions": { + "to": "nerospace:station" + }, + "trigger": "minecraft:changed_dimension" + } + }, + "display": { + "description": "Dock at the Orbital Station", + "frame": "goal", + "icon": { + "id": "nerospace:station_floor" + }, + "title": "Orbital" + }, + "requirements": [ + [ + "reached_station" + ] + ], + "sends_telemetry_event": true +} \ No newline at end of file From eba71c7c4ed6e1dfe1f1580b302b17ed00f237cd Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 18:08:08 +0200 Subject: [PATCH 65/82] Add cross-loader BER seam and Star Guide hologram Introduce a reusable cross-loader block-entity-renderer seam and the Star Guide pedestal hologram BER. Adds ClientBlockEntityRenderers with a Sink interface, StarGuideHologramRenderer and StarGuideHologramRenderState in common, and wires registration into the Fabric and NeoForge client setups. Update docs/MULTILOADER_PORT_CHECKLIST.md to mark Slice 2b done and note the BlockEntityRendererProvider two-type-parameter gotcha. This unblocks future BER ports (solar tracker, quarry drill, etc.) by centralizing BER registration logic. --- docs/MULTILOADER_PORT_CHECKLIST.md | 21 +++++- .../client/ClientBlockEntityRenderers.java | 31 ++++++++ .../client/StarGuideHologramRenderState.java | 17 +++++ .../client/StarGuideHologramRenderer.java | 75 +++++++++++++++++++ .../fabric/NerospaceFabricClient.java | 13 ++++ .../neoforge/NeoForgeClientSetup.java | 12 +++ 6 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderState.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderer.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 93dc1ea..43338a4 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,15 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~209 classes ported, ~55 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~212 classes ported, ~52 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — Star Guide slice 2b (hologram BER + reusable BER seam).** All 4 cells compile green +> (26.1.2 + 26.2). Added the first cross-loader block-entity-renderer seam (`ClientBlockEntityRenderers.Sink`) +> + the Star Guide pedestal hologram renderer (spinning next-step icon). **26.x gotcha: `BlockEntityRendererProvider` +> is 2-type-param ``.** The seam is reusable for future BERs (solar +> sun-tracking deck, quarry drill head, etc.). + > **2026-06-21 update — Star Guide slice 2a (advancement data — the guide now tracks progress).** All 4 > cells green (full `:neoforge:build`+`:fabric:build` on 26.2; no Java changed — pure data). Copied all 42 > nerospace advancements into common; 39 use vanilla triggers and track real completion, the 3 custom-trigger @@ -371,9 +377,16 @@ checked by a headless build). are not orphaned) — they display but stay incomplete until granted. Repointed 2 display icons off unported items (`station_charter`→`station_floor`, `new_life`→`meadow_loper_spawn_egg`). **The guide now tracks live progress.** All JSON parse-validated; item predicates + the 4 `changed_dimension` targets all resolve. -- [ ] **Slice 2b (deferred).** The hologram BER (`StarGuideHologramRenderer`/`RenderState` — cosmetic; the BE - already computes + syncs the next-step stack); the "seen-pulse" (needs a `STAR_GUIDE_SEEN` player attachment - seam); converting the 3 `impossible` advancements back to real completion (needs `ModCriteria` via reflection +- [x] **Slice 2b — hologram BER DONE (4 cells green).** Added a reusable cross-loader BER seam + `client/ClientBlockEntityRenderers` (`Sink` mirrors `ClientEntityRenderers` — NeoForge + `RegisterRenderers.registerBlockEntityRenderer`, Fabric `BlockEntityRendererRegistry.register`) + + `client/{StarGuideHologramRenderer, StarGuideHologramRenderState}` (verbatim 26.x BER submission). The + pedestal now floats the spinning next-step hologram. **26.x gotcha: `BlockEntityRendererProvider` takes 2 + type params ``** (probed via build error) — the Sink carries both. The + seam now unblocks future BERs (solar sun-tracking, quarry drill, etc.). (Fabric `BlockEntityRendererRegistry` + is soft-deprecated — works; a later switch to vanilla `BlockEntityRenderers.register` is optional.) +- [ ] **Slice 2c (deferred, minor).** The "seen-pulse" (needs a `STAR_GUIDE_SEEN` player attachment seam); + converting the 3 `impossible` advancements back to real completion (needs `ModCriteria` via reflection /version-split, or code-granting from the terraform/station-founding events). The station_charter / new_life guide-step icons resolve once STATION_CHARTER / LOPER_HAUNCH are ported. diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java new file mode 100644 index 0000000..faaec0b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java @@ -0,0 +1,31 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; + +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * Cross-loader block-entity-renderer wiring (the BER analogue of {@link ClientEntityRenderers}). The + * renderer set is identical on both loaders, so it lives here once and each loader passes its own + * registration function ({@link Sink}) — NeoForge's {@code RegisterRenderers} event + * ({@code registerBlockEntityRenderer}), Fabric's {@code BlockEntityRenderers.register}. + */ +public final class ClientBlockEntityRenderers { + + /** A loader's BER-registration entry point. */ + public interface Sink { + void register( + BlockEntityType type, BlockEntityRendererProvider provider); + } + + private ClientBlockEntityRenderers() { + } + + public static void registerAll(Sink sink) { + // Star Guide pedestal: the floating, spinning next-step hologram above a loaded pedestal. + sink.register(ModBlockEntities.STAR_GUIDE.get(), context -> new StarGuideHologramRenderer()); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderState.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderState.java new file mode 100644 index 0000000..c8318fb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderState.java @@ -0,0 +1,17 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; +import net.minecraft.client.renderer.item.ItemStackRenderState; + +/** Render state for the Star Guide pedestal hologram: the floating next-step icon + animation. */ +public class StarGuideHologramRenderState extends BlockEntityRenderState { + + /** Whether the pedestal is loaded (hologram visible). */ + public boolean visible; + /** Y-spin in degrees. */ + public float spin; + /** Vertical bob offset (blocks). */ + public float bob; + /** The hologram icon's pooled item render state. */ + public final ItemStackRenderState renderState = new ItemStackRenderState(); +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderer.java new file mode 100644 index 0000000..b956ec9 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideHologramRenderer.java @@ -0,0 +1,75 @@ +package za.co.neroland.nerospace.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.math.Axis; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.util.Mth; +import net.minecraft.world.item.ItemDisplayContext; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.phys.Vec3; + +import za.co.neroland.nerospace.progression.StarGuideBlockEntity; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The Star Guide pedestal hologram: a slowly spinning, bobbing icon floating above a LOADED pedestal + * showing the nearest player's next incomplete progression step (server-computed, BE-synced) — or the + * Star Guide Book itself once everything is complete. + * + *

Cross-loader port: vanilla BER submission API; identical to the standalone mod. Registered via the + * {@link ClientBlockEntityRenderers} seam.

+ */ +public class StarGuideHologramRenderer + implements BlockEntityRenderer { + + /** Packed full-bright light coords (same constant the RocketRenderer uses for its glow). */ + private static final int FULL_BRIGHT = 0x00F000F0; + + @Override + public StarGuideHologramRenderState createRenderState() { + return new StarGuideHologramRenderState(); + } + + @Override + public void extractRenderState(StarGuideBlockEntity guide, StarGuideHologramRenderState state, + float partialTick, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(guide, state, partialTick, cameraPos, breakProgress); + state.visible = guide.hasBook() && guide.getLevel() != null; + if (!state.visible) { + return; + } + float now = guide.getLevel().getGameTime() + partialTick; + state.spin = (now * 1.5F) % 360.0F; + state.bob = Mth.sin(now * 0.06F) * 0.05F; + + ItemStack icon = guide.getHologram(); + if (icon.isEmpty()) { + icon = new ItemStack(ModItems.STAR_GUIDE_BOOK.get()); // all complete (or no player near) + } + Minecraft.getInstance().getItemModelResolver().updateForTopItem( + state.renderState, icon, ItemDisplayContext.GROUND, guide.getLevel(), null, + (int) guide.getBlockPos().asLong()); + } + + @Override + public void submit(StarGuideHologramRenderState state, PoseStack poseStack, + SubmitNodeCollector collector, CameraRenderState cameraState) { + if (!state.visible) { + return; + } + poseStack.pushPose(); + poseStack.translate(0.5F, 1.35F + state.bob, 0.5F); + poseStack.mulPose(Axis.YP.rotationDegrees(state.spin)); + poseStack.scale(0.75F, 0.75F, 0.75F); + // Emissive: a hologram is its own light source, so render full-bright instead of with the + // pedestal's world light (it read pitch-black at night). + state.renderState.submit(poseStack, collector, FULL_BRIGHT, OverlayTexture.NO_OVERLAY, 0); + poseStack.popPose(); + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index c21e340..5fc8a44 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -2,13 +2,19 @@ import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.rendering.v1.BlockEntityRendererRegistry; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.minecraft.client.gui.screens.MenuScreens; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.client.ClientBlockEntityRenderers; import za.co.neroland.nerospace.client.ClientEntityRenderers; import za.co.neroland.nerospace.client.ClientOxygenVisuals; import za.co.neroland.nerospace.client.MeteorTrackerHud; @@ -50,6 +56,13 @@ public void register(EntityType type, EntityRend EntityRendererRegistry.register(type, provider); } }); + ClientBlockEntityRenderers.registerAll(new ClientBlockEntityRenderers.Sink() { + @Override + public void register( + BlockEntityType type, BlockEntityRendererProvider provider) { + BlockEntityRendererRegistry.register(type, provider); + } + }); // Meteor Tracker readout + oxygen-field visuals — counterpart to NeoForge's ClientTickEvent.Post. ClientTickEvents.END_CLIENT_TICK.register(mc -> { diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index 5170835..cbac6eb 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -1,11 +1,15 @@ package za.co.neroland.nerospace.neoforge; import net.minecraft.client.renderer.block.FluidModel; +import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; import net.minecraft.client.renderer.entity.EntityRendererProvider; import net.minecraft.client.resources.model.sprite.Material; import net.minecraft.resources.Identifier; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; import net.neoforged.bus.api.IEventBus; import net.neoforged.neoforge.client.event.ClientTickEvent; import net.neoforged.neoforge.client.event.EntityRenderersEvent; @@ -15,6 +19,7 @@ import net.neoforged.neoforge.common.NeoForge; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.client.ClientBlockEntityRenderers; import za.co.neroland.nerospace.client.ClientEntityRenderers; import za.co.neroland.nerospace.client.ClientOxygenVisuals; import za.co.neroland.nerospace.client.MeteorTrackerHud; @@ -56,6 +61,13 @@ public void register(EntityType type, EntityRend event.registerEntityRenderer(type, provider); } }); + ClientBlockEntityRenderers.registerAll(new ClientBlockEntityRenderers.Sink() { + @Override + public void register( + BlockEntityType type, BlockEntityRendererProvider provider) { + event.registerBlockEntityRenderer(type, provider); + } + }); } private static void onRegisterScreens(RegisterMenuScreensEvent event) { From 16782e31df3205350dbf6ae5423f2aece18cb9f5 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 21:55:03 +0200 Subject: [PATCH 66/82] Add Star Guide seen-mask attachment & UI pulse Introduce per-player STAR_GUIDE_SEEN attachment and GUI support so completed-but-unseen steps pulse until acknowledged. - Docs: mark Slice 2c done and note NeoForge AttachmentType.builder() default must be a lambda (ambiguous method-ref). - API: add getStarGuideSeen/setStarGuideSeen to IPlatformHelper. - Menu: StarGuideMenu now doubles DATA_COUNT to include seen masks, reads/writes seen masks via Services.PLATFORM, exposes seenMask/isStepSeen, and implements clickMenuButton to mark a step seen server-side. - Screen: StarGuideScreen pulses completed-but-unseen steps and sends a menu button click when a step is selected so the server can clear the pulse. - Fabric: register STAR_GUIDE_SEEN attachment (Codec.INT.listOf(), initializer List::of, copyOnDeath) and implement platform accessors in FabricPlatformHelper. - NeoForge: register STAR_GUIDE_SEEN attachment using a lambda default (() -> List.of()) to avoid overload ambiguity, and implement accessors in NeoForgePlatformHelper. Serialization uses Codec.INT.listOf() and attachments copy on death; UI and networking use existing container/button sync to persist acknowledgements. --- docs/MULTILOADER_PORT_CHECKLIST.md | 21 +++++-- .../nerospace/client/StarGuideScreen.java | 12 +++- .../nerospace/platform/IPlatformHelper.java | 10 ++++ .../nerospace/progression/StarGuideMenu.java | 60 +++++++++++++++---- .../nerospace/fabric/FabricAttachments.java | 9 +++ .../platform/FabricPlatformHelper.java | 10 ++++ .../neoforge/NeoForgeAttachments.java | 9 +++ .../platform/NeoForgePlatformHelper.java | 10 ++++ 8 files changed, 123 insertions(+), 18 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 43338a4..129884a 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -4,6 +4,13 @@ Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still n cross-loader `multiloader/` project. As of this audit: **~212 classes ported, ~52 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — Star Guide slice 2c (seen-pulse; the guide is now feature-complete).** All 4 cells +> compile green. Added a `List` `STAR_GUIDE_SEEN` player attachment via the existing seam + +> restored the menu seen-masks + screen pulse (completed-but-unseen steps pulse until clicked). 26.x gotcha: +> NeoForge `AttachmentType.builder` default must be a lambda (`List::of` is ambiguous). Star Guide = browse + +> live progress + hologram + seen-pulse; only converting the 3 `impossible` advancements remains (blocked on +> ModCriteria). + > **2026-06-21 update — Star Guide slice 2b (hologram BER + reusable BER seam).** All 4 cells compile green > (26.1.2 + 26.2). Added the first cross-loader block-entity-renderer seam (`ClientBlockEntityRenderers.Sink`) > + the Star Guide pedestal hologram renderer (spinning next-step icon). **26.x gotcha: `BlockEntityRendererProvider` @@ -385,10 +392,16 @@ checked by a headless build). type params ``** (probed via build error) — the Sink carries both. The seam now unblocks future BERs (solar sun-tracking, quarry drill, etc.). (Fabric `BlockEntityRendererRegistry` is soft-deprecated — works; a later switch to vanilla `BlockEntityRenderers.register` is optional.) -- [ ] **Slice 2c (deferred, minor).** The "seen-pulse" (needs a `STAR_GUIDE_SEEN` player attachment seam); - converting the 3 `impossible` advancements back to real completion (needs `ModCriteria` via reflection - /version-split, or code-granting from the terraform/station-founding events). The station_charter / new_life - guide-step icons resolve once STATION_CHARTER / LOPER_HAUNCH are ported. +- [x] **Slice 2c — seen-pulse DONE (4 cells green).** Added a `List` `STAR_GUIDE_SEEN` player + attachment through the existing data-attachment seam (`IPlatformHelper.get/setStarGuideSeen` + + `NeoForgeAttachments`/`FabricAttachments`, `Codec.INT.listOf()`, copy-on-death). Restored the menu's seen + masks (`DATA_COUNT = CHAPTER_COUNT*2`, `clickMenuButton` marks seen via `Services.PLATFORM`) + the screen's + completed-but-unseen pulse (clicking a step acknowledges it). **The Star Guide is now feature-complete** + (browse + live progress + hologram + seen-pulse). 26.x gotcha: NeoForge `AttachmentType.builder(...)` is + overloaded, so the default must be a lambda `() -> List.of()` (not the `List::of` method ref — ambiguous). +- [ ] **Slice 2d (deferred, blocked).** Converting the 3 `impossible` advancements back to real completion + (needs `ModCriteria` via reflection/version-split, or code-granting from terraform/station events); the + station_charter / new_life guide-step icons resolve once STATION_CHARTER / LOPER_HAUNCH are ported. ### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** - [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java index c0eb65a..3b49f32 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java @@ -101,6 +101,11 @@ private int rowSpacing() { private void selectStep(int step) { this.selectedStep = step; + // Report "seen" so the completed-pulse stops (server writes the STAR_GUIDE_SEEN attachment). + if (this.minecraft != null && this.minecraft.gameMode != null) { + this.minecraft.gameMode.handleInventoryButtonClick( + this.menu.containerId, this.selectedChapter * 16 + step); + } } @Override @@ -128,11 +133,14 @@ protected void extractForeground(GuiGraphicsExtractor g) { } } - // Step nodes: completed steps lit, with a completion pip. + // Step nodes: completed steps lit (steady once seen, pulsing until clicked once). + long now = System.currentTimeMillis(); + boolean pulseOn = (now / 400L) % 2L == 0L; for (int i = 0; i < this.stepButtons.size(); i++) { boolean done = this.menu.isStepComplete(this.selectedChapter, i); + boolean seen = this.menu.isStepSeen(this.selectedChapter, i); SpaceButton node = this.stepButtons.get(i); - node.setSelected(done); + node.setSelected(done && (seen || pulseOn)); if (done) { g.fill(node.getX() + node.getWidth() - 5, node.getY() + 2, // completion pip node.getX() + node.getWidth() - 2, node.getY() + 5, DONE); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java index dd6ce1f..645ae12 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java @@ -58,4 +58,14 @@ public interface IPlatformHelper { /** Records the chunk's terraform stage. */ void setTerraformStage(net.minecraft.world.level.chunk.LevelChunk chunk, int value); + + // --- Per-player Star Guide "seen" masks (data-attachment seam) ----------- + // One bitmask per chapter (bit i = step i acknowledged). Persists across logout, copies on death; + // defaults to an empty list. Backs the Star Guide GUI's completed-but-unseen step pulse. + + /** The player's per-chapter Star Guide "seen" bitmasks (empty list if unset). */ + java.util.List getStarGuideSeen(net.minecraft.world.entity.player.Player player); + + /** Stores the player's per-chapter Star Guide "seen" bitmasks. */ + void setStarGuideSeen(net.minecraft.world.entity.player.Player player, java.util.List value); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java index e804538..57be91c 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java @@ -1,5 +1,8 @@ package za.co.neroland.nerospace.progression; +import java.util.ArrayList; +import java.util.List; + import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; @@ -8,20 +11,24 @@ import net.minecraft.world.inventory.SimpleContainerData; import net.minecraft.world.item.ItemStack; +import za.co.neroland.nerospace.platform.Services; import za.co.neroland.nerospace.registry.ModMenuTypes; /** - * Star Guide menu: no slots — just synced per-chapter completion bitmasks read live from the player's - * advancements (bit i = step i of that chapter). Data slots sync as shorts, so masks are safe while - * chapters stay ≤ 16 steps (enforced by {@link StarGuide}'s table shape). + * Star Guide menu: no slots — just synced per-chapter data. Slots {@code [0..N)} = completion masks + * read live from the player's advancements (bit i = step i of that chapter); slots {@code [N..2N)} = + * "seen" masks from the {@code STAR_GUIDE_SEEN} player attachment (a completed-but-unseen step pulses + * in the GUI until clicked). Clicking a step sends a menu button ({@code chapter * 16 + step}) that + * marks it seen server-side. Data slots sync as shorts, so masks are safe while chapters stay ≤ 16 + * steps (enforced by {@link StarGuide}'s table shape). * - *

Cross-loader port note (slice 1): the standalone mod also tracks a "seen" mask via a - * {@code STAR_GUIDE_SEEN} player attachment (completed-but-unseen steps pulse). That attachment is a - * separate cross-loader seam and is deferred — the multiloader menu syncs completion only.

+ *

Cross-loader port: the "seen" attachment is reached through the {@link Services#PLATFORM} seam + * (NeoForge {@code player.getData}/Fabric {@code getAttachedOrCreate}) instead of the root's direct + * {@code ModAttachments} access.

*/ public class StarGuideMenu extends AbstractContainerMenu { - public static final int DATA_COUNT = StarGuide.CHAPTER_COUNT; + public static final int DATA_COUNT = StarGuide.CHAPTER_COUNT * 2; private final ContainerData data; @@ -30,7 +37,7 @@ public StarGuideMenu(int containerId, Inventory playerInventory) { this(containerId, playerInventory, playerInventory.player); } - /** Server constructor: data reads live from the player's advancements. */ + /** Server constructor: data reads live from the player's advancements + seen attachment. */ @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring public StarGuideMenu(int containerId, Inventory playerInventory, Player player) { super(ModMenuTypes.STAR_GUIDE.get(), containerId); @@ -51,17 +58,43 @@ public ItemStack quickMoveStack(Player player, int index) { return ItemStack.EMPTY; // no slots } + /** Step click → mark seen (button id = chapter * 16 + step). */ + @Override + public boolean clickMenuButton(Player player, int id) { + int chapter = id / 16; + int step = id % 16; + if (chapter < 0 || chapter >= StarGuide.CHAPTER_COUNT + || step >= StarGuide.CHAPTERS.get(chapter).steps().size()) { + return false; + } + List seen = new ArrayList<>(Services.PLATFORM.getStarGuideSeen(player)); + while (seen.size() < StarGuide.CHAPTER_COUNT) { + seen.add(0); + } + seen.set(chapter, seen.get(chapter) | (1 << step)); + Services.PLATFORM.setStarGuideSeen(player, List.copyOf(seen)); + return true; + } + // --- Screen helpers ------------------------------------------------------------------------ public int completionMask(int chapter) { return this.data.get(chapter); } + public int seenMask(int chapter) { + return this.data.get(StarGuide.CHAPTER_COUNT + chapter); + } + public boolean isStepComplete(int chapter, int step) { return (completionMask(chapter) & (1 << step)) != 0; } - /** Live server-side view: per-chapter completion masks from the player's advancements. */ + public boolean isStepSeen(int chapter, int step) { + return (seenMask(chapter) & (1 << step)) != 0; + } + + /** Live server-side view: advancements (completion) + the seen attachment. */ private static final class ProgressData implements ContainerData { private final ServerPlayer player; @@ -72,9 +105,12 @@ private static final class ProgressData implements ContainerData { @Override public int get(int index) { - return index >= 0 && index < StarGuide.CHAPTER_COUNT - ? StarGuideProgress.chapterMask(this.player, index) - : 0; + if (index < StarGuide.CHAPTER_COUNT) { + return StarGuideProgress.chapterMask(this.player, index); + } + List seen = Services.PLATFORM.getStarGuideSeen(this.player); + int chapter = index - StarGuide.CHAPTER_COUNT; + return chapter >= 0 && chapter < seen.size() ? seen.get(chapter) : 0; } @Override diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java index 6649720..adbdaa5 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java @@ -1,5 +1,7 @@ package za.co.neroland.nerospace.fabric; +import java.util.List; + import com.mojang.serialization.Codec; import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; @@ -34,6 +36,13 @@ public final class FabricAttachments { .persistent(Codec.INT) .buildAndRegister(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "terraform_stage")); + /** Per-player: one Star Guide "seen" bitmask per chapter (bit i = step i acknowledged). */ + public static final AttachmentType> STAR_GUIDE_SEEN = AttachmentRegistry.>builder() + .initializer(List::of) + .persistent(Codec.INT.listOf()) + .copyOnDeath() + .buildAndRegister(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "star_guide_seen")); + private FabricAttachments() { } diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java index 29ef4ca..be86970 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java @@ -75,4 +75,14 @@ public int getTerraformStage(LevelChunk chunk) { public void setTerraformStage(LevelChunk chunk, int value) { chunk.setAttached(FabricAttachments.TERRAFORM_STAGE, value); } + + @Override + public java.util.List getStarGuideSeen(Player player) { + return player.getAttachedOrCreate(FabricAttachments.STAR_GUIDE_SEEN); + } + + @Override + public void setStarGuideSeen(Player player, java.util.List value) { + player.setAttached(FabricAttachments.STAR_GUIDE_SEEN, value); + } } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java index 024ce14..f78a11f 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java @@ -1,5 +1,6 @@ package za.co.neroland.nerospace.neoforge; +import java.util.List; import java.util.function.Supplier; import com.mojang.serialization.Codec; @@ -43,6 +44,14 @@ public final class NeoForgeAttachments { .serialize(Codec.INT.fieldOf("terraform_stage")) .build()); + /** Per-player: one Star Guide "seen" bitmask per chapter (bit i = step i acknowledged). */ + public static final Supplier>> STAR_GUIDE_SEEN = ATTACHMENT_TYPES.register( + "star_guide_seen", + () -> AttachmentType.>builder(() -> List.of()) + .serialize(Codec.INT.listOf().fieldOf("star_guide_seen")) + .copyOnDeath() + .build()); + private NeoForgeAttachments() { } diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java index 2db61ac..88dc0e5 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -79,4 +79,14 @@ public int getTerraformStage(LevelChunk chunk) { public void setTerraformStage(LevelChunk chunk, int value) { chunk.setData(NeoForgeAttachments.TERRAFORM_STAGE.get(), value); } + + @Override + public java.util.List getStarGuideSeen(Player player) { + return player.getData(NeoForgeAttachments.STAR_GUIDE_SEEN.get()); + } + + @Override + public void setStarGuideSeen(Player player, java.util.List value) { + player.setData(NeoForgeAttachments.STAR_GUIDE_SEEN.get(), value); + } } From bb7a432c5de0c047ede09529fe8f915084b8edd1 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:00:36 +0200 Subject: [PATCH 67/82] Add StarGuideGrants and wire into server ticks Introduce StarGuideGrants (multiloader/common) to directly award the terraform guide advancements (guide/terraformed_ground at stage >=1, guide/living_world at stage >=3) by inspecting the player's chunk terraform stage on a 40-tick interval and awarding remaining criteria via PlayerAdvancements. Wire the new tick method into Fabric and NeoForge server tick handlers so each ServerPlayer runs OxygenManager.tick and StarGuideGrants.tick. Update the port checklist doc to reflect the code-granted terraform advancements (slice 2d) and adjust port counts/status notes. --- docs/MULTILOADER_PORT_CHECKLIST.md | 20 +++++-- .../progression/StarGuideGrants.java | 59 +++++++++++++++++++ .../nerospace/fabric/NerospaceFabric.java | 6 +- .../nerospace/neoforge/NerospaceNeoForge.java | 2 + 4 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 129884a..8fc509f 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,15 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~212 classes ported, ~52 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~213 classes ported, ~51 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — Star Guide slice 2d (terraform advancements code-granted).** All 4 cells compile +> green. `progression/StarGuideGrants` awards `guide/terraformed_ground` + `guide/living_world` from the +> per-player tick when the player stands on terraformed/living ground — routing around `ModCriteria` by +> directly awarding the impossible-criterion advancements. **41/42 advancements now track real completion;** +> only `station_charter` stays inert (blocked on station founding). + > **2026-06-21 update — Star Guide slice 2c (seen-pulse; the guide is now feature-complete).** All 4 cells > compile green. Added a `List` `STAR_GUIDE_SEEN` player attachment via the existing seam + > restored the menu seen-masks + screen pulse (completed-but-unseen steps pulse until clicked). 26.x gotcha: @@ -399,9 +405,15 @@ checked by a headless build). completed-but-unseen pulse (clicking a step acknowledges it). **The Star Guide is now feature-complete** (browse + live progress + hologram + seen-pulse). 26.x gotcha: NeoForge `AttachmentType.builder(...)` is overloaded, so the default must be a lambda `() -> List.of()` (not the `List::of` method ref — ambiguous). -- [ ] **Slice 2d (deferred, blocked).** Converting the 3 `impossible` advancements back to real completion - (needs `ModCriteria` via reflection/version-split, or code-granting from terraform/station events); the - station_charter / new_life guide-step icons resolve once STATION_CHARTER / LOPER_HAUNCH are ported. +- [x] **Slice 2d — terraform advancements code-granted (4 cells green).** `progression/StarGuideGrants` + (driven from the per-player server tick, beside `OxygenManager.tick`) awards the impossible-criterion + `guide/terraformed_ground` (chunk stage ≥ 1) and `guide/living_world` (stage ≥ 3) directly when the player + stands on terraformed / fully-living ground — replicating the standalone mod's `PlayerTrigger` **without** + `ModCriteria`. **41 of 42 advancements now track real completion.** 26.x: award via + `getOrStartProgress(holder).getRemainingCriteria()` → `PlayerAdvancements.award(holder, criterion)`. +- [ ] **Slice 2e (deferred, blocked).** Only `guide/station_charter` stays inert (its founded_station trigger + needs the unported station-founding system). The station_charter / new_life guide-step icons resolve once + STATION_CHARTER / LOPER_HAUNCH are ported. ### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** - [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java new file mode 100644 index 0000000..8218fd6 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java @@ -0,0 +1,59 @@ +package za.co.neroland.nerospace.progression; + +import net.minecraft.advancements.AdvancementHolder; +import net.minecraft.advancements.AdvancementProgress; +import net.minecraft.resources.Identifier; +import net.minecraft.server.ServerAdvancementManager; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.platform.Services; + +/** + * Code-grants the terraform progression advancements the multiloader can't fire with a custom criterion + * trigger. {@code ModCriteria} is deferred (its {@code PlayerTrigger} base moved packages 26.1↔26.2), so + * the {@code guide/terraformed_ground} and {@code guide/living_world} advancements ship as + * {@code minecraft:impossible} (they load and keep the advancement tree intact). This awards their + * remaining criteria directly when the player stands on terraformed / fully-living ground — replicating + * the standalone mod's {@code PlayerTrigger} without the version-split class. Driven from the per-player + * server tick (alongside {@code OxygenManager.tick}). {@code guide/station_charter} stays inert until the + * station-founding system is ported. + */ +public final class StarGuideGrants { + + /** Throttle: progression is slow, so a periodic chunk-stage check is plenty. */ + private static final int CHECK_INTERVAL_TICKS = 40; + + private StarGuideGrants() { + } + + public static void tick(ServerPlayer player) { + if (player.tickCount % CHECK_INTERVAL_TICKS != 0) { + return; + } + LevelChunk chunk = player.level().getChunkAt(player.blockPosition()); + int stage = Services.PLATFORM.getTerraformStage(chunk); + if (stage >= 1) { + grant(player, "guide/terraformed_ground"); + } + if (stage >= 3) { + grant(player, "guide/living_world"); + } + } + + private static void grant(ServerPlayer player, String path) { + ServerAdvancementManager manager = player.level().getServer().getAdvancements(); + AdvancementHolder holder = manager.get(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, path)); + if (holder == null) { + return; + } + AdvancementProgress progress = player.getAdvancements().getOrStartProgress(holder); + if (progress.isDone()) { + return; + } + for (String criterion : progress.getRemainingCriteria()) { + player.getAdvancements().award(holder, criterion); + } + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 4d0fd89..09fdfa1 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -33,6 +33,7 @@ import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.telemetry.NerospaceTelemetry; +import za.co.neroland.nerospace.progression.StarGuideGrants; import za.co.neroland.nerospace.world.OxygenManager; /** @@ -89,7 +90,10 @@ public void register(EntityType type, SpawnPlacementType plac FabricAttachments.init(); FabricNetwork.registerCommon(); ServerTickEvents.END_SERVER_TICK.register(server -> { - server.getPlayerList().getPlayers().forEach(OxygenManager::tick); + server.getPlayerList().getPlayers().forEach(player -> { + OxygenManager.tick(player); + StarGuideGrants.tick(player); + }); MeteorEvents.tick(server); OxygenFieldEvents.tick(server); TerraformDrift.tick(server); diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index abb1704..8c16a09 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -28,6 +28,7 @@ import za.co.neroland.nerospace.registry.ModEntityAttributes; import za.co.neroland.nerospace.registry.ModSpawnPlacements; import za.co.neroland.nerospace.registry.NeoForgeRegistrationFactory; +import za.co.neroland.nerospace.progression.StarGuideGrants; import za.co.neroland.nerospace.world.OxygenManager; import za.co.neroland.nerospace.world.TerraformDrift; import za.co.neroland.nerospace.world.TerraformManager; @@ -58,6 +59,7 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { NeoForge.EVENT_BUS.addListener((PlayerTickEvent.Post event) -> { if (event.getEntity() instanceof ServerPlayer serverPlayer) { OxygenManager.tick(serverPlayer); + StarGuideGrants.tick(serverPlayer); } }); // Natural meteor showers + oxygen-field diffusion + terraform drift: tick the per-level drivers once per server tick. From b88e04018c44fa45e46826d07f3ebe8803eb6b16 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:18:06 +0200 Subject: [PATCH 68/82] Implement station founding: charter, core, registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full station-founding flow driven by a new Station Charter item: right-clicking the charter allocates a slot in a server-wide, POPIA-clean StationRegistry (SavedData), lays a 7×7 landing pad in the void station dimension, places and binds a Station Core block entity, and teleports the player there. Breaking the Station Core unregisters the slot and drops the named charter. Changes include: - New classes: StationCharterItem, StationCoreBlock, StationCoreBlockEntity, StationRegistry (SavedData + codec). - Register Station Core block & block-entity and Station Charter item in ModBlocks/ModBlockEntities/ModItems; add charter to creative tab. - Make StarGuide use the real station_charter item icon and make StarGuideGrants.grant(...) public for code-granted advancements. - Add assets: blockstate/model/texture for station_core and item model/texture for station_charter; add lang entries and update guide/station_charter advancement icon. - Update docs/MULTILOADER_PORT_CHECKLIST.md to record completion of station founding. This decouples founding from the deferred rocket FOUND flow and routes the station_charter advancement via code-granting so all guide advancements now track real completion. --- docs/MULTILOADER_PORT_CHECKLIST.md | 27 ++- .../nerospace/item/StationCharterItem.java | 99 +++++++++++ .../nerospace/progression/StarGuide.java | 3 +- .../progression/StarGuideGrants.java | 3 +- .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/registry/ModBlocks.java | 7 + .../neroland/nerospace/registry/ModItems.java | 7 +- .../nerospace/rocket/StationCoreBlock.java | 71 ++++++++ .../rocket/StationCoreBlockEntity.java | 96 +++++++++++ .../nerospace/rocket/StationRegistry.java | 157 ++++++++++++++++++ .../nerospace/blockstates/station_core.json | 7 + .../nerospace/items/station_charter.json | 6 + .../assets/nerospace/lang/en_us.json | 6 + .../nerospace/models/block/station_core.json | 6 + .../models/item/station_charter.json | 6 + .../nerospace/textures/block/station_core.png | Bin 0 -> 419 bytes .../textures/item/station_charter.png | Bin 0 -> 237 bytes .../advancement/guide/station_charter.json | 2 +- 18 files changed, 496 insertions(+), 12 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/item/StationCharterItem.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlock.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlockEntity.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationRegistry.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/station_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/station_charter.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/station_core.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/item/station_charter.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/station_core.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/item/station_charter.png diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 8fc509f..cff333d 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,19 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~213 classes ported, ~51 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~217 classes ported, ~47 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — station founding (charter-driven; closes the last advancement).** All 4 cells green +> (full `:neoforge:build`+`:fabric:build` on 26.2; compile on 26.1.2). Ported `rocket/{StationRegistry +> (SavedData, POPIA-clean — no player identity), StationCoreBlock, StationCoreBlockEntity}` + a new +> `item/StationCharterItem` whose right-click founds a station (allocates a slot, lays the 7×7 pad, binds a +> Station Core in the `nerospace:station` void dim, travels there) and code-grants `guide/station_charter` — +> **decoupling founding from the deferred rocket FOUND row.** Registered block (no block item / loot table) + +> BE + charter item; copied assets + lang; repointed the Star-Guide step + advancement icons to the now-real +> `station_charter` item. **All 42 advancements now track real completion.** Break the Core to unregister + +> reclaim the (named) charter. Deferred (slice 2): the rocket's per-station selection/return rows. + > **2026-06-21 update — Star Guide slice 2d (terraform advancements code-granted).** All 4 cells compile > green. `progression/StarGuideGrants` awards `guide/terraformed_ground` + `guide/living_world` from the > per-player tick when the player stands on terraformed/living ground — routing around `ModCriteria` by @@ -240,9 +250,11 @@ checked by a headless build). - [x] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer` (bakes each tier layer directly — no model-layer registry), `RocketRenderState`; entity + item textures copied. - [x] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). -- [ ] `StationCoreBlock`(+BE), `StationRegistry` (multi-station slots), Station Charter, `founded_station` - criterion — **deferred**: needs the data-attachment + criteria seams (+ structures). The Orbital Station - destination currently docks the rider at the shared origin platform. +- [x] **Station founding DONE (4 cells green).** `StationCoreBlock`(+BE), `StationRegistry` (SavedData, + POPIA-clean), and a new `StationCharterItem` — right-click the charter to found a station (slot + 7×7 pad + + bound Core in the void station dim) and travel there; breaking the Core unregisters + pops the named charter; + `guide/station_charter` is code-granted on founding (routes around `ModCriteria`). Founding is **charter-driven** + rather than via the rocket FOUND row (the rocket's per-station selection/return rows remain deferred). ### Quarry (`machine/quarry/` 11 + client) — **DONE (4 cells green); modules + BER deferred** - [x] Area miner ported: `QuarryControllerBlock`(+BE) + `QuarryMenu`/`QuarryScreen`, `QuarryFrameBlock`, @@ -411,9 +423,10 @@ checked by a headless build). stands on terraformed / fully-living ground — replicating the standalone mod's `PlayerTrigger` **without** `ModCriteria`. **41 of 42 advancements now track real completion.** 26.x: award via `getOrStartProgress(holder).getRemainingCriteria()` → `PlayerAdvancements.award(holder, criterion)`. -- [ ] **Slice 2e (deferred, blocked).** Only `guide/station_charter` stays inert (its founded_station trigger - needs the unported station-founding system). The station_charter / new_life guide-step icons resolve once - STATION_CHARTER / LOPER_HAUNCH are ported. +- [x] **Slice 2e — DONE via station founding.** `guide/station_charter` is now code-granted when a station is + founded (the charter item), and its Star-Guide step + advancement icons point at the now-real `station_charter` + item. **All 42 advancements track real completion.** Only the `new_life` guide-step icon stays substituted + (Meadow Loper spawn egg) until `LOPER_HAUNCH` is ported — purely cosmetic. ### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** - [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/StationCharterItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/StationCharterItem.java new file mode 100644 index 0000000..85050eb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/StationCharterItem.java @@ -0,0 +1,99 @@ +package za.co.neroland.nerospace.item; + +import java.util.Set; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; + +import za.co.neroland.nerospace.progression.StarGuideGrants; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModDimensions; +import za.co.neroland.nerospace.rocket.StationCoreBlockEntity; +import za.co.neroland.nerospace.rocket.StationRegistry; + +/** + * The Station Charter — founds a player station. Right-click to allocate the next station slot in the + * {@code nerospace:station} void dimension, lay a 7×7 landing pad, anchor a bound {@link + * StationCoreBlockEntity}, and travel there. Rename the charter in an anvil to name the station; + * breaking the Station Core unregisters it and pops the charter back (re-foundable elsewhere). + * + *

Cross-loader note: the standalone mod founds via the rocket's FOUND launch node; the multiloader + * rocket deferred its station-selection rows, so founding is driven from the charter directly here + * (the {@code guide/station_charter} advancement is code-granted, routing around the deferred + * {@code ModCriteria} the same way the terraform advancements are).

+ */ +public class StationCharterItem extends Item { + + /** 7×7 landing pad (radius 3), matching the standalone mod's station platform. */ + private static final int PLATFORM_RADIUS = 3; + + public StationCharterItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (level.isClientSide() || !(player instanceof ServerPlayer serverPlayer)) { + return InteractionResult.SUCCESS; + } + MinecraftServer server = serverPlayer.level().getServer(); + if (server == null) { + return InteractionResult.PASS; + } + ServerLevel station = server.getLevel(ModDimensions.STATION_LEVEL); + if (station == null) { + return InteractionResult.PASS; + } + + StationRegistry registry = StationRegistry.get(server); + if (registry.isFull()) { + serverPlayer.sendSystemMessage(Component.translatable("item.nerospace.station_charter.full")); + return InteractionResult.SUCCESS; + } + + ItemStack held = player.getItemInHand(hand); + Component customName = held.get(DataComponents.CUSTOM_NAME); + StationRegistry.StationEntry entry = registry.found(customName == null ? null : customName.getString()); + if (entry == null) { + serverPlayer.sendSystemMessage(Component.translatable("item.nerospace.station_charter.full")); + return InteractionResult.SUCCESS; + } + held.shrink(1); + + BlockPos centre = entry.center(); + station.getChunk(centre.getX() >> 4, centre.getZ() >> 4); + buildStationPlatform(station, centre); + station.setBlockAndUpdate(centre, ModBlocks.STATION_CORE.get().defaultBlockState()); + if (station.getBlockEntity(centre) instanceof StationCoreBlockEntity core) { + core.bindStation(entry.slot(), entry.name()); + } + + serverPlayer.teleportTo(station, centre.getX() + 0.5, centre.getY() + 1.0, centre.getZ() + 0.5, + Set.of(), serverPlayer.getYRot(), serverPlayer.getXRot(), true); + StarGuideGrants.grant(serverPlayer, "guide/station_charter"); + serverPlayer.sendSystemMessage(Component.translatable( + "item.nerospace.station_charter.founded", entry.name())); + return InteractionResult.SUCCESS; + } + + /** Lay a 7×7 station-floor landing pad so the arriving rider has solid ground in the void. */ + private static void buildStationPlatform(ServerLevel level, BlockPos centre) { + BlockState floor = ModBlocks.STATION_FLOOR.get().defaultBlockState(); + for (int dx = -PLATFORM_RADIUS; dx <= PLATFORM_RADIUS; dx++) { + for (int dz = -PLATFORM_RADIUS; dz <= PLATFORM_RADIUS; dz++) { + level.setBlockAndUpdate(new BlockPos(centre.getX() + dx, centre.getY(), centre.getZ() + dz), floor); + } + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java index 4d020fe..8cafaf5 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java @@ -76,8 +76,7 @@ private static Step step(String id, Supplier icon, String ad step("rocket_launch_pad", () -> ModBlocks.ROCKET_LAUNCH_PAD.get(), "guide/rocket_launch_pad"), step("rocket_tier_1", () -> ModItems.ROCKET_TIER_1.get(), "rocket"), step("station", () -> ModBlocks.STATION_FLOOR.get(), "station"), - // STATION_CHARTER not yet ported — substitute the Station Floor icon. - step("station_charter", () -> ModBlocks.STATION_FLOOR.get(), "guide/station_charter"))), + step("station_charter", () -> ModItems.STATION_CHARTER.get(), "guide/station_charter"))), new Chapter("new_worlds", List.of( step("rocket_tier_2", () -> ModItems.ROCKET_TIER_2.get(), "guide/rocket_tier_2"), step("greenxertz", () -> ModItems.NEROSTEEL_INGOT.get(), "greenxertz"), diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java index 8218fd6..a6dfef7 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java @@ -42,7 +42,8 @@ public static void tick(ServerPlayer player) { } } - private static void grant(ServerPlayer player, String path) { + /** Awards an impossible-criterion guide advancement directly (routes around the deferred ModCriteria). */ + public static void grant(ServerPlayer player, String path) { ServerAdvancementManager manager = player.level().getServer().getAdvancements(); AdvancementHolder holder = manager.get(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, path)); if (holder == null) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index 5cdc9d9..e771356 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -20,6 +20,7 @@ import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; import za.co.neroland.nerospace.progression.StarGuideBlockEntity; +import za.co.neroland.nerospace.rocket.StationCoreBlockEntity; import za.co.neroland.nerospace.storage.CreativeBatteryBlockEntity; import za.co.neroland.nerospace.storage.CreativeFluidTankBlockEntity; import za.co.neroland.nerospace.storage.CreativeGasTankBlockEntity; @@ -136,6 +137,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("star_guide", key -> new BlockEntityType<>(StarGuideBlockEntity::new, java.util.Set.of(ModBlocks.STAR_GUIDE.get()))); + public static final RegistryEntry> STATION_CORE = + BLOCK_ENTITIES.register("station_core", + key -> new BlockEntityType<>(StationCoreBlockEntity::new, java.util.Set.of(ModBlocks.STATION_CORE.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 848ac5c..5f0a2eb 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -31,6 +31,7 @@ import za.co.neroland.nerospace.pipe.UniversalPipeBlock; import za.co.neroland.nerospace.progression.StarGuideBlock; import za.co.neroland.nerospace.rocket.LaunchGantryBlock; +import za.co.neroland.nerospace.rocket.StationCoreBlock; import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; import za.co.neroland.nerospace.storage.CreativeBatteryBlock; import za.co.neroland.nerospace.storage.CreativeFluidTankBlock; @@ -108,6 +109,12 @@ public final class ModBlocks { .setId(key).mapColor(MapColor.COLOR_BLACK).strength(4.0F, 6.0F) .requiresCorrectToolForDrops().lightLevel(s -> 9).sound(SoundType.AMETHYST))); + /** The founded-station anchor. Placed only by founding (no block item / loot table); breaking it pops the charter. */ + public static final RegistryEntry STATION_CORE = BLOCKS.register("station_core", + key -> new StationCoreBlock(BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_CYAN).strength(4.0F, 1200.0F) + .requiresCorrectToolForDrops().lightLevel(s -> 10).sound(SoundType.METAL))); + // Block entity — item storage (pilot for the block-entity + capability seam). public static final RegistryEntry ITEM_STORE = BLOCKS.register("item_store", key -> new ItemStoreBlock(BlockBehaviour.Properties.of() diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index d95a51b..d7d51db 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -36,6 +36,7 @@ import za.co.neroland.nerospace.item.PipeFilterItem; import za.co.neroland.nerospace.item.PipeUpgradeItem; import za.co.neroland.nerospace.item.StarGuideBookItem; +import za.co.neroland.nerospace.item.StationCharterItem; import za.co.neroland.nerospace.meteor.MeteorCallerItem; import za.co.neroland.nerospace.module.ModuleType; import za.co.neroland.nerospace.module.UpgradeModuleItem; @@ -166,6 +167,10 @@ public final class ModItems { public static final RegistryEntry GLACIRA_COMPASS = ITEMS.register("glacira_compass", key -> new DestinationCompassItem(new Item.Properties().stacksTo(1).setId(key), ModDimensions.GLACIRA_LEVEL)); + /** Station Charter: right-click founds a player station in the void station dimension + travels there. */ + public static final RegistryEntry STATION_CHARTER = ITEMS.register("station_charter", + key -> new StationCharterItem(new Item.Properties().stacksTo(16).setId(key))); + /** Creative-only Meteor Caller: right-click the ground to call a loot-bearing meteor down on that spot. */ public static final RegistryEntry METEOR_CALLER = ITEMS.register("meteor_caller", key -> new MeteorCallerItem(new Item.Properties().stacksTo(1).setId(key))); @@ -287,7 +292,7 @@ public static Map, List> creativeTabItems GREENXERTZ_NAVIGATOR.get(), STATION_COMPASS.get(), GREENXERTZ_COMPASS.get(), CINDARA_COMPASS.get(), GLACIRA_COMPASS.get(), METEOR_CALLER.get(), METEOR_TRACKER.get(), CONFIGURATOR.get(), PIPE_FILTER.get(), SPEED_UPGRADE.get(), CAPACITY_UPGRADE.get(), - STAR_GUIDE_BOOK.get()), + STAR_GUIDE_BOOK.get(), STATION_CHARTER.get()), CreativeModeTabs.SPAWN_EGGS, List.of(XERTZ_STALKER_SPAWN_EGG.get(), QUARTZ_CRAWLER_SPAWN_EGG.get(), GREENLING_SPAWN_EGG.get(), ALIEN_VILLAGER_SPAWN_EGG.get(), CINDER_STALKER_SPAWN_EGG.get(), diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlock.java new file mode 100644 index 0000000..d8475f8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlock.java @@ -0,0 +1,71 @@ +package za.co.neroland.nerospace.rocket; + +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +/** + * The Station Core block: placed only by the founding flow (no recipe, no loot table — breaking it + * pops a named charter via the block entity's remove hook and unregisters the station). Right-click + * reads the station's name; comparator reads 15 while bound. + * + *

Cross-loader port: vanilla {@code BaseEntityBlock} interactions; identical to the standalone mod.

+ */ +public class StationCoreBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(StationCoreBlock::new); + + public StationCoreBlock(Properties properties) { + super(properties); + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new StationCoreBlockEntity(pos, state); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, + Player player, BlockHitResult hit) { + if (!level.isClientSide() && level.getBlockEntity(pos) instanceof StationCoreBlockEntity core) { + player.sendSystemMessage(core.isBound() + ? Component.translatable("block.nerospace.station_core.bound", core.stationName()) + : Component.translatable("block.nerospace.station_core.unbound")); + } + return InteractionResult.SUCCESS; + } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof StationCoreBlockEntity core + ? core.comparatorSignal() : 0; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlockEntity.java new file mode 100644 index 0000000..0fa1d92 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationCoreBlockEntity.java @@ -0,0 +1,96 @@ +package za.co.neroland.nerospace.rocket; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Containers; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; + +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The Station Core — the anchor placed at a founded station's centre. Holds the station's slot id + + * name; breaking it unregisters the station and pops a charter named after it (re-foundable + * elsewhere). Only obtainable by founding — there is no crafting recipe and the block has no loot + * table. + * + *

Cross-loader port: vanilla value-IO + {@code Containers}/{@code DataComponents}; identical to the + * standalone mod.

+ */ +public class StationCoreBlockEntity extends BlockEntity { + + /** −1 until {@link #bindStation} — a core placed outside the founding flow anchors nothing. */ + private int slot = -1; + private String stationName = ""; + + public StationCoreBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.STATION_CORE.get(), pos, state); + } + + /** Binds this core to its founded station (called by the founding flow / tests). */ + public void bindStation(int slot, String name) { + this.slot = slot; + this.stationName = name; + setChanged(); + } + + public int stationSlot() { + return this.slot; + } + + public String stationName() { + return this.stationName; + } + + public boolean isBound() { + return this.slot >= 0; + } + + public int comparatorSignal() { + return isBound() ? 15 : 0; + } + + /** + * Breaking the core unregisters its station and pops a charter named after it. The platform simply + * remains as scrap in the void; the slot is never reused (see {@link StationRegistry}). + */ + @Override + public void preRemoveSideEffects(BlockPos pos, BlockState state) { + super.preRemoveSideEffects(pos, state); + if (!(this.level instanceof ServerLevel serverLevel) || !isBound()) { + return; + } + StationRegistry.StationEntry removed = + StationRegistry.get(serverLevel.getServer()).unregister(this.slot); + ItemStack charter = new ItemStack(ModItems.STATION_CHARTER.get()); + String name = removed != null ? removed.name() : this.stationName; + if (name != null && !name.isBlank()) { + charter.set(DataComponents.CUSTOM_NAME, Component.literal(name)); + } + Containers.dropItemStack(serverLevel, + pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, charter); + this.slot = -1; + } + + // --- Persistence --------------------------------------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putInt("Slot", this.slot); + output.putString("StationName", this.stationName); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + this.slot = input.getIntOr("Slot", -1); + this.stationName = input.getStringOr("StationName", ""); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationRegistry.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationRegistry.java new file mode 100644 index 0000000..39b0cbc --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/StationRegistry.java @@ -0,0 +1,157 @@ +package za.co.neroland.nerospace.rocket; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +import net.minecraft.core.BlockPos; +import net.minecraft.resources.Identifier; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.saveddata.SavedData; +import net.minecraft.world.level.saveddata.SavedDataType; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.NerospaceCommon; + +/** + * The server-global registry of player-founded stations. Stored on the overworld (always loaded) via + * the same {@link SavedDataType} codec pattern as {@code OxygenFieldManager}. All stations live in the + * single {@code nerospace:station} void dimension at well-separated X offsets; slot numbers are never + * reused, so a new station can never be founded inside an abandoned hull. + * + *

Privacy (POPIA/GDPR): entries deliberately store NO player identity — no names, no UUIDs, + * no founder field. Stations are server-global and usable by everyone, so nothing personal is ever + * written to disk.

+ * + *

Cross-loader port: identical to the standalone mod except the {@code SavedDataType} uses the 4-arg + * NeoForm ctor ({@code DataFixTypes = null}) and {@code org.jetbrains.annotations.Nullable}.

+ */ +public final class StationRegistry extends SavedData { + + public static final Identifier ID = Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "stations"); + + /** Hard cap on founded stations (a full X-row of ~262k blocks; lift post-1.0 if ever hit). */ + public static final int MAX_STATIONS = 64; + + /** X spacing between station slots — far beyond any render/simulation distance. */ + public static final int SLOT_SPACING = 4096; + + /** The Y level every platform is built at (matches the origin public platform). */ + public static final int PLATFORM_Y = 64; + + public static final SavedDataType TYPE = new SavedDataType<>( + ID, StationRegistry::new, codec(), null); + + /** One founded station. The name comes from the founding charter (or an auto "Station N"). */ + public record StationEntry(int slot, String name, BlockPos center) { + + public static final Codec CODEC = RecordCodecBuilder.create(inst -> inst.group( + Codec.INT.fieldOf("slot").forGetter(StationEntry::slot), + Codec.STRING.fieldOf("name").forGetter(StationEntry::name), + BlockPos.CODEC.fieldOf("center").forGetter(StationEntry::center) + ).apply(inst, StationEntry::of)); + + /** Boxed-parameter factory for the codec (avoids the ECJ unboxing null-safety warning). */ + private static StationEntry of(Integer slot, String name, BlockPos center) { + return new StationEntry(slot.intValue(), name, center); + } + } + + /** Insertion-ordered (founding order) — the UI cycles stations in this order. */ + private final Map stations = new LinkedHashMap<>(); + private int nextSlot; + + public StationRegistry() { + } + + private static Codec codec() { + return RecordCodecBuilder.create(inst -> inst.group( + StationEntry.CODEC.listOf().fieldOf("stations") + .forGetter(r -> new ArrayList<>(r.stations.values())), + Codec.INT.fieldOf("next_slot").forGetter(r -> r.nextSlot) + ).apply(inst, StationRegistry::fromEntries)); + } + + private static StationRegistry fromEntries(List entries, Integer nextSlot) { + StationRegistry registry = new StationRegistry(); + for (StationEntry entry : entries) { + registry.stations.put(entry.slot(), entry); + } + registry.nextSlot = nextSlot.intValue(); + return registry; + } + + /** The one registry, stored on the overworld so it is always loaded. */ + public static StationRegistry get(MinecraftServer server) { + return server.overworld().getDataStorage().computeIfAbsent(TYPE); + } + + /** Where station slot {@code i} sits in the station dimension (the origin platform is slot −1). */ + public static BlockPos centerFor(int slot) { + return new BlockPos(SLOT_SPACING * (slot + 1), PLATFORM_Y, 0); + } + + /** + * Founds a new station: allocates the next slot (never reused), registers and returns the entry — + * or {@code null} when {@link #MAX_STATIONS} is reached. A blank name auto-names "Station N". + */ + @Nullable + public StationEntry found(@Nullable String name) { + if (this.stations.size() >= MAX_STATIONS) { + return null; + } + int slot = this.nextSlot++; + String stationName = name == null || name.isBlank() ? "Station " + (slot + 1) : name; + StationEntry entry = new StationEntry(slot, stationName, centerFor(slot)); + this.stations.put(slot, entry); + setDirty(); + return entry; + } + + /** Unregisters {@code slot}; @return the removed entry, or {@code null} if it wasn't registered. */ + @Nullable + public StationEntry unregister(int slot) { + StationEntry removed = this.stations.remove(slot); + if (removed != null) { + setDirty(); + } + return removed; + } + + @Nullable + public StationEntry get(int slot) { + return this.stations.get(slot); + } + + /** All stations in founding order (the UI's cycle order). */ + public List all() { + return List.copyOf(this.stations.values()); + } + + public int count() { + return this.stations.size(); + } + + public boolean isFull() { + return this.stations.size() >= MAX_STATIONS; + } + + /** The slot after {@code currentSlot} in founding order (wraps; −1/none starts at the first). */ + public int nextSlotAfter(int currentSlot) { + List ordered = all(); + if (ordered.isEmpty()) { + return -1; + } + for (int i = 0; i < ordered.size(); i++) { + if (ordered.get(i).slot() == currentSlot) { + return ordered.get((i + 1) % ordered.size()).slot(); + } + } + return ordered.get(0).slot(); + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_core.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_core.json new file mode 100644 index 0000000..c1eab99 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/station_core.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/station_core" + } + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/station_charter.json b/multiloader/common/src/main/resources/assets/nerospace/items/station_charter.json new file mode 100644 index 0000000..1cc14e3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/station_charter.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:item/station_charter" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 76bc39e..75f80ab 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -51,6 +51,9 @@ "block.nerospace.rocket_launch_pad.report.t3_ready": "Tier 3 ready: Station Wall ring or Heavy complex present", "block.nerospace.solar_panel": "Solar Panel", "block.nerospace.star_guide": "Star Guide", + "block.nerospace.station_core": "Station Core", + "block.nerospace.station_core.bound": "Station Core: %s", + "block.nerospace.station_core.unbound": "Station Core: not bound to a station", "block.nerospace.station_floor": "Station Floor", "block.nerospace.station_wall": "Station Wall", "block.nerospace.terraform_monitor": "Terraform Monitor", @@ -284,6 +287,9 @@ "item.nerospace.speed_module": "Speed Module", "item.nerospace.speed_upgrade": "Speed Upgrade", "item.nerospace.star_guide_book": "Star Guide Book", + "item.nerospace.station_charter": "Station Charter", + "item.nerospace.station_charter.founded": "Station founded: %s", + "item.nerospace.station_charter.full": "The station registry is full", "item.nerospace.station_compass": "Station Compass", "item.nerospace.woolly_drift_spawn_egg": "Woolly Drift Spawn Egg", "item.nerospace.xertz_quartz": "Xertz Quartz", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/station_core.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/station_core.json new file mode 100644 index 0000000..7bc2148 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/station_core.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "nerospace:block/station_core" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/item/station_charter.json b/multiloader/common/src/main/resources/assets/nerospace/models/item/station_charter.json new file mode 100644 index 0000000..83595b3 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/item/station_charter.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/generated", + "textures": { + "layer0": "nerospace:item/station_charter" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_core.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d3ba0a556a0db1297544011682715cd4280501 GIT binary patch literal 419 zcmV;U0bKrxP)lD;GJo4%cXzDwS1gD@iR9EG&bBV3i+W8A3>5kwOkD5%FhOS*1)N$JNUc zrV3&#&e>!=1;N)WyEF6No0)BRzuEE+BmjB-fH4MZQ;MPlAj?`f=b@P(YY%`i1^^Ku z%UVpPbDElbm*0m%g|%rY+<$+3dalT!B5=+#8V@~^Jq|O`wju{wu&->4;i!LEB`;q>VNsM}m{ALA zPnD1Qmt1uY0hj@+e8ITWjs$iNd}W7bf;mX}4L`*a09pIU*0|Hgye*l1-LKaru6qn0 z@1Ye%bKj1(qqA7$3s(7}rnaU2TX;qN~xf@|YkZHJ8y(1ei?c z!CscNBEs*alj%I%1HncAJp6g*d}LeIRe3ZVgjmD-&Gxi+RvXk`@d?Kzx>EMVQd$52 N002ovPDHLkV1iMRvxWcw literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/station_charter.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/station_charter.png new file mode 100644 index 0000000000000000000000000000000000000000..93ef9ab069c19055e140d15c6912c14a3ffeb067 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`TRdGHLn`JZCrD%*=s2^nnB(XC z2@@K^4>z%LOYq4?Fke63u`c2K#Nz*^AtDkWkawr@07LTOA9`kP8dWn2ZnmtG6hDx~ zV0PC0osGghh8by-yY!mY9el|Oq}itK5NYeke*dA0bva|Y)u{j;<(m!C<#GmVE(j&< z*qp)FmH2`o+r-Gg-~o@zqD6wU( Date: Sun, 21 Jun 2026 22:28:59 +0200 Subject: [PATCH 69/82] Add energy multiplier config; apply to generators Add energyRateMultiplier to NerospaceConfig (default 1.0, clamped 0.1..10) and persist it to nerospace.properties. Implement a scale(base, multiplier) helper plus parse/clamp helpers, load the value at mod init, and include the property in the written config comment. Wire the multiplier into generator code by applying NerospaceConfig.scale(...) for FE/tick in CombustionGenerator, PassiveGenerator, and SolarPanel so generator output follows the config. Update the docs checklist to mark the config seam slice 1 complete. --- docs/MULTILOADER_PORT_CHECKLIST.md | 18 ++++++-- .../nerospace/config/NerospaceConfig.java | 41 ++++++++++++++++++- .../CombustionGeneratorBlockEntity.java | 3 +- .../machine/PassiveGeneratorBlockEntity.java | 3 +- .../machine/SolarPanelBlockEntity.java | 3 +- 5 files changed, 61 insertions(+), 7 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index cff333d..8f3d908 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -4,6 +4,11 @@ Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still n cross-loader `multiloader/` project. As of this audit: **~217 classes ported, ~47 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — config seam slice 1 (energy multiplier).** All 4 cells compile green. Extended the +> properties `NerospaceConfig` with `energyRateMultiplier` (0.1×..10×) + a `scale()` clamp helper, wired into +> all three generators' FE/tick. Establishes the cross-loader balance-config pattern (properties file, no +> `ModConfigSpec` seam); the root's other 4 multipliers wire in incrementally (kept out of the file until wired). + > **2026-06-21 update — station founding (charter-driven; closes the last advancement).** All 4 cells green > (full `:neoforge:build`+`:fabric:build` on 26.2; compile on 26.1.2). Ported `rocket/{StationRegistry > (SavedData, POPIA-clean — no player identity), StationCoreBlock, StationCoreBlockEntity}` + a new @@ -512,9 +517,16 @@ checked by a headless build). - [ ] `command/NerospaceCommands` — `/nerospace` debug/admin commands (vanilla Brigadier; loader event differs). - [ ] `compat/jei/*` — recipe-viewer integration. NeoForge = JEI; Fabric would use REI/EMI. Cross-mod, low priority. -### Config / tuning -- [ ] `Config` + `Tuning` — NeoForge `ModConfigSpec`-based; needs a cross-loader config seam (or a simple - shared config). Many ported machines currently use inlined constants where the root reads `Tuning`. +### Config / tuning — **slice 1 DONE (4 cells green); cross-loader seam established** +- [x] **Slice 1 — config seam + energy multiplier.** Extended the properties-based `config/NerospaceConfig` + (no NeoForge `ModConfigSpec` — the cross-loader seam is the properties file the telemetry batch added) with + `energyRateMultiplier` (clamp 0.1×..10×, default 1) + a `scale(base, mult)` helper (min-1 clamp, mirroring + the root `Tuning` contract); wired it into the Combustion / Passive / Solar generator FE-per-tick. Loads at + mod init (before ticking). This proves the cross-loader balance-config pattern beyond the telemetry toggle. +- [ ] **Remaining (incremental).** The root's other four multipliers (`oxygenDrainMultiplier`, + `oxygenCapacityMultiplier`, `fuelCostMultiplier`, `machineSpeedMultiplier`) + the base-value constants — wire + each into its consumer(s) as a follow-up (kept out of the config file until wired, so every key does + something). A full `Tuning` base-value class is optional (the multiloader inlines base values per machine). ### Spawn rules - [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java index a45307b..f8a8956 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -22,9 +22,16 @@ public final class NerospaceConfig { private static final String FILE_NAME = "nerospace.properties"; private static final String KEY_TELEMETRY = "telemetryEnabled"; + private static final String KEY_ENERGY_RATE = "energyRateMultiplier"; + + /** Multiplier range (mirrors the root config spec): 0.1× .. 10×. */ + private static final double MULT_MIN = 0.1D; + private static final double MULT_MAX = 10.0D; /** Anonymous crash reporting (Sentry, EU) is ON by default; players opt out by setting this false. */ private static volatile boolean telemetryEnabled = true; + /** Scales the FE/tick of every generator (combustion, passive, solar). Clamped 0.1×..10×. */ + private static volatile double energyRateMultiplier = 1.0D; private static volatile boolean loaded; private NerospaceConfig() { @@ -34,6 +41,22 @@ public static boolean isTelemetryEnabled() { return telemetryEnabled; } + public static double energyRateMultiplier() { + return energyRateMultiplier; + } + + /** + * Applies a balance multiplier to a base integer rate, clamped to a minimum of 1 so an extreme low + * multiplier (0.1×) can never zero a rate (mirrors the root {@code Tuning} clamping contract). + */ + public static int scale(int base, double multiplier) { + return Math.max(1, (int) Math.round(base * multiplier)); + } + + private static double clampMultiplier(double value) { + return Math.max(MULT_MIN, Math.min(MULT_MAX, value)); + } + /** Reads (creating with defaults if absent) the config file. Safe to call once at mod init. */ public static synchronized void load() { if (loaded) { @@ -53,6 +76,8 @@ public static synchronized void load() { props.load(in); telemetryEnabled = Boolean.parseBoolean( props.getProperty(KEY_TELEMETRY, Boolean.toString(telemetryEnabled)).trim()); + energyRateMultiplier = clampMultiplier(parseDouble( + props.getProperty(KEY_ENERGY_RATE), energyRateMultiplier)); } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not read {}; using defaults.", FILE_NAME, e); } @@ -61,17 +86,31 @@ public static synchronized void load() { } } + /** Lenient double parse — falls back to {@code fallback} on null/blank/invalid input. */ + private static double parseDouble(String value, double fallback) { + if (value == null || value.isBlank()) { + return fallback; + } + try { + return Double.parseDouble(value.trim()); + } catch (NumberFormatException e) { + return fallback; + } + } + /** Writes the default config file with an explanatory comment (best-effort). */ private static void write(Path file) { Properties props = new Properties(); props.setProperty(KEY_TELEMETRY, Boolean.toString(telemetryEnabled)); + props.setProperty(KEY_ENERGY_RATE, Double.toString(energyRateMultiplier)); try { Files.createDirectories(file.getParent()); try (OutputStream out = Files.newOutputStream(file)) { props.store(out, "Nerospace config. telemetryEnabled: send anonymous, Nerospace-only " + "crash reports (Sentry, EU servers) — stack trace + mod/MC/loader/OS/Java " + "versions only; no IP, username, UUID, world data or chat; file paths are " - + "scrubbed of your account name. Set to false to opt out. See PRIVACY.md."); + + "scrubbed of your account name. Set to false to opt out. See PRIVACY.md. " + + "energyRateMultiplier: scales FE/tick of all generators (0.1..10, default 1)."); } } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not write {}; using defaults.", FILE_NAME, e); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java index 8e880a3..e45590d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.Nullable; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.menu.CombustionGeneratorMenu; @@ -109,7 +110,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { if (this.burnTime > 0) { if (this.energy.getAmount() < this.energy.getCapacity()) { this.burnTime--; - this.energy.generate(FE_PER_TICK); + this.energy.generate(NerospaceConfig.scale(FE_PER_TICK, NerospaceConfig.energyRateMultiplier())); } } else { ItemStack fuel = this.items.get(FUEL_SLOT); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java index aa25db5..386e788 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java @@ -20,6 +20,7 @@ import org.jetbrains.annotations.Nullable; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; @@ -95,7 +96,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { } if (this.coreTicks > 0 && this.energy.getAmount() < this.energy.getCapacity()) { this.coreTicks--; - this.energy.generate(FE_PER_TICK); + this.energy.generate(NerospaceConfig.scale(FE_PER_TICK, NerospaceConfig.energyRateMultiplier())); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java index 100c88d..1391678 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java @@ -7,6 +7,7 @@ import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; @@ -46,7 +47,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { if (level.isRaining() || level.isThundering()) { rate /= 2; } - this.energy.generate(rate); + this.energy.generate(NerospaceConfig.scale(rate, NerospaceConfig.energyRateMultiplier())); } @Override From 03770b38e25c39eab5d4bb935ad8ab48cec8c38d Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:33:27 +0200 Subject: [PATCH 70/82] Add oxygen multipliers to config and manager Introduce oxygenDrainMultiplier and oxygenCapacityMultiplier to NerospaceConfig: new property keys, volatile fields (default 1.0), getters, clamped range (0.1..10), and load/save support in nerospace.properties (with updated property comments). Update OxygenManager to apply these multipliers when scaling player/suit air capacity and drain (uses NerospaceConfig.scale and the new getters) and add the required import. This enables global tuning of oxygen depletion rates and air capacity. --- docs/MULTILOADER_PORT_CHECKLIST.md | 10 ++++---- .../nerospace/config/NerospaceConfig.java | 24 ++++++++++++++++++- .../nerospace/world/OxygenManager.java | 8 ++++--- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 8f3d908..7243b52 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -523,10 +523,12 @@ checked by a headless build). `energyRateMultiplier` (clamp 0.1×..10×, default 1) + a `scale(base, mult)` helper (min-1 clamp, mirroring the root `Tuning` contract); wired it into the Combustion / Passive / Solar generator FE-per-tick. Loads at mod init (before ticking). This proves the cross-loader balance-config pattern beyond the telemetry toggle. -- [ ] **Remaining (incremental).** The root's other four multipliers (`oxygenDrainMultiplier`, - `oxygenCapacityMultiplier`, `fuelCostMultiplier`, `machineSpeedMultiplier`) + the base-value constants — wire - each into its consumer(s) as a follow-up (kept out of the config file until wired, so every key does - something). A full `Tuning` base-value class is optional (the multiloader inlines base values per machine). +- [x] **Slice 2 — oxygen multipliers.** Added `oxygenDrainMultiplier` + `oxygenCapacityMultiplier` to + `NerospaceConfig`; wired into `OxygenManager` (per-check drain + player/suit air capacity, both `scale`-clamped; + the attachment default self-corrects on the first tick). **3 of the root's 5 multipliers now wired.** +- [ ] **Remaining (incremental).** `fuelCostMultiplier` (rocket launch/fuel cost) + `machineSpeedMultiplier` + (machine work intervals — inverse-scaled) — wire each into its consumer as a follow-up (kept out of the config + file until wired, so every key does something). A full `Tuning` base-value class is optional. ### Spawn rules - [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java index f8a8956..b61f3f5 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -23,6 +23,8 @@ public final class NerospaceConfig { private static final String FILE_NAME = "nerospace.properties"; private static final String KEY_TELEMETRY = "telemetryEnabled"; private static final String KEY_ENERGY_RATE = "energyRateMultiplier"; + private static final String KEY_OXYGEN_DRAIN = "oxygenDrainMultiplier"; + private static final String KEY_OXYGEN_CAPACITY = "oxygenCapacityMultiplier"; /** Multiplier range (mirrors the root config spec): 0.1× .. 10×. */ private static final double MULT_MIN = 0.1D; @@ -32,6 +34,10 @@ public final class NerospaceConfig { private static volatile boolean telemetryEnabled = true; /** Scales the FE/tick of every generator (combustion, passive, solar). Clamped 0.1×..10×. */ private static volatile double energyRateMultiplier = 1.0D; + /** Scales how fast oxygen drains off a safe zone (bare + suited). Clamped 0.1×..10×. */ + private static volatile double oxygenDrainMultiplier = 1.0D; + /** Scales player + suit air capacity. Clamped 0.1×..10×. */ + private static volatile double oxygenCapacityMultiplier = 1.0D; private static volatile boolean loaded; private NerospaceConfig() { @@ -45,6 +51,14 @@ public static double energyRateMultiplier() { return energyRateMultiplier; } + public static double oxygenDrainMultiplier() { + return oxygenDrainMultiplier; + } + + public static double oxygenCapacityMultiplier() { + return oxygenCapacityMultiplier; + } + /** * Applies a balance multiplier to a base integer rate, clamped to a minimum of 1 so an extreme low * multiplier (0.1×) can never zero a rate (mirrors the root {@code Tuning} clamping contract). @@ -78,6 +92,10 @@ public static synchronized void load() { props.getProperty(KEY_TELEMETRY, Boolean.toString(telemetryEnabled)).trim()); energyRateMultiplier = clampMultiplier(parseDouble( props.getProperty(KEY_ENERGY_RATE), energyRateMultiplier)); + oxygenDrainMultiplier = clampMultiplier(parseDouble( + props.getProperty(KEY_OXYGEN_DRAIN), oxygenDrainMultiplier)); + oxygenCapacityMultiplier = clampMultiplier(parseDouble( + props.getProperty(KEY_OXYGEN_CAPACITY), oxygenCapacityMultiplier)); } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not read {}; using defaults.", FILE_NAME, e); } @@ -103,6 +121,8 @@ private static void write(Path file) { Properties props = new Properties(); props.setProperty(KEY_TELEMETRY, Boolean.toString(telemetryEnabled)); props.setProperty(KEY_ENERGY_RATE, Double.toString(energyRateMultiplier)); + props.setProperty(KEY_OXYGEN_DRAIN, Double.toString(oxygenDrainMultiplier)); + props.setProperty(KEY_OXYGEN_CAPACITY, Double.toString(oxygenCapacityMultiplier)); try { Files.createDirectories(file.getParent()); try (OutputStream out = Files.newOutputStream(file)) { @@ -110,7 +130,9 @@ private static void write(Path file) { + "crash reports (Sentry, EU servers) — stack trace + mod/MC/loader/OS/Java " + "versions only; no IP, username, UUID, world data or chat; file paths are " + "scrubbed of your account name. Set to false to opt out. See PRIVACY.md. " - + "energyRateMultiplier: scales FE/tick of all generators (0.1..10, default 1)."); + + "energyRateMultiplier: scales FE/tick of all generators. oxygenDrainMultiplier: " + + "scales how fast air drains. oxygenCapacityMultiplier: scales air capacity. " + + "All multipliers 0.1..10, default 1."); } } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not write {}; using defaults.", FILE_NAME, e); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java index d244979..982300f 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java @@ -14,6 +14,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.platform.Services; import za.co.neroland.nerospace.registry.ModBlocks; import za.co.neroland.nerospace.registry.ModDimensions; @@ -73,7 +74,8 @@ public static void tick(ServerPlayer player) { } boolean suited = isFullSuit(player); - int max = suited ? OXYGEN_SUIT_MAX : OXYGEN_MAX; + int max = NerospaceConfig.scale(suited ? OXYGEN_SUIT_MAX : OXYGEN_MAX, + NerospaceConfig.oxygenCapacityMultiplier()); boolean airless = PLANETS.contains(level.dimension()) && !player.getAbilities().instabuild @@ -91,8 +93,8 @@ public static void tick(ServerPlayer player) { oxygen = max; } else { // An uncountered dimension hazard (Cindara heat / Glacira cold) multiplies the drain. - int drain = (suited ? SUIT_DRAIN_PER_CHECK : BARE_DRAIN_PER_CHECK) - * hazardDrainMultiplier(level, player); + int drain = NerospaceConfig.scale(suited ? SUIT_DRAIN_PER_CHECK : BARE_DRAIN_PER_CHECK, + NerospaceConfig.oxygenDrainMultiplier()) * hazardDrainMultiplier(level, player); oxygen = Math.max(0, oxygen - drain); hazardFeedback(level, player); } From eea57dea2df8a7a6d8b0d4efc429686ef8fd1f95 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:37:20 +0200 Subject: [PATCH 71/82] Add fuelCostMultiplier and wire to RocketTier Introduce a fuelCostMultiplier config and apply it to rocket fuel consumption. NerospaceConfig: add KEY_FUEL_COST, backing field, getter, load/save handling, clamped scaling and docs/help text. RocketTier: import config and use NerospaceConfig.scale(...) with fuelCostMultiplier in fuelPerLaunch(), still clamped to the tank capacity. Also update MULTILOADER_PORT_CHECKLIST.md to mark Slice 3 done and adjust the next slice notes. --- docs/MULTILOADER_PORT_CHECKLIST.md | 8 +++++--- .../co/neroland/nerospace/config/NerospaceConfig.java | 11 +++++++++++ .../za/co/neroland/nerospace/rocket/RocketTier.java | 6 ++++-- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 7243b52..96962c6 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -526,9 +526,11 @@ checked by a headless build). - [x] **Slice 2 — oxygen multipliers.** Added `oxygenDrainMultiplier` + `oxygenCapacityMultiplier` to `NerospaceConfig`; wired into `OxygenManager` (per-check drain + player/suit air capacity, both `scale`-clamped; the attachment default self-corrects on the first tick). **3 of the root's 5 multipliers now wired.** -- [ ] **Remaining (incremental).** `fuelCostMultiplier` (rocket launch/fuel cost) + `machineSpeedMultiplier` - (machine work intervals — inverse-scaled) — wire each into its consumer as a follow-up (kept out of the config - file until wired, so every key does something). A full `Tuning` base-value class is optional. +- [x] **Slice 3 — fuelCostMultiplier.** Added `fuelCostMultiplier`; wired into `RocketTier.fuelPerLaunch()` + (scaled, still clamped to the tank so a launch is always possible). **4 of the root's 5 multipliers wired.** +- [ ] **Slice 4 (last multiplier).** `machineSpeedMultiplier` — inverse-scaled (faster ⇒ shorter work + interval, clamped ≥1 tick); needs a `scaleInterval` helper + wiring into each machine's work period + (grinder, refinery, terraformer, quarry, …). Multiple consumers, so its own slice. ### Spawn rules - [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java index b61f3f5..fa7024b 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -25,6 +25,7 @@ public final class NerospaceConfig { private static final String KEY_ENERGY_RATE = "energyRateMultiplier"; private static final String KEY_OXYGEN_DRAIN = "oxygenDrainMultiplier"; private static final String KEY_OXYGEN_CAPACITY = "oxygenCapacityMultiplier"; + private static final String KEY_FUEL_COST = "fuelCostMultiplier"; /** Multiplier range (mirrors the root config spec): 0.1× .. 10×. */ private static final double MULT_MIN = 0.1D; @@ -38,6 +39,8 @@ public final class NerospaceConfig { private static volatile double oxygenDrainMultiplier = 1.0D; /** Scales player + suit air capacity. Clamped 0.1×..10×. */ private static volatile double oxygenCapacityMultiplier = 1.0D; + /** Scales the fuel a rocket burns per launch (clamped to the tank). Clamped 0.1×..10×. */ + private static volatile double fuelCostMultiplier = 1.0D; private static volatile boolean loaded; private NerospaceConfig() { @@ -59,6 +62,10 @@ public static double oxygenCapacityMultiplier() { return oxygenCapacityMultiplier; } + public static double fuelCostMultiplier() { + return fuelCostMultiplier; + } + /** * Applies a balance multiplier to a base integer rate, clamped to a minimum of 1 so an extreme low * multiplier (0.1×) can never zero a rate (mirrors the root {@code Tuning} clamping contract). @@ -96,6 +103,8 @@ public static synchronized void load() { props.getProperty(KEY_OXYGEN_DRAIN), oxygenDrainMultiplier)); oxygenCapacityMultiplier = clampMultiplier(parseDouble( props.getProperty(KEY_OXYGEN_CAPACITY), oxygenCapacityMultiplier)); + fuelCostMultiplier = clampMultiplier(parseDouble( + props.getProperty(KEY_FUEL_COST), fuelCostMultiplier)); } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not read {}; using defaults.", FILE_NAME, e); } @@ -123,6 +132,7 @@ private static void write(Path file) { props.setProperty(KEY_ENERGY_RATE, Double.toString(energyRateMultiplier)); props.setProperty(KEY_OXYGEN_DRAIN, Double.toString(oxygenDrainMultiplier)); props.setProperty(KEY_OXYGEN_CAPACITY, Double.toString(oxygenCapacityMultiplier)); + props.setProperty(KEY_FUEL_COST, Double.toString(fuelCostMultiplier)); try { Files.createDirectories(file.getParent()); try (OutputStream out = Files.newOutputStream(file)) { @@ -132,6 +142,7 @@ private static void write(Path file) { + "scrubbed of your account name. Set to false to opt out. See PRIVACY.md. " + "energyRateMultiplier: scales FE/tick of all generators. oxygenDrainMultiplier: " + "scales how fast air drains. oxygenCapacityMultiplier: scales air capacity. " + + "fuelCostMultiplier: scales fuel burned per rocket launch. " + "All multipliers 0.1..10, default 1."); } } catch (IOException e) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java index a29d73b..e208020 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java @@ -5,6 +5,7 @@ import net.minecraft.resources.ResourceKey; import net.minecraft.world.level.Level; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.registry.ModDimensions; /** @@ -52,9 +53,10 @@ public int fuelCapacity() { return this.fuelCapacity; } - /** Fuel consumed by a single launch, in millibuckets (clamped to the tank so a launch is always possible). */ + /** Fuel consumed by a single launch, in millibuckets (config-scaled, clamped to the tank so a launch is always possible). */ public int fuelPerLaunch() { - return Math.min(this.fuelPerLaunch, this.fuelCapacity); + return Math.min(NerospaceConfig.scale(this.fuelPerLaunch, NerospaceConfig.fuelCostMultiplier()), + this.fuelCapacity); } /** Ordered list of reachable destinations (lowest unlock first, signature destination last). */ From 484ead504244b1c2e81b1149e23289f999d73724 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 22:44:55 +0200 Subject: [PATCH 72/82] Add machineSpeed multiplier and wire to machines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a new machineSpeedMultiplier to NerospaceConfig (KEY_MACHINE_SPEED) with getter and a scaleInterval(baseTicks, speedMultiplier) helper that inverse-scales work intervals (clamped >= 1 tick). Persist loading/saving of the property and include it in the config help text. Wire the multiplier into machines: use scaleInterval for progress thresholds and tick/interval checks in FuelRefinery, NerosiumGrinder, HydrationModule, and Terraformer, and fold the multiplier into the QuarryController mining rate. Update MULTILOADER_PORT_CHECKLIST.md to mark the config seam complete — all five balance multipliers are now active cross-loader for tuning. --- docs/MULTILOADER_PORT_CHECKLIST.md | 17 ++++++++++++---- .../nerospace/config/NerospaceConfig.java | 20 +++++++++++++++++++ .../machine/FuelRefineryBlockEntity.java | 5 +++-- .../machine/HydrationModuleBlockEntity.java | 4 +++- .../machine/NerosiumGrinderBlockEntity.java | 5 +++-- .../machine/TerraformerBlockEntity.java | 5 ++++- .../quarry/QuarryControllerBlockEntity.java | 4 +++- 7 files changed, 49 insertions(+), 11 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 96962c6..112b5dd 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -4,6 +4,11 @@ Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still n cross-loader `multiloader/` project. As of this audit: **~217 classes ported, ~47 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — config seam COMPLETE (all 5 multipliers).** All 4 cells compile green. Slices 2–4 +> added `oxygenDrain`/`oxygenCapacity` (→ OxygenManager), `fuelCost` (→ RocketTier.fuelPerLaunch), and +> `machineSpeed` (+ inverse `scaleInterval` → grinder/refinery/hydration/terraformer/quarry). All five of the +> root's balance multipliers (0.1×..10×) are now live cross-loader through the properties `NerospaceConfig`. + > **2026-06-21 update — config seam slice 1 (energy multiplier).** All 4 cells compile green. Extended the > properties `NerospaceConfig` with `energyRateMultiplier` (0.1×..10×) + a `scale()` clamp helper, wired into > all three generators' FE/tick. Establishes the cross-loader balance-config pattern (properties file, no @@ -517,7 +522,7 @@ checked by a headless build). - [ ] `command/NerospaceCommands` — `/nerospace` debug/admin commands (vanilla Brigadier; loader event differs). - [ ] `compat/jei/*` — recipe-viewer integration. NeoForge = JEI; Fabric would use REI/EMI. Cross-mod, low priority. -### Config / tuning — **slice 1 DONE (4 cells green); cross-loader seam established** +### Config / tuning — **DONE (4 cells green): all 5 multipliers wired, cross-loader seam complete** - [x] **Slice 1 — config seam + energy multiplier.** Extended the properties-based `config/NerospaceConfig` (no NeoForge `ModConfigSpec` — the cross-loader seam is the properties file the telemetry batch added) with `energyRateMultiplier` (clamp 0.1×..10×, default 1) + a `scale(base, mult)` helper (min-1 clamp, mirroring @@ -528,9 +533,13 @@ checked by a headless build). the attachment default self-corrects on the first tick). **3 of the root's 5 multipliers now wired.** - [x] **Slice 3 — fuelCostMultiplier.** Added `fuelCostMultiplier`; wired into `RocketTier.fuelPerLaunch()` (scaled, still clamped to the tank so a launch is always possible). **4 of the root's 5 multipliers wired.** -- [ ] **Slice 4 (last multiplier).** `machineSpeedMultiplier` — inverse-scaled (faster ⇒ shorter work - interval, clamped ≥1 tick); needs a `scaleInterval` helper + wiring into each machine's work period - (grinder, refinery, terraformer, quarry, …). Multiple consumers, so its own slice. +- [x] **Slice 4 — machineSpeedMultiplier (last multiplier; config seam COMPLETE).** Added `machineSpeedMultiplier` + + a `scaleInterval` helper (inverse, clamped ≥1 tick); wired into the grinder + refinery (progress thresholds, + both the completion check and the synced max-progress data), the hydration module + terraformer (modulo work + intervals), and the quarry (folded into the mining rate). **All 5 of the root's balance multipliers are now + live cross-loader** (energyRate, oxygenDrain, oxygenCapacity, fuelCost, machineSpeed) via the properties + `NerospaceConfig` — no NeoForge `ModConfigSpec`. Base values stay inlined per machine (a central `Tuning` + class is optional). ### Spawn rules - [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java index fa7024b..c8b282d 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -26,6 +26,7 @@ public final class NerospaceConfig { private static final String KEY_OXYGEN_DRAIN = "oxygenDrainMultiplier"; private static final String KEY_OXYGEN_CAPACITY = "oxygenCapacityMultiplier"; private static final String KEY_FUEL_COST = "fuelCostMultiplier"; + private static final String KEY_MACHINE_SPEED = "machineSpeedMultiplier"; /** Multiplier range (mirrors the root config spec): 0.1× .. 10×. */ private static final double MULT_MIN = 0.1D; @@ -41,6 +42,8 @@ public final class NerospaceConfig { private static volatile double oxygenCapacityMultiplier = 1.0D; /** Scales the fuel a rocket burns per launch (clamped to the tank). Clamped 0.1×..10×. */ private static volatile double fuelCostMultiplier = 1.0D; + /** Scales machine work speed (inverse: higher ⇒ shorter work intervals). Clamped 0.1×..10×. */ + private static volatile double machineSpeedMultiplier = 1.0D; private static volatile boolean loaded; private NerospaceConfig() { @@ -66,6 +69,19 @@ public static double fuelCostMultiplier() { return fuelCostMultiplier; } + public static double machineSpeedMultiplier() { + return machineSpeedMultiplier; + } + + /** + * Inverse-scales a base work interval by the machine-speed multiplier: a higher speed yields a + * SHORTER interval, clamped to ≥1 tick (so 10× can't produce a zero-tick interval). Mirrors the root + * {@code Tuning} interval-clamp contract. + */ + public static int scaleInterval(int baseTicks, double speedMultiplier) { + return Math.max(1, (int) Math.round(baseTicks / Math.max(0.01D, speedMultiplier))); + } + /** * Applies a balance multiplier to a base integer rate, clamped to a minimum of 1 so an extreme low * multiplier (0.1×) can never zero a rate (mirrors the root {@code Tuning} clamping contract). @@ -105,6 +121,8 @@ public static synchronized void load() { props.getProperty(KEY_OXYGEN_CAPACITY), oxygenCapacityMultiplier)); fuelCostMultiplier = clampMultiplier(parseDouble( props.getProperty(KEY_FUEL_COST), fuelCostMultiplier)); + machineSpeedMultiplier = clampMultiplier(parseDouble( + props.getProperty(KEY_MACHINE_SPEED), machineSpeedMultiplier)); } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not read {}; using defaults.", FILE_NAME, e); } @@ -133,6 +151,7 @@ private static void write(Path file) { props.setProperty(KEY_OXYGEN_DRAIN, Double.toString(oxygenDrainMultiplier)); props.setProperty(KEY_OXYGEN_CAPACITY, Double.toString(oxygenCapacityMultiplier)); props.setProperty(KEY_FUEL_COST, Double.toString(fuelCostMultiplier)); + props.setProperty(KEY_MACHINE_SPEED, Double.toString(machineSpeedMultiplier)); try { Files.createDirectories(file.getParent()); try (OutputStream out = Files.newOutputStream(file)) { @@ -143,6 +162,7 @@ private static void write(Path file) { + "energyRateMultiplier: scales FE/tick of all generators. oxygenDrainMultiplier: " + "scales how fast air drains. oxygenCapacityMultiplier: scales air capacity. " + "fuelCostMultiplier: scales fuel burned per rocket launch. " + + "machineSpeedMultiplier: scales machine work speed (higher = faster). " + "All multipliers 0.1..10, default 1."); } } catch (IOException e) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java index 964989a..2dd08a0 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java @@ -25,6 +25,7 @@ import org.jetbrains.annotations.Nullable; import za.co.neroland.nerospace.energy.EnergyBuffer; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.fluid.FluidTank; import za.co.neroland.nerospace.fluid.ModFluids; @@ -74,7 +75,7 @@ public int get(int index) { case 2 -> (int) tank.getAmount(); case 3 -> (int) tank.getCapacity(); case 4 -> progress; - case 5 -> WORK_TICKS; + case 5 -> NerospaceConfig.scaleInterval(WORK_TICKS, NerospaceConfig.machineSpeedMultiplier()); default -> 0; }; } @@ -148,7 +149,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { this.energy.consume(FE_PER_TICK); this.progress++; - if (this.progress >= WORK_TICKS) { + if (this.progress >= NerospaceConfig.scaleInterval(WORK_TICKS, NerospaceConfig.machineSpeedMultiplier())) { this.progress = 0; this.items.get(CARBON_SLOT).shrink(1); this.items.get(CATALYST_SLOT).shrink(1); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java index ac28733..273189b 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java @@ -21,6 +21,7 @@ import org.jetbrains.annotations.Nullable; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.menu.HydrationModuleMenu; import za.co.neroland.nerospace.registry.ModBlockEntities; import za.co.neroland.nerospace.registry.ModItems; @@ -101,7 +102,8 @@ public static int hydrationUnits(ItemStack stack) { public void tick(Level level, BlockPos pos, BlockState state) { if (!(level instanceof ServerLevel serverLevel) - || serverLevel.getGameTime() % WORK_INTERVAL_TICKS != 0) { + || serverLevel.getGameTime() % NerospaceConfig.scaleInterval( + WORK_INTERVAL_TICKS, NerospaceConfig.machineSpeedMultiplier()) != 0) { return; } meltPulse(serverLevel, pos); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java index 2b2388d..eec98d9 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java @@ -20,6 +20,7 @@ import org.jetbrains.annotations.Nullable; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.menu.NerosiumGrinderMenu; @@ -50,7 +51,7 @@ public class NerosiumGrinderBlockEntity extends BlockEntity implements WorldlyCo public int get(int index) { return switch (index) { case 0 -> progress; - case 1 -> MAX_PROGRESS; + case 1 -> NerospaceConfig.scaleInterval(MAX_PROGRESS, NerospaceConfig.machineSpeedMultiplier()); case 2 -> energy.getRaw(); case 3 -> CAPACITY; default -> 0; @@ -89,7 +90,7 @@ public void tick(Level level, BlockPos pos, BlockState state) { if (canWork) { this.progress++; this.energy.consume(ENERGY_PER_TICK); - if (this.progress >= MAX_PROGRESS) { + if (this.progress >= NerospaceConfig.scaleInterval(MAX_PROGRESS, NerospaceConfig.machineSpeedMultiplier())) { craft(result); this.progress = 0; } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java index 7d71b0f..8c41853 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java @@ -29,6 +29,7 @@ import org.jetbrains.annotations.Nullable; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.menu.TerraformerMenu; @@ -227,7 +228,9 @@ public void tick(Level level, BlockPos pos, BlockState state) { } // Redstone switch: wired machines only sweep while powered. - if (serverLevel.getGameTime() % WORK_INTERVAL_TICKS == 0 && MachineRedstone.allowsRun(level, pos)) { + if (serverLevel.getGameTime() % NerospaceConfig.scaleInterval( + WORK_INTERVAL_TICKS, NerospaceConfig.machineSpeedMultiplier()) == 0 + && MachineRedstone.allowsRun(level, pos)) { work(serverLevel, pos); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java index 6eb63fa..ec62f12 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java @@ -37,6 +37,7 @@ import org.jetbrains.annotations.Nullable; +import za.co.neroland.nerospace.config.NerospaceConfig; import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.fluid.FluidTank; @@ -398,7 +399,8 @@ private void mine(ServerLevel level) { private long miningInterval(ServerLevel level) { double planet = PlanetMiningProfile.forDimension(level.dimension()).speedMultiplier(); - double rate = this.tier.baseBlocksPerCycle() * this.modules.speedMultiplier() * planet; + double rate = this.tier.baseBlocksPerCycle() * this.modules.speedMultiplier() * planet + * NerospaceConfig.machineSpeedMultiplier(); return Math.max(1L, Math.round(MINE_INTERVAL / Math.max(0.01, rate))); } From ac8379981382b3e97c60f60e2d82ac1c9b4353ed Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:01:56 +0200 Subject: [PATCH 73/82] Add /nerospace gallery command and register it Introduce a cross-loader creative debug command and wire it into both Fabric and NeoForge. - Add multiloader/common NerospaceCommands: implements `/nerospace gallery` (builds a large creative-only showcase of blocks, machines, rockets, suits, creatures, meteor site, quarries, solar arrays, etc.) and `/nerospace gallery clear` (wipes the footprint and spawned entities). The command is player-only and requires creative instabuild. Iterates BuiltInRegistries.BLOCK filtered to the mod namespace, uses ArmorStand constructor for spawning, and pre-configures block entities and pipes. - Register the command in Fabric via CommandRegistrationCallback and in NeoForge via RegisterCommandsEvent. - Update docs/MULTILOADER_PORT_CHECKLIST.md to mark command/NerospaceCommands as ported and note cross-loader adaptations (skipped a few unported cosmetic bits). This ports the gallery admin/debug command into the multiloader common code so each loader can register it with its own command hook. --- docs/MULTILOADER_PORT_CHECKLIST.md | 16 +- .../nerospace/command/NerospaceCommands.java | 608 ++++++++++++++++++ .../nerospace/fabric/NerospaceFabric.java | 5 + .../nerospace/neoforge/NerospaceNeoForge.java | 5 + 4 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 112b5dd..f3b5d67 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -1,9 +1,15 @@ # Nerospace multiloader — port checklist Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still needs ported into the -cross-loader `multiloader/` project. As of this audit: **~217 classes ported, ~47 remaining**, all four +cross-loader `multiloader/` project. As of this audit: **~218 classes ported, ~46 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-21 update — /nerospace commands ported.** All 4 cells compile green. `command/NerospaceCommands` +> (the `/nerospace gallery` creative showcase) behind a cross-loader `register(CommandDispatcher)` seam +> (NeoForge `RegisterCommandsEvent` / Fabric `CommandRegistrationCallback`). Adapted: block iteration via +> `BuiltInRegistries.BLOCK` namespace filter (no `RegistrationProvider` iteration), single `SOLAR_PANEL`, +> `ArmorStand` constructor (no `EntityType.ARMOR_STAND` on 26.2), dropped unported `quarry.stageDisplay`. + > **2026-06-21 update — config seam COMPLETE (all 5 multipliers).** All 4 cells compile green. Slices 2–4 > added `oxygenDrain`/`oxygenCapacity` (→ OxygenManager), `fuelCost` (→ RocketTier.fuelPerLaunch), and > `machineSpeed` (+ inverse `scaleInterval` → grinder/refinery/hydration/terraformer/quarry). All five of the @@ -519,7 +525,13 @@ checked by a headless build). serverbound(...)`). Client-safety contract documented in `ModNetwork`. ### Commands & compat -- [ ] `command/NerospaceCommands` — `/nerospace` debug/admin commands (vanilla Brigadier; loader event differs). +- [x] `command/NerospaceCommands` — **DONE (4 cells green).** `/nerospace gallery` [clear] creative showcase + builder, behind a cross-loader `register(CommandDispatcher)` seam (NeoForge `RegisterCommandsEvent`, Fabric + `CommandRegistrationCallback`). Cross-loader/version adaptations: iterate `BuiltInRegistries.BLOCK` filtered + to the mod namespace (the `RegistrationProvider` has no entry iteration); single `SOLAR_PANEL` (tiers + unported); spawn the armor stands via the `ArmorStand` constructor (the de-obf `EntityType.ARMOR_STAND` + constant isn't on the 26.2 classpath); dropped the unported `quarry.stageDisplay` preview + the Creative + Fluid Tank `setSource` (fixed rocket_fuel here). - [ ] `compat/jei/*` — recipe-viewer integration. NeoForge = JEI; Fabric would use REI/EMI. Cross-mod, low priority. ### Config / tuning — **DONE (4 cells green): all 5 multipliers wired, cross-loader seam complete** diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java new file mode 100644 index 0000000..91d49b4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/command/NerospaceCommands.java @@ -0,0 +1,608 @@ +package za.co.neroland.nerospace.command; + +import java.util.ArrayList; +import java.util.List; + +import com.mojang.brigadier.Command; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.Identifier; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.decoration.ArmorStand; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LeverBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.AttachFace; +import net.minecraft.world.phys.AABB; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.meteor.FallingMeteorEntity; +import za.co.neroland.nerospace.meteor.MeteorCoreBlockEntity; +import za.co.neroland.nerospace.machine.CombustionGeneratorBlockEntity; +import za.co.neroland.nerospace.machine.FuelRefineryBlockEntity; +import za.co.neroland.nerospace.machine.HydrationModuleBlockEntity; +import za.co.neroland.nerospace.machine.NerosiumGrinderBlockEntity; +import za.co.neroland.nerospace.machine.quarry.QuarryControllerBlockEntity; +import za.co.neroland.nerospace.machine.quarry.QuarryRegion; +import za.co.neroland.nerospace.pipe.PipeIoMode; +import za.co.neroland.nerospace.pipe.PipeResourceType; +import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.rocket.RocketEntity; +import za.co.neroland.nerospace.rocket.RocketLaunchPadBlock; +import za.co.neroland.nerospace.rocket.RocketTier; +import za.co.neroland.nerospace.storage.CreativeItemStoreBlockEntity; + +/** + * Creative-only debug commands (cheats / op level 2). {@code /nerospace gallery} builds a showcase + * platform near the player: every Nerospace block floating two blocks above the floor (so all faces + * are visible) on a ~3-block grid, every machine RUNNING the way it is meant to be wired in + * survival (fuelled, powered and fed — except the Terraformer and Oxygen Generator, which sit + * behind an off lever so the world-changing machines only run when deliberately switched on), all + * four rocket tiers standing on their required pad formations, every suit variant on a stand, and + * each creature spawned twice — once with AI and once frozen (NoAI) — for inspection. + * {@code /nerospace gallery clear} wipes that footprint (blocks + spawned entities) so a rebuild — + * or the screenshot harness — doesn't stack duplicates. + */ +public final class NerospaceCommands { + + private static final int SPACING = 3; // blocks between display cells + private static final int FLOAT_ABOVE = 3; // display sits this many blocks above the floor (2 air gap) + private static final int SUIT_SPACING = 3; // blocks between suit stands (roomier than the old 2) + private static final float SUIT_YAW = -10.0f; // every suit stand faces one way, angled a few degrees left + + private NerospaceCommands() { + } + + /** + * Cross-loader registration: each loader calls this from its command hook (NeoForge + * {@code RegisterCommandsEvent}, Fabric {@code CommandRegistrationCallback}) with the dispatcher. + */ + public static void register(com.mojang.brigadier.CommandDispatcher dispatcher) { + // Player-only; the executor further restricts to creative. (Commands themselves require the + // world to have cheats/commands enabled, so this is effectively creative + commands gated.) + dispatcher.register( + Commands.literal("nerospace") + .requires(src -> src.getPlayer() != null) + .then(Commands.literal("gallery") + .executes(ctx -> buildGallery(ctx.getSource())) + .then(Commands.literal("clear") + .executes(ctx -> clearGallery(ctx.getSource()))))); + } + + private static int buildGallery(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Run this as a player.")); + return 0; + } + if (!player.getAbilities().instabuild) { + source.sendFailure(Component.literal("The Nerospace gallery is creative-only.")); + return 0; + } + ServerLevel level = player.level(); + BlockPos origin = player.blockPosition(); + + // The cross-loader RegistrationProvider has no entry iteration, so walk the vanilla block + // registry filtered to this mod's namespace (same effect as the root's BLOCKS.getEntries()). + List blocks = new ArrayList<>(); + for (Block block : BuiltInRegistries.BLOCK) { + Identifier bid = BuiltInRegistries.BLOCK.getKey(block); + if (!NerospaceCommon.MOD_ID.equals(bid.getNamespace())) { + continue; + } + if (block != ModBlocks.ROCKET_FUEL_BLOCK.get()) { // skip the fluid block (renders oddly free-standing) + blocks.add(block); + } + } + + int cols = (int) Math.ceil(Math.sqrt(Math.max(1, blocks.size()))); + int rows = (int) Math.ceil(blocks.size() / (double) cols); + // ROTUNDA: each cluster sits on a ring ~48 blocks out on its own compass bearing, so a camera + // near the centre shoots each one outward against empty ground with no other display in frame. + // The /nsgallery capture harness mirrors these bearings. Bases are placed so the body centres + // on the ring. Tune distances together with the harness if reframing. + int ox = origin.getX() + 38; // block grid → EAST + int oz = origin.getZ() - 9; + int fy = origin.getY(); + + BlockState floor = ModBlocks.STATION_FLOOR.get().defaultBlockState(); + + // Floor slab under the whole grid (with a 1-block margin). + for (int gx = -1; gx <= cols * SPACING; gx++) { + for (int gz = -1; gz <= rows * SPACING; gz++) { + level.setBlockAndUpdate(new BlockPos(ox + gx, fy, oz + gz), floor); + } + } + // Floating block displays (2 air blocks below each → visible from all angles). + for (int i = 0; i < blocks.size(); i++) { + int col = i % cols; + int row = i / cols; + level.setBlockAndUpdate( + new BlockPos(ox + col * SPACING, fy + FLOAT_ABOVE, oz + row * SPACING), + blocks.get(i).defaultBlockState()); + } + + // MACHINES, ALL RUNNING (one strip, four wired clusters — each exactly the survival hookup): + // A. Combustion Generator (coal) → pipe → Grinder (raw nerosium), Passive Generator feeding in. + // B. Creative Battery → pipe → Fuel Refinery (coal + blaze powder) → pipe → Fuel Tank. + // C. Creative Battery → pipe → Oxygen Generator — parked behind an OFF lever. + // D. Creative Battery → pipe → Terraformer + touching Hydration Module (glacite) and + // Terraform Monitor — parked behind an OFF lever (it WILL reshape the area when on). + int sx = origin.getX() - 13; // machine strip → SOUTH + int sz = origin.getZ() + 48; + for (int dx = -1; dx <= 27; dx++) { + for (int dz = -2; dz <= 2; dz++) { + level.setBlockAndUpdate(new BlockPos(sx + dx, fy, sz + dz), floor); + } + } + BlockState lever = Blocks.LEVER.defaultBlockState() + .setValue(LeverBlock.FACE, AttachFace.FLOOR) + .setValue(LeverBlock.FACING, Direction.EAST); + + // A: the classic first power line. + level.setBlockAndUpdate(new BlockPos(sx, fy + 1, sz), ModBlocks.COMBUSTION_GENERATOR.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(sx + 1, fy + 1, sz), ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(sx + 2, fy + 1, sz), ModBlocks.NEROSIUM_GRINDER.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(sx + 1, fy + 1, sz + 1), ModBlocks.PASSIVE_GENERATOR.get().defaultBlockState()); + if (level.getBlockEntity(new BlockPos(sx, fy + 1, sz)) instanceof CombustionGeneratorBlockEntity gen) { + gen.setItem(CombustionGeneratorBlockEntity.FUEL_SLOT, new ItemStack(Items.COAL, 64)); + } + if (level.getBlockEntity(new BlockPos(sx + 2, fy + 1, sz)) instanceof NerosiumGrinderBlockEntity grinder) { + grinder.setItem(NerosiumGrinderBlockEntity.INPUT_SLOT, new ItemStack(ModItems.RAW_NEROSIUM.get(), 64)); + } + + // B: refining line — power in from the endless battery, fuel out into a Fuel Tank. + int bx = sx + 6; + level.setBlockAndUpdate(new BlockPos(bx, fy + 1, sz), ModBlocks.CREATIVE_BATTERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(bx + 1, fy + 1, sz), ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(bx + 2, fy + 1, sz), ModBlocks.FUEL_REFINERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(bx + 3, fy + 1, sz), ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(bx + 4, fy + 1, sz), ModBlocks.FUEL_TANK.get().defaultBlockState()); + setAllModes(level, new BlockPos(bx + 1, fy + 1, sz), Direction.WEST, PipeIoMode.IN); + setAllModes(level, new BlockPos(bx + 1, fy + 1, sz), Direction.EAST, PipeIoMode.OUT); + setAllModes(level, new BlockPos(bx + 3, fy + 1, sz), Direction.WEST, PipeIoMode.IN); + setAllModes(level, new BlockPos(bx + 3, fy + 1, sz), Direction.EAST, PipeIoMode.OUT); + if (level.getBlockEntity(new BlockPos(bx + 2, fy + 1, sz)) instanceof FuelRefineryBlockEntity refinery) { + refinery.setItem(FuelRefineryBlockEntity.CARBON_SLOT, new ItemStack(Items.COAL, 64)); + refinery.setItem(FuelRefineryBlockEntity.CATALYST_SLOT, new ItemStack(Items.BLAZE_POWDER, 64)); + } + + // C: oxygen generator behind its lever (off until flipped — then the bubble forms). + int cx = sx + 13; + level.setBlockAndUpdate(new BlockPos(cx, fy + 1, sz), ModBlocks.CREATIVE_BATTERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(cx + 1, fy + 1, sz), ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(cx + 2, fy + 1, sz), ModBlocks.OXYGEN_GENERATOR.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(cx + 3, fy + 1, sz), lever); + setAllModes(level, new BlockPos(cx + 1, fy + 1, sz), Direction.WEST, PipeIoMode.IN); + setAllModes(level, new BlockPos(cx + 1, fy + 1, sz), Direction.EAST, PipeIoMode.OUT); + + // D: terraformer cluster behind its lever, with the full deeper-terraform support crew. + int tx = sx + 19; + level.setBlockAndUpdate(new BlockPos(tx, fy + 1, sz), ModBlocks.CREATIVE_BATTERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(tx + 1, fy + 1, sz), ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(tx + 2, fy + 1, sz), ModBlocks.TERRAFORMER.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(tx + 2, fy + 1, sz + 1), ModBlocks.HYDRATION_MODULE.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(tx + 2, fy + 1, sz - 1), ModBlocks.TERRAFORM_MONITOR.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(tx + 3, fy + 1, sz), lever); + setAllModes(level, new BlockPos(tx + 1, fy + 1, sz), Direction.WEST, PipeIoMode.IN); + setAllModes(level, new BlockPos(tx + 1, fy + 1, sz), Direction.EAST, PipeIoMode.OUT); + if (level.getBlockEntity(new BlockPos(tx + 2, fy + 1, sz + 1)) instanceof HydrationModuleBlockEntity module) { + module.setItem(HydrationModuleBlockEntity.INPUT_SLOT, new ItemStack(ModItems.GLACITE.get(), 64)); + } + + // FOUR LIVE PIPE SCENARIOS: creative source → 3 pipes → sink, one row per resource layer. + // The source-touching face is set IN (pull-only — otherwise the pipe would void its buffer + // back into the endless source) and the sink-touching face OUT, mirroring real Configurator use. + int px = origin.getX() - 50; // pipe scenarios → WEST (rows run north-south → broadside from the centre) + int pz = origin.getZ() + 5; + Block[][] scenarioRows = { + {ModBlocks.CREATIVE_BATTERY.get(), ModBlocks.BATTERY.get()}, + {ModBlocks.CREATIVE_FLUID_TANK.get(), ModBlocks.FLUID_TANK.get()}, + {ModBlocks.CREATIVE_GAS_TANK.get(), ModBlocks.GAS_TANK.get()}, + {ModBlocks.CREATIVE_ITEM_STORE.get(), ModBlocks.ITEM_STORE.get()}, + }; + for (int row = 0; row < scenarioRows.length; row++) { + int rz = pz - row * 3; + for (int dx = -1; dx <= 5; dx++) { + for (int dz = -1; dz <= 1; dz++) { + level.setBlockAndUpdate(new BlockPos(px + dx, fy, rz + dz), floor); + } + } + level.setBlockAndUpdate(new BlockPos(px, fy + 1, rz), scenarioRows[row][0].defaultBlockState()); + for (int dx = 1; dx <= 3; dx++) { + level.setBlockAndUpdate(new BlockPos(px + dx, fy + 1, rz), + ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + } + level.setBlockAndUpdate(new BlockPos(px + 4, fy + 1, rz), scenarioRows[row][1].defaultBlockState()); + setAllModes(level, new BlockPos(px + 1, fy + 1, rz), Direction.WEST, PipeIoMode.IN); + setAllModes(level, new BlockPos(px + 3, fy + 1, rz), Direction.EAST, PipeIoMode.OUT); + } + // Pre-configure the endless sources so the rows run on arrival. + // (The multiloader Creative Fluid Tank is a fixed endless rocket_fuel source — it has no setSource.) + if (level.getBlockEntity(new BlockPos(px, fy + 1, pz - 9)) instanceof CreativeItemStoreBlockEntity store) { + store.setSource(new ItemStack(ModItems.NEROSIUM_INGOT.get())); + } + + // Suit displays (every variant) + a LOADED Star Guide pedestal (book installed → hologram runs). + int ax = origin.getX() - 40; // suits + Star Guide → NORTH-WEST (well clear of the pipes spoke) + int az = origin.getZ() - 34; + int suit0 = 0; + int suit1 = SUIT_SPACING; + int suit2 = SUIT_SPACING * 2; + int suit3 = SUIT_SPACING * 3; + int guideX = SUIT_SPACING * 4; + for (int dx = -1; dx <= guideX + 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + level.setBlockAndUpdate(new BlockPos(ax + dx, fy, az + dz), floor); + } + } + spawnSuitStand(level, new BlockPos(ax + suit0, fy + 1, az), Component.literal("Oxygen Suit"), SUIT_YAW, + ModItems.OXYGEN_SUIT_HELMET.get(), ModItems.OXYGEN_SUIT_CHESTPLATE.get(), + ModItems.OXYGEN_SUIT_LEGGINGS.get(), ModItems.OXYGEN_SUIT_BOOTS.get()); + spawnSuitStand(level, new BlockPos(ax + suit1, fy + 1, az), Component.literal("Tier 2 Oxygen Suit"), SUIT_YAW, + ModItems.OXYGEN_SUIT_T2_HELMET.get(), ModItems.OXYGEN_SUIT_T2_CHESTPLATE.get(), + ModItems.OXYGEN_SUIT_T2_LEGGINGS.get(), ModItems.OXYGEN_SUIT_T2_BOOTS.get()); + spawnSuitStand(level, new BlockPos(ax + suit2, fy + 1, az), Component.literal("Thermal Suit"), SUIT_YAW, + ModItems.OXYGEN_SUIT_HEAT_HELMET.get(), ModItems.OXYGEN_SUIT_HEAT_CHESTPLATE.get(), + ModItems.OXYGEN_SUIT_HEAT_LEGGINGS.get(), ModItems.OXYGEN_SUIT_HEAT_BOOTS.get()); + spawnSuitStand(level, new BlockPos(ax + suit3, fy + 1, az), Component.literal("Cryo Suit"), SUIT_YAW, + ModItems.OXYGEN_SUIT_COLD_HELMET.get(), ModItems.OXYGEN_SUIT_COLD_CHESTPLATE.get(), + ModItems.OXYGEN_SUIT_COLD_LEGGINGS.get(), ModItems.OXYGEN_SUIT_COLD_BOOTS.get()); + BlockPos guidePos = new BlockPos(ax + guideX, fy + 1, az); + level.setBlockAndUpdate(guidePos, ModBlocks.STAR_GUIDE.get().defaultBlockState()); + if (level.getBlockEntity(guidePos) + instanceof za.co.neroland.nerospace.progression.StarGuideBlockEntity guide) { + guide.installBook(new ItemStack(ModItems.STAR_GUIDE_BOOK.get())); + } + + // ROCKET ROW: every tier on the pad formation it actually requires (RocketItem gating): + // T1 + T2: a full 3x3 pad. T3: a 3x3 pad ringed with Station Wall. + // T4: the Heavy Launch Complex — full 5x5 pad + a Launch Gantry on its border ring. + int rx = origin.getX() - 14; // rocket row → NORTH (the hero spoke) + int rz0 = origin.getZ() - 49; + for (int dx = -2; dx <= 31; dx++) { + for (int dz = -3; dz <= 5; dz++) { + level.setBlockAndUpdate(new BlockPos(rx + dx, fy, rz0 + dz), floor); + } + } + BlockState pad = ModBlocks.ROCKET_LAUNCH_PAD.get().defaultBlockState(); + // T1 (3x3 + the classic pad-side Fuel Tank). + fillPad(level, new BlockPos(rx, fy + 1, rz0), 3, pad); + level.setBlockAndUpdate(new BlockPos(rx + 3, fy + 1, rz0 + 1), ModBlocks.FUEL_TANK.get().defaultBlockState()); + spawnRocket(level, rx + 1, fy + 1, rz0 + 1, RocketTier.TIER_1); + // T2 (3x3). + fillPad(level, new BlockPos(rx + 8, fy + 1, rz0), 3, pad); + spawnRocket(level, rx + 9, fy + 1, rz0 + 1, RocketTier.TIER_2); + // T3 (3x3 ringed with Station Wall). + fillPad(level, new BlockPos(rx + 16, fy + 1, rz0), 3, pad); + BlockState wall = ModBlocks.STATION_WALL.get().defaultBlockState(); + for (int dx = -1; dx <= 3; dx++) { + for (int dz = -1; dz <= 3; dz++) { + if (dx == -1 || dx == 3 || dz == -1 || dz == 3) { + level.setBlockAndUpdate(new BlockPos(rx + 16 + dx, fy + 1, rz0 + dz), wall); + } + } + } + spawnRocket(level, rx + 17, fy + 1, rz0 + 1, RocketTier.TIER_3); + // T4 (Heavy Launch Complex: 5x5 + gantry + fuel tank). + fillPad(level, new BlockPos(rx + 24, fy + 1, rz0 - 1), 5, pad); + level.setBlockAndUpdate(new BlockPos(rx + 23, fy + 1, rz0 + 1), + ModBlocks.LAUNCH_GANTRY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(rx + 29, fy + 1, rz0 + 1), + ModBlocks.FUEL_TANK.get().defaultBlockState()); + spawnRocket(level, rx + 26, fy + 1, rz0 + 1, RocketTier.TIER_4); + + // Creatures: each spawned twice — live (AI) and frozen (NoAI) — on a small floor strip. + int mx = origin.getX() + 18; // creatures → SOUTH-EAST + int mz = origin.getZ() + 33; + for (int dx = -1; dx <= 8 * 4; dx++) { + for (int dz = -1; dz <= 3; dz++) { + level.setBlockAndUpdate(new BlockPos(mx + dx, fy, mz + dz), floor); + } + } + List> creatures = List.of( + ModEntities.XERTZ_STALKER.get(), ModEntities.QUARTZ_CRAWLER.get(), + ModEntities.GREENLING.get(), ModEntities.CINDER_STALKER.get(), + ModEntities.FROST_STRIDER.get(), + // Terraform livestock (DEEPER_TERRAFORM_DESIGN.md §5). + ModEntities.MEADOW_LOPER.get(), ModEntities.EMBER_STRUTTER.get(), + ModEntities.WOOLLY_DRIFT.get()); + // One frozen (NoAI) row only — AI mobs wander, which breaks reproducible screenshots. + for (int i = 0; i < creatures.size(); i++) { + spawnShowcase(level, creatures.get(i), new BlockPos(mx + i * 4, fy + 1, mz + 1), true); + } + + // METEOR SITE (meteor-events-design.md): a small crater of meteor_rock around a loot-bearing + // meteor_core, with a frozen meteor hovering above it (spins + trails for the shot). SW spoke. + buildMeteorSite(level, floor, origin.getX() - 28, origin.getZ() + 30, fy); + + // QUARRY (MINER_DESIGN): two NE displays. + // 1. Landmark-only — three landmarks in an L (shows the projected marker lasers). + // 2. Fully operating — a powered quarry mid-dig: frame ring, drill head, a real pit forming. + BlockState landmark = ModBlocks.QUARRY_LANDMARK.get().defaultBlockState(); + int lx = origin.getX() + 28; // landmark-only display (NE, nearer the centre) + int lz = origin.getZ() - 40; + for (int dx = -1; dx <= 7; dx++) { + for (int dz = -1; dz <= 7; dz++) { + level.setBlockAndUpdate(new BlockPos(lx + dx, fy, lz + dz), floor); + } + } + level.setBlockAndUpdate(new BlockPos(lx, fy + 1, lz), landmark); + level.setBlockAndUpdate(new BlockPos(lx + 6, fy + 1, lz), landmark); + level.setBlockAndUpdate(new BlockPos(lx, fy + 1, lz + 6), landmark); + + // Operating quarries: staged straight into a deep mid-dig so the frame, gantry, drill head and + // interior-only excavation all read at a glance. Two sizes — a standard 9x9 and a big 17x17 to + // stress-test rendering + mining over a large area. + buildGalleryQuarry(level, floor, origin.getX() + 42, origin.getZ() - 40, fy, 8, 8); + buildGalleryQuarry(level, floor, origin.getX() + 64, origin.getZ() - 56, fy, 16, 12); + + // SOLAR ARRAYS (SOLAR_PANEL_DESIGN, SW bearing): one unit per tier, then a multi-unit seam-joined + // field per tier (so the per-cell trackers reading as one surface is visible), plus a + // battery → universal cable → panel hookup that lights the panel's power connector. + buildSolarArrays(level, floor, origin.getX() - 50, origin.getZ() + 36, fy); + + source.sendSuccess(() -> Component.literal("Built the Nerospace gallery: " + + blocks.size() + " blocks, 4 RUNNING machine clusters (grinder line, fuel refinery " + + "line, oxygen generator + lever, terraformer crew + lever — flip a lever to start " + + "those two), 4 live pipe scenarios (energy/fluid/gas/items), all 4 suit variants, " + + "a loaded Star Guide pedestal, all 4 rocket tiers on their required pads (3x3, " + + "3x3, walled ring, Heavy Launch Complex), 8 creatures (frozen for clean shots), " + + "a meteor crash site (crater + loot core + hovering meteor), and the solar arrays " + + "(T1/T2/T3 single units + a seam-joined field per tier + a cabled hookup showing " + + "the power connector)."), false); + return Command.SINGLE_SUCCESS; + } + + /** + * Wipe the gallery built at the player's feet so a rebuild (or the screenshot harness) doesn't + * stack duplicates. Clears the whole footprint to air from the floor layer ({@code origin.y}) up, + * leaving the natural ground at {@code origin.y - 1} intact, and removes every non-player entity + * in the box (rockets, suit stands, creatures). Run it standing where you ran {@code gallery}. + */ + private static int clearGallery(CommandSourceStack source) { + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(Component.literal("Run this as a player.")); + return 0; + } + if (!player.getAbilities().instabuild) { + source.sendFailure(Component.literal("The Nerospace gallery is creative-only.")); + return 0; + } + ServerLevel level = player.level(); + BlockPos origin = player.blockPosition(); + int ox = origin.getX(); + int oy = origin.getY(); + int oz = origin.getZ(); + + // Footprint of the ROTUNDA buildGallery() (clusters sit ~48 out on N/S/E/W/SE/NW bearings) + // plus margin, so the clear covers every cluster — else reruns stack creatures/rockets/stands. + // The floor sits at oy, so clearing oy..topY to air restores the original flat ground at oy-1. + int minX = ox - 56; + int maxX = ox + 62; + int minZ = oz - 58; + int maxZ = oz + 56; + int topY = oy + 16; + + BlockState air = Blocks.AIR.defaultBlockState(); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + int cleared = 0; + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = oy; y <= topY; y++) { + cursor.set(x, y, z); + if (!level.getBlockState(cursor).isAir()) { + level.setBlock(cursor, air, 2); // flag 2 = notify clients, skip neighbour cascade + cleared++; + } + } + } + } + + // Remove the spawned entities (rockets, armour stands, creatures) — everything but players. + AABB box = new AABB(minX, oy - 1, minZ, maxX + 1, topY + 4, maxZ + 1); + int removed = 0; + for (Entity entity : level.getEntitiesOfClass(Entity.class, box, e -> !(e instanceof Player))) { + entity.discard(); + removed++; + } + + int clearedBlocks = cleared; + int removedEntities = removed; + source.sendSuccess(() -> Component.literal("Cleared the Nerospace gallery: " + clearedBlocks + + " blocks → air, " + removedEntities + " entities removed."), false); + return Command.SINGLE_SUCCESS; + } + + /** + * Solar showcase (SW). Front row: one of each tier as a single unit — a 1×1 T1, a 2×2 T2 (one big + * panel) and a 3×3 T3 (one big panel). Behind it: several units of each tier side by side — nine T1 + * panels (a seam-joined 3×3 field), four T2 units and two T3 units — so multiple arrays tiling is + * visible. A Creative Battery → Universal Pipe → T1 panel line shows the dynamic power connector (the + * panel grows a stub toward the cable so the hookup butts up with no gap). Built at {@code (baseX, + * baseZ)}, extending east (+X) and south (+Z); panels sit on the floor with the tracking deck above. + */ + private static void buildSolarArrays(ServerLevel level, BlockState floor, int baseX, int baseZ, int fy) { + int sy = fy + 1; + for (int dx = -2; dx <= 20; dx++) { + for (int dz = -2; dz <= 10; dz++) { + level.setBlockAndUpdate(new BlockPos(baseX + dx, fy, baseZ + dz), floor); + } + } + + // Front row: one of each tier (multiblock anchors auto-fill their N×N footprint via onPlace). + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX, sy, baseZ); + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 2, sy, baseZ); // fills +2..3 + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 5, sy, baseZ); // fills +5..7 + + // Cable hookup: Creative Battery → Universal Pipe → T1 panel (lights the panel's west connector). + level.setBlockAndUpdate(new BlockPos(baseX + 10, sy, baseZ), + ModBlocks.CREATIVE_BATTERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(baseX + 11, sy, baseZ), + ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 12, sy, baseZ); + + // Multi-unit seam-joined fields, set back (+Z) so footprints don't touch the front row. + // T1: a 3x3 field of nine single panels → one continuous tracking surface. + for (int dx = 0; dx <= 2; dx++) { + for (int dz = 4; dz <= 6; dz++) { + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + dx, sy, baseZ + dz); + } + } + // T2: four 2x2 units → a 4x4 field. + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 5, sy, baseZ + 4); + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 7, sy, baseZ + 4); + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 5, sy, baseZ + 6); + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 7, sy, baseZ + 6); + // T3: two 3x3 units → a 6x3 field. + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 11, sy, baseZ + 4); // fills +11..13 + placeSolar(level, ModBlocks.SOLAR_PANEL.get(), baseX + 14, sy, baseZ + 4); // fills +14..16 + } + + /** Place a solar panel anchor; multiblock tiers auto-expand their footprint in {@code onPlace}. */ + private static void placeSolar(ServerLevel level, Block block, int x, int y, int z) { + level.setBlockAndUpdate(new BlockPos(x, y, z), block.defaultBlockState()); + } + + /** + * Build one staged, fully-powered gallery quarry: a {@code (side+1) x (side+1)} region with its + * frame ring, a west-side creative battery + pipe feed, an interior-only pre-carved pit + * {@code pitDepth} deep (the columns under the frame stay, matching real mining), dropped straight + * into MINING so the gantry + drill animate immediately. + */ + private static void buildGalleryQuarry(ServerLevel level, BlockState floor, int qx, int qz, int fy, + int side, int pitDepth) { + int refY = fy + 1; + int mid = side / 2; + for (int dx = -5; dx <= side; dx++) { // ground: power pad (west) + under the region + for (int dz = -1; dz <= side; dz++) { + level.setBlockAndUpdate(new BlockPos(qx + dx, fy, qz + dz), floor); + } + } + QuarryRegion region = new QuarryRegion(qx, qz, qx + side, qz + side, refY); + BlockState frameBlock = ModBlocks.QUARRY_FRAME.get().defaultBlockState(); + for (BlockPos fp : region.framePositions()) { + level.setBlockAndUpdate(fp, frameBlock); + } + // Pre-carve a starter pit — INTERIOR only, leaving the columns under the frame intact. + for (int x = qx + 1; x <= qx + side - 1; x++) { + for (int z = qz + 1; z <= qz + side - 1; z++) { + for (int y = refY - 1; y >= refY - pitDepth; y--) { + level.setBlockAndUpdate(new BlockPos(x, y, z), Blocks.AIR.defaultBlockState()); + } + } + } + BlockPos quarryPos = new BlockPos(qx - 2, refY, qz + mid); + level.setBlockAndUpdate(new BlockPos(qx - 4, refY, qz + mid), + ModBlocks.CREATIVE_BATTERY.get().defaultBlockState()); + level.setBlockAndUpdate(new BlockPos(qx - 3, refY, qz + mid), + ModBlocks.UNIVERSAL_PIPE.get().defaultBlockState()); + level.setBlockAndUpdate(quarryPos, ModBlocks.QUARRY_CONTROLLER.get().defaultBlockState()); + setAllModes(level, new BlockPos(qx - 3, refY, qz + mid), Direction.WEST, PipeIoMode.IN); + setAllModes(level, new BlockPos(qx - 3, refY, qz + mid), Direction.EAST, PipeIoMode.OUT); + if (level.getBlockEntity(quarryPos) instanceof QuarryControllerBlockEntity quarry) { + quarry.setItem(QuarryControllerBlockEntity.FRAME_SLOT, + new ItemStack(ModItems.FRAME_CASING.get(), 64)); + // (The root's quarry.stageDisplay preview-region call isn't in the ported BE — omitted; cosmetic.) + } + } + + /** + * A showcase meteor crash site: a 7x7 floor pad, a 5x5 {@code meteor_rock} crater floor with a + * raised rim, a loot-pre-rolled {@code meteor_core} nestled in the centre, and a frozen + * {@link FallingMeteorEntity} hovering above (spins + trails, but never falls — gallery only). + */ + private static void buildMeteorSite(ServerLevel level, BlockState floor, int cx, int cz, int fy) { + for (int dx = -3; dx <= 3; dx++) { + for (int dz = -3; dz <= 3; dz++) { + level.setBlockAndUpdate(new BlockPos(cx + dx, fy, cz + dz), floor); + } + } + BlockState rock = ModBlocks.METEOR_ROCK.get().defaultBlockState(); + for (int dx = -2; dx <= 2; dx++) { + for (int dz = -2; dz <= 2; dz++) { + level.setBlockAndUpdate(new BlockPos(cx + dx, fy + 1, cz + dz), rock); // crater floor + if (Math.abs(dx) == 2 || Math.abs(dz) == 2) { + level.setBlockAndUpdate(new BlockPos(cx + dx, fy + 2, cz + dz), rock); // raised rim + } + } + } + BlockPos corePos = new BlockPos(cx, fy + 2, cz); + level.setBlockAndUpdate(corePos, ModBlocks.METEOR_CORE.get().defaultBlockState()); + if (level.getBlockEntity(corePos) instanceof MeteorCoreBlockEntity core) { + core.generateLoot(level.getRandom().nextLong()); + } + FallingMeteorEntity.spawnFrozen(level, cx + 0.5D, fy + 11, cz + 0.5D); + } + + /** A full {@code size x size} square of launch pads with min-corner {@code corner}. */ + private static void fillPad(ServerLevel level, BlockPos corner, int size, BlockState pad) { + for (int dx = 0; dx < size; dx++) { + for (int dz = 0; dz < size; dz++) { + level.setBlockAndUpdate(corner.offset(dx, 0, dz), pad); + } + } + } + + /** A rocket standing on the pad surface of the pad block at {@code (x, y, z)}. */ + private static void spawnRocket(ServerLevel level, int x, int y, int z, RocketTier tier) { + level.addFreshEntity(new RocketEntity(level, + x + 0.5D, y + RocketLaunchPadBlock.SURFACE_HEIGHT, z + 0.5D, tier)); + } + + /** An invulnerable, named armor stand wearing the given four-piece suit. */ + private static void spawnSuitStand(ServerLevel level, BlockPos pos, Component name, float yaw, + Item helmet, Item chestplate, Item leggings, Item boots) { + // Build the stand via its constructor (the de-obf EntityType.ARMOR_STAND constant isn't on the + // 26.2 classpath) and add it to the world directly. + ArmorStand stand = new ArmorStand(level, pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5); + stand.setItemSlot(EquipmentSlot.HEAD, new ItemStack(helmet)); + stand.setItemSlot(EquipmentSlot.CHEST, new ItemStack(chestplate)); + stand.setItemSlot(EquipmentSlot.LEGS, new ItemStack(leggings)); + stand.setItemSlot(EquipmentSlot.FEET, new ItemStack(boots)); + stand.setCustomName(name); + stand.setCustomNameVisible(true); + stand.setInvulnerable(true); + stand.setYRot(yaw); // uniform facing so the row reads as a clean line, angled a few degrees off straight-on + stand.setYBodyRot(yaw); + stand.setYHeadRot(yaw); + level.addFreshEntity(stand); + } + + /** Set one face of the pipe at {@code pos} to {@code mode} for ALL four resource layers. */ + private static void setAllModes(ServerLevel level, BlockPos pos, Direction face, PipeIoMode mode) { + if (level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity pipe) { + for (PipeResourceType type : PipeResourceType.VALUES) { + pipe.setMode(face, type, mode); + } + } + } + + private static void spawnShowcase(ServerLevel level, EntityType type, BlockPos pos, boolean noAi) { + Mob mob = type.spawn(level, pos, EntitySpawnReason.COMMAND); + if (mob != null) { + mob.setNoAi(noAi); + mob.setPersistenceRequired(); + } + } +} diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 09fdfa1..406134f 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -1,6 +1,7 @@ package za.co.neroland.nerospace.fabric; import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; @@ -22,6 +23,7 @@ import net.minecraft.world.level.levelgen.placement.PlacedFeature; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.command.NerospaceCommands; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; import za.co.neroland.nerospace.gas.NerospaceGasStorage; @@ -98,6 +100,9 @@ public void register(EntityType type, SpawnPlacementType plac OxygenFieldEvents.tick(server); TerraformDrift.tick(server); }); + // Creative debug commands (/nerospace gallery). + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> + NerospaceCommands.register(dispatcher)); // Terraform catch-up: convert any in-range columns on chunks that load after the frontier passed. // (Fabric's Load SAM passes a third "newly generated" flag, which we don't need.) ServerChunkEvents.CHUNK_LOAD.register((serverLevel, chunk, newlyGenerated) -> diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 8c16a09..3a138f3 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -8,6 +8,7 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import net.neoforged.neoforge.event.entity.RegisterSpawnPlacementsEvent; +import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent; @@ -21,6 +22,7 @@ import net.minecraft.world.level.levelgen.Heightmap; import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.command.NerospaceCommands; import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.telemetry.NerospaceTelemetry; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; @@ -68,6 +70,9 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { OxygenFieldEvents.tick(event.getServer()); TerraformDrift.tick(event.getServer()); }); + // Creative debug commands (/nerospace gallery) — game-bus command registration. + NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> + NerospaceCommands.register(event.getDispatcher())); // Terraform catch-up: convert any in-range columns on chunks that load after the frontier passed. NeoForge.EVENT_BUS.addListener((ChunkEvent.Load event) -> { if (event.getLevel() instanceof ServerLevel serverLevel && event.getChunk() instanceof LevelChunk chunk) { From 3403e704494e3e6d232ce6be0ee9c8e0f164097a Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:15:27 +0200 Subject: [PATCH 74/82] Add orbital station docking selection Expose per-station docking selection for rockets across UI, menu and entity logic.\n\n- Docs: mark Star Guide / rocket slice 2 as done and describe station selection UI behavior.\n- RocketScreen: add a station "Dock:" cycler button, show/hide it when the Orbital Station is chosen, and route clicks to menu button.\n- RocketMenu: add BUTTON_CYCLE_STATION, increase ContainerData length to 6, handle the new button id, and provide helpers getStationSlot(), isStationDestination(), and getStationName() used by the client UI.\n- RocketEntity: add synced DATA_STATION (-1 = origin), expose getStationSlot(), implement server-side cycleStation() that iterates StationRegistry entries, use the selected station center() as rocket arrival when docking to the Station dimension, and persist StationSlot in save/load.\n\nThe change is compile-verified; NeoForge item-capability automation proxy remains deferred. --- docs/MULTILOADER_PORT_CHECKLIST.md | 22 +++++--- .../nerospace/client/RocketScreen.java | 18 +++++++ .../nerospace/rocket/RocketEntity.java | 54 +++++++++++++++++-- .../neroland/nerospace/rocket/RocketMenu.java | 30 ++++++++++- 4 files changed, 111 insertions(+), 13 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index f3b5d67..b961d6d 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -28,7 +28,8 @@ build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **decoupling founding from the deferred rocket FOUND row.** Registered block (no block item / loot table) + > BE + charter item; copied assets + lang; repointed the Star-Guide step + advancement icons to the now-real > `station_charter` item. **All 42 advancements now track real completion.** Break the Core to unregister + -> reclaim the (named) charter. Deferred (slice 2): the rocket's per-station selection/return rows. +> reclaim the (named) charter. **Slice 2 DONE:** the rocket's per-station selection — the in-rocket UI cycles the +> Orbital Station destination between the origin platform and each founded station, and the rocket docks at the chosen one. > **2026-06-21 update — Star Guide slice 2d (terraform advancements code-granted).** All 4 cells compile > green. `progression/StarGuideGrants` awards `guide/terraformed_ground` + `guide/living_world` from the @@ -255,14 +256,19 @@ checked by a headless build). ## 🚧 Remaining subsystems -### Rockets & travel (`rocket/` 11 + client + items) — **core DONE (4 cells green); station-founding deferred** +### Rockets & travel (`rocket/` 11 + client + items) — **DONE (4 cells green); item-cap proxy deferred** - [x] `RocketTier`, `Destinations` (ported; `Tuning` values inlined as identity-multiplier base values). - [~] `RocketEntity` — rebuilt on the cross-loader `FluidTank` + a plain `SimpleContainer(1)` intake + - vanilla `ServerPlayer.teleportTo`. **Deferred:** the NeoForge-transfer entity item-capability - **automation proxy** (pipe/hopper → docked rocket) and the multi-station selection. Risk: travel/teleport - unverifiable headlessly — compile-verified only. -- [x] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen` (destination selector + fuel gauge). Menu is - **non-extended** (no loader-divergent extended-menu API); the station/FOUND rows are deferred. + vanilla `ServerPlayer.teleportTo`. **Per-station selection DONE:** `DATA_STATION` synced slot (−1 = origin), + `cycleStation()` cycles origin → each founded station (founding order) → origin via `StationRegistry`, and + `completeLaunch()` docks the rider at the selected station's `center()` (else the origin platform); the slot + persists in `addAdditionalSaveData` (`StationSlot`). **Deferred:** the NeoForge-transfer entity item-capability + **automation proxy** (pipe/hopper → docked rocket). Risk: travel/teleport unverifiable headlessly — compile-verified only. +- [x] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen`. Menu is **non-extended** (no loader-divergent + extended-menu API); buttons route via `clickMenuButton`. **Station selection DONE:** `BUTTON_CYCLE_STATION` + + a synced `[5]=stationSlot` data value + a `RocketScreen` "Dock:" cycler shown only when the Orbital Station + is the chosen destination (label = stable founding-order "Station N"/"Origin Platform"; the custom charter + name stays server-side since `ContainerData` is int-only). The standalone FOUND row stays dropped — founding is charter-driven. - [x] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer` (bakes each tier layer directly — no model-layer registry), `RocketRenderState`; entity + item textures copied. - [x] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). @@ -270,7 +276,7 @@ checked by a headless build). POPIA-clean), and a new `StationCharterItem` — right-click the charter to found a station (slot + 7×7 pad + bound Core in the void station dim) and travel there; breaking the Core unregisters + pops the named charter; `guide/station_charter` is code-granted on founding (routes around `ModCriteria`). Founding is **charter-driven** - rather than via the rocket FOUND row (the rocket's per-station selection/return rows remain deferred). + rather than via the rocket FOUND row; the rocket's **per-station selection is now DONE** (see `RocketEntity`/`RocketMenu` above). ### Quarry (`machine/quarry/` 11 + client) — **DONE (4 cells green); modules + BER deferred** - [x] Area miner ported: `QuarryControllerBlock`(+BE) + `QuarryMenu`/`QuarryScreen`, `QuarryFrameBlock`, diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java index e59e330..6892d75 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java @@ -36,6 +36,7 @@ public class RocketScreen extends TexturedContainerScreen { private static final List> ALL_DESTINATIONS = RocketTier.TIER_4.destinations(); private SpaceButton launchButton; + private SpaceButton stationButton; private final List destinationButtons = new ArrayList<>(); public RocketScreen(RocketMenu menu, Inventory playerInventory, Component title) { @@ -62,6 +63,11 @@ protected void init() { x += 36; } + // Station dock cycler: shown only when the Orbital Station is the chosen destination. + this.stationButton = new SpaceButton(this.leftPos + 8, this.topPos + 52, 160, 12, + Component.empty(), ACCENT, b -> onCycleStation()); + this.addRenderableWidget(this.stationButton); + this.launchButton = new SpaceButton(this.leftPos + 8, this.topPos + 68, 160, 14, Component.translatable("gui.nerospace.rocket.launch"), ACCENT, b -> onLaunch()); this.addRenderableWidget(this.launchButton); @@ -83,6 +89,14 @@ protected void extractForeground(GuiGraphicsExtractor g) { node.visible = i < reachable; node.setSelected(i == selected); } + if (this.stationButton != null) { + boolean stationDest = this.menu.isStationDestination(); + this.stationButton.visible = stationDest; + this.stationButton.active = stationDest; + if (stationDest) { + this.stationButton.setMessage(Component.literal("Dock: " + this.menu.getStationName())); + } + } if (this.launchButton != null) { this.launchButton.active = this.menu.isLaunchable(); } @@ -116,6 +130,10 @@ private void onSelectDestination(int index) { sendButton(RocketMenu.SELECT_DEST_BASE + index); } + private void onCycleStation() { + sendButton(RocketMenu.BUTTON_CYCLE_STATION); + } + private void onLaunch() { sendButton(RocketMenu.BUTTON_LAUNCH); } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java index 05b4730..3883cee 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java @@ -74,6 +74,9 @@ public class RocketEntity extends Entity implements MenuProvider { /** Index into the current tier's destination list. */ private static final EntityDataAccessor DATA_DEST = SynchedEntityData.defineId(RocketEntity.class, EntityDataSerializers.INT); + /** Selected founded-station slot for the Orbital Station destination ({@code -1} = the origin platform). */ + private static final EntityDataAccessor DATA_STATION = + SynchedEntityData.defineId(RocketEntity.class, EntityDataSerializers.INT); /** Ticks of ascent before the rider is transported. */ public static final int LAUNCH_DURATION = 100; @@ -108,7 +111,8 @@ private static int maxTierFuelCapacity() { private final SimpleContainer fuelInput = new SimpleContainer(1); /** - * Synced to the menu: [0]=fuel, [1]=capacity, [2]=tierOrdinal, [3]=launchable, [4]=destinationIndex. + * Synced to the menu: [0]=fuel, [1]=capacity, [2]=tierOrdinal, [3]=launchable, [4]=destinationIndex, + * [5]=stationSlot (−1 = origin; only meaningful for the Orbital Station destination). */ private final ContainerData dataAccess = new ContainerData() { @Override @@ -119,6 +123,7 @@ public int get(int index) { case 2 -> getTier().ordinal(); case 3 -> canLaunch() ? 1 : 0; case 4 -> getDestinationIndex(); + case 5 -> getStationSlot(); default -> 0; }; } @@ -130,7 +135,7 @@ public void set(int index, int value) { @Override public int getCount() { - return 5; + return 6; } }; @@ -187,6 +192,7 @@ protected void defineSynchedData(SynchedEntityData.Builder builder) { builder.define(DATA_TIER, RocketTier.TIER_1.ordinal()); builder.define(DATA_LAUNCHING, false); builder.define(DATA_DEST, 0); + builder.define(DATA_STATION, -1); } public int getFuel() { @@ -259,6 +265,41 @@ public void setDestinationIndex(int index) { } } + // --- Orbital-station selection (which founded station the Station destination docks at) --- + + /** Selected founded-station slot, or {@code -1} for the shared origin platform. */ + public int getStationSlot() { + return this.entityData.get(DATA_STATION); + } + + /** + * Cycles the docking target for the Orbital Station destination: origin → each founded station (in + * founding order) → back to origin. Server-side; routed from the menu button. + */ + public void cycleStation() { + if (!(level() instanceof ServerLevel server) || isLaunching()) { + return; + } + java.util.List all = StationRegistry.get(server.getServer()).all(); + int current = getStationSlot(); + int next; + if (all.isEmpty()) { + next = -1; + } else if (current < 0) { + next = all.get(0).slot(); + } else { + int idx = -1; + for (int i = 0; i < all.size(); i++) { + if (all.get(i).slot() == current) { + idx = i; + break; + } + } + next = (idx < 0 || idx + 1 >= all.size()) ? -1 : all.get(idx + 1).slot(); + } + this.entityData.set(DATA_STATION, next); + } + public boolean isLaunching() { return this.entityData.get(DATA_LAUNCHING); } @@ -434,8 +475,11 @@ private void completeLaunch() { double arrivalZ; Component arrivalMessage; if (targetKey.equals(ModDimensions.STATION_LEVEL)) { - // Origin = the shared public platform (the multi-station founding system is deferred). - BlockPos centre = new BlockPos(0, PLATFORM_Y, 0); + // Dock at the selected founded station, or the shared origin platform when none is chosen. + int slot = getStationSlot(); + StationRegistry.StationEntry entry = + slot >= 0 ? StationRegistry.get(server).get(slot) : null; + BlockPos centre = entry != null ? entry.center() : new BlockPos(0, PLATFORM_Y, 0); arrivalMessage = Component.translatable("entity.nerospace.rocket.docked"); destination.getChunk(centre.getX() >> 4, centre.getZ() >> 4); if (!destination.getBlockState(centre).is(ModBlocks.STATION_FLOOR.get())) { @@ -570,6 +614,7 @@ public boolean hurtServer(ServerLevel level, net.minecraft.world.damagesource.Da protected void readAdditionalSaveData(ValueInput input) { this.entityData.set(DATA_TIER, input.getIntOr("Tier", RocketTier.TIER_1.ordinal())); this.entityData.set(DATA_DEST, input.getIntOr("Destination", getTier().defaultDestinationIndex())); + this.entityData.set(DATA_STATION, input.getIntOr("StationSlot", -1)); Fluid fluid = BuiltInRegistries.FLUID.getValue( Identifier.parse(input.getStringOr("FuelFluid", "minecraft:empty"))); this.fuelTank.setRaw(fluid, input.getIntOr("FuelAmount", 0)); @@ -583,6 +628,7 @@ protected void readAdditionalSaveData(ValueInput input) { protected void addAdditionalSaveData(ValueOutput output) { output.putInt("Tier", getTier().ordinal()); output.putInt("Destination", getDestinationIndex()); + output.putInt("StationSlot", getStationSlot()); output.putString("FuelFluid", BuiltInRegistries.FLUID.getKey(this.fuelTank.getRawFluid()).toString()); output.putInt("FuelAmount", this.fuelTank.getRawAmount()); output.store("FuelInput", ItemStack.OPTIONAL_CODEC, this.fuelInput.getItem(0)); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java index dc46b79..1fbdcb8 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java @@ -12,6 +12,7 @@ import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import za.co.neroland.nerospace.registry.ModDimensions; import za.co.neroland.nerospace.registry.ModMenuTypes; /** @@ -32,10 +33,12 @@ public class RocketMenu extends AbstractContainerMenu { public static final int BUTTON_LAUNCH = 0; public static final int BUTTON_CYCLE_DEST = 1; + /** Cycles which founded station the Orbital Station destination docks at (origin → each station → origin). */ + public static final int BUTTON_CYCLE_STATION = 2; /** Select destination {@code n} via button id {@code SELECT_DEST_BASE + n}. */ public static final int SELECT_DEST_BASE = 100; - private static final int DATA_COUNT = 5; + private static final int DATA_COUNT = 6; private static final int FUEL_SLOT_INDEX = 0; private static final int PLAYER_INV_START = 1; private static final int PLAYER_INV_END = PLAYER_INV_START + 36; // exclusive @@ -80,6 +83,10 @@ public boolean clickMenuButton(Player player, int id) { current.cycleDestination(); return true; } + if (id == BUTTON_CYCLE_STATION) { + current.cycleStation(); + return true; + } if (id >= SELECT_DEST_BASE) { current.setDestinationIndex(id - SELECT_DEST_BASE); return true; @@ -167,6 +174,27 @@ public boolean hasMultipleDestinations() { return getTier().destinations().size() > 1; } + // --- Orbital-station selection (shown only when the Orbital Station is the destination) ---------- + + /** Selected founded-station slot, or {@code -1} for the shared origin platform. */ + public int getStationSlot() { + return this.data.get(5); + } + + /** Whether the currently selected destination is the Orbital Station dimension. */ + public boolean isStationDestination() { + return ModDimensions.STATION_LEVEL.equals(getTier().destination(getDestinationIndex())); + } + + /** + * Display label for the selected docking target. The custom charter name lives server-side (it can't + * ride the int-only {@link ContainerData}), so the client shows the stable founding-order label. + */ + public String getStationName() { + int slot = getStationSlot(); + return slot < 0 ? "Origin Platform" : "Station " + (slot + 1); + } + /** Fuel-intake slot: only rocket fuel buckets/canisters may be placed. */ private static class FuelSlot extends Slot { FuelSlot(Container container, int slot, int x, int y) { From 9bc53916cc34bec90ebc904abf16c87f6743c274 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:42:42 +0200 Subject: [PATCH 75/82] Sync station names to client for Rocket UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a small clientbound payload and client cache so rockets can show real station names in the in-rocket "Dock:" cycler. Introduces StationSyncPayload (custom packet + stream codec) and ClientStations (client-side slot→name map). ModNetwork registers the payload handler, RocketEntity now sends a snapshot to the player when opening the rocket, and RocketScreen reads ClientStations.name(...) to display the synced label. Documentation updated. Payload is identity-free (slots and display names only) to avoid sending player info. --- docs/MULTILOADER_PORT_CHECKLIST.md | 6 +- .../nerospace/client/ClientStations.java | 40 +++++++++++ .../nerospace/client/RocketScreen.java | 3 +- .../nerospace/network/ModNetwork.java | 3 + .../nerospace/network/StationSyncPayload.java | 69 +++++++++++++++++++ .../nerospace/rocket/RocketEntity.java | 4 ++ 6 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientStations.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/network/StationSyncPayload.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index b961d6d..e9d00c8 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -267,8 +267,10 @@ checked by a headless build). - [x] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen`. Menu is **non-extended** (no loader-divergent extended-menu API); buttons route via `clickMenuButton`. **Station selection DONE:** `BUTTON_CYCLE_STATION` + a synced `[5]=stationSlot` data value + a `RocketScreen` "Dock:" cycler shown only when the Orbital Station - is the chosen destination (label = stable founding-order "Station N"/"Origin Platform"; the custom charter - name stays server-side since `ContainerData` is int-only). The standalone FOUND row stays dropped — founding is charter-driven. + is the chosen destination. **Real charter names** ride a small clientbound `StationSyncPayload` (slot→name + parallel arrays, POPIA-clean — no player identity) pushed when the player opens a rocket and cached in + `client/ClientStations`; the cycler shows the live name (falling back to "Station N"/"Origin Platform") since + the int-only `ContainerData` can't carry strings. The standalone FOUND row stays dropped — founding is charter-driven. - [x] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer` (bakes each tier layer directly — no model-layer registry), `RocketRenderState`; entity + item textures copied. - [x] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientStations.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientStations.java new file mode 100644 index 0000000..ce2f013 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientStations.java @@ -0,0 +1,40 @@ +package za.co.neroland.nerospace.client; + +import java.util.HashMap; +import java.util.Map; + +import za.co.neroland.nerospace.network.StationSyncPayload; + +/** + * Client-side holder for the founded stations' display names (slot → name), fed by + * {@link StationSyncPayload} (the clientbound handler registered in {@code ModNetwork.init()}) when the + * player opens a rocket, and read by {@code RocketScreen} to label the "Dock:" cycler. Pure data — no + * client-only imports — so it loads safely even where the handler is registered from common code. + */ +public final class ClientStations { + + private static final Map NAMES = new HashMap<>(); + + private ClientStations() { + } + + public static void accept(StationSyncPayload payload) { + NAMES.clear(); + int[] slots = payload.slots(); + String[] names = payload.names(); + for (int i = 0; i < slots.length; i++) { + NAMES.put(slots[i], names[i]); + } + } + + /** + * The selected docking target's label: the shared origin platform for {@code slot < 0}, the synced + * charter name when known, else the stable founding-order fallback ("Station N"). + */ + public static String name(int slot) { + if (slot < 0) { + return "Origin Platform"; + } + return NAMES.getOrDefault(slot, "Station " + (slot + 1)); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java index 6892d75..eb97d2c 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java @@ -94,7 +94,8 @@ protected void extractForeground(GuiGraphicsExtractor g) { this.stationButton.visible = stationDest; this.stationButton.active = stationDest; if (stationDest) { - this.stationButton.setMessage(Component.literal("Dock: " + this.menu.getStationName())); + this.stationButton.setMessage( + Component.literal("Dock: " + ClientStations.name(this.menu.getStationSlot()))); } } if (this.launchButton != null) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java index 5c39052..813f3ee 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java @@ -84,5 +84,8 @@ public static void init() { // Oxygen field: server → nearby clients range-limited concentration snapshot for the visual layers. clientbound(OxygenFieldSyncPayload.TYPE, OxygenFieldSyncPayload.STREAM_CODEC, za.co.neroland.nerospace.client.ClientOxygenField::accept); + // Founded-station names: server → a player opening a rocket, so the "Dock:" cycler shows real names. + clientbound(StationSyncPayload.TYPE, StationSyncPayload.STREAM_CODEC, + za.co.neroland.nerospace.client.ClientStations::accept); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/network/StationSyncPayload.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/StationSyncPayload.java new file mode 100644 index 0000000..6e5dc6c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/StationSyncPayload.java @@ -0,0 +1,69 @@ +package za.co.neroland.nerospace.network; + +import java.util.List; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.Identifier; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.rocket.StationRegistry; + +/** + * Server → client snapshot of the founded stations' display names (slot → name), pushed to a player + * when they open a rocket so the in-rocket "Dock:" cycler can show the real charter name rather than + * the generic "Station N" label. {@link net.minecraft.world.inventory.ContainerData} is int-only, so + * the names can't ride the menu's synced data — this small parallel-array payload carries them instead. + * + *

Privacy (POPIA/GDPR): carries only the station slot and its display name + * (chosen by whoever founded it). No player identity — no names, UUIDs or founders — is ever sent, in + * keeping with {@link StationRegistry}'s deliberately identity-free storage.

+ * + *

Cross-loader note: registered (with its client handler {@code ClientStations::accept}) in + * {@code ModNetwork.init()}; both loaders' networking seams pick it up from the clientbound list.

+ */ +public record StationSyncPayload(int[] slots, String[] names) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "station_sync")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.of(StationSyncPayload::write, StationSyncPayload::read); + + /** Snapshot the registry's stations in founding order. */ + public static StationSyncPayload of(StationRegistry registry) { + List all = registry.all(); + int[] slots = new int[all.size()]; + String[] names = new String[all.size()]; + for (int i = 0; i < all.size(); i++) { + slots[i] = all.get(i).slot(); + names[i] = all.get(i).name(); + } + return new StationSyncPayload(slots, names); + } + + private static void write(RegistryFriendlyByteBuf buf, StationSyncPayload payload) { + buf.writeVarInt(payload.slots.length); + for (int i = 0; i < payload.slots.length; i++) { + buf.writeVarInt(payload.slots[i]); + buf.writeUtf(payload.names[i]); + } + } + + private static StationSyncPayload read(RegistryFriendlyByteBuf buf) { + int n = buf.readVarInt(); + int[] slots = new int[n]; + String[] names = new String[n]; + for (int i = 0; i < n; i++) { + slots[i] = buf.readVarInt(); + names[i] = buf.readUtf(); + } + return new StationSyncPayload(slots, names); + } + + @Override + public Type type() { + return TYPE; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java index 3883cee..82e118a 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java @@ -559,6 +559,10 @@ public InteractionResult interact(Player player, InteractionHand hand, Vec3 hitL player.startRiding(this); } if (player instanceof ServerPlayer serverPlayer) { + // Push the founded-station names so the in-rocket "Dock:" cycler can label them. + za.co.neroland.nerospace.network.ModNetwork.sendToPlayer(serverPlayer, + za.co.neroland.nerospace.network.StationSyncPayload.of( + StationRegistry.get(serverPlayer.level().getServer()))); serverPlayer.openMenu(this); } } From e6459ec853c3a55051f5e3df10e6a21352523907 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Sun, 21 Jun 2026 23:58:25 +0200 Subject: [PATCH 76/82] Add tiered solar panels and pooling array Introduce multi-tier solar panels and a pooled-array system. Adds SolarArray (flood-fill pooling and per-tick balancing) and SolarTier (tier definitions, scaled FE/buffer/throughput). SolarPanelBlock was made tier-aware (codec, tier field, comparator output) and SolarPanelBlockEntity rewritten to use EnergyBuffer, hold an anchor, lazily adopt a SolarArray, compute tier-scaled generation (daylight/weather/airless-dimension factor), and persist anchor/energy. Registry updates register T2/T3 blocks/items and include them in the block entity type and creative tab. Also adds blockstates, item models, block models, textures and language entries for the new tiered panels. --- docs/MULTILOADER_PORT_CHECKLIST.md | 14 +- .../nerospace/machine/SolarArray.java | 131 ++++++++++++++++++ .../nerospace/machine/SolarPanelBlock.java | 38 ++++- .../machine/SolarPanelBlockEntity.java | 130 ++++++++++++++--- .../neroland/nerospace/machine/SolarTier.java | 62 +++++++++ .../nerospace/registry/ModBlockEntities.java | 3 +- .../nerospace/registry/ModBlocks.java | 12 +- .../neroland/nerospace/registry/ModItems.java | 4 +- .../nerospace/blockstates/solar_panel_t2.json | 7 + .../nerospace/blockstates/solar_panel_t3.json | 7 + .../nerospace/items/solar_panel_t2.json | 6 + .../nerospace/items/solar_panel_t3.json | 6 + .../assets/nerospace/lang/en_us.json | 2 + .../models/block/solar_panel_t2.json | 8 ++ .../models/block/solar_panel_t3.json | 8 ++ .../textures/block/solar_panel_t2.png | Bin 0 -> 271 bytes .../textures/block/solar_panel_t2_base.png | Bin 0 -> 237 bytes .../textures/block/solar_panel_t3.png | Bin 0 -> 262 bytes .../textures/block/solar_panel_t3_base.png | Bin 0 -> 237 bytes .../loot_table/blocks/solar_panel_t2.json | 12 ++ .../loot_table/blocks/solar_panel_t3.json | 12 ++ 21 files changed, 436 insertions(+), 26 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarArray.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarTier.java create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t2.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t3.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t2.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t3.json create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png create mode 100644 multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json create mode 100644 multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index e9d00c8..f8821f6 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -476,8 +476,18 @@ checked by a headless build). speed / energy / Silk-Touch / Fortune multipliers now drive the dig (the quarry's earlier `×1.0` deferral is resolved). Assets + 4 lang keys copied. -### Solar — tiers/array/BER (`solar/` 4; single-tier base **done**) -- [~] `SolarTier`, `SolarArray` (multi-panel pooling), the root tiered block/BE + sun-tracking BER. +### Solar — tiers/array/BER (`machine/Solar*`) — **slice 1 DONE (4 cells green); multiblock + BER deferred** +- [x] **Tiers + array pooling DONE.** `SolarTier` (T1/T2/T3, config-scaled FE/buffer via `NerospaceConfig`) + + `SolarArray` (flood-fill same-tier pooling, rebalanced each tick so a pipe on ANY panel drains the + whole run) + tier-aware `SolarPanelBlock` (comparator output) + `SolarPanelBlockEntity` rebuilt on the + multiloader `EnergyBuffer` (the NeoForge transfer `SimpleEnergyHandler` isn't ported). `solar_panel` + stays Tier 1 (**non-breaking**) and `solar_panel_t2` / `solar_panel_t3` are added; the shared `SOLAR_PANEL` + BE type is bound to all three, so the existing per-loader energy cap (`be.getEnergy()`) covers them with + no per-loader change. Daylight uses vanilla `getSkyDarken()` (the NeoForge dimension clock / + `getDayTime()` / `LevelData.getDayTime()` aren't on the de-obf classpath); airless dims get the 2× sun + bonus via `ModDimensions` keys. Assets: tier textures copied from root + hand-authored block/item/loot JSON; 2 lang keys. +- [~] **Deferred (slice 2):** the N×N multiblock footprint (every tier is 1×1 for now — `SolarTier.footprint` + is carried but unused for placement) and the tilting sun-tracking deck renderer (the BER seam is ready). ### Creative storage variants (`storage/Creative*`) — **DONE (4 cells green)** - [x] `AbstractStorageBlock` (shared base) + `CreativeFluidTank` (endless rocket_fuel), `CreativeGasTank` diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarArray.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarArray.java new file mode 100644 index 0000000..b5bd5c5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarArray.java @@ -0,0 +1,131 @@ +package za.co.neroland.nerospace.machine; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; + +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; + +/** + * A connected run of same-tier solar panel units treated as ONE machine: total storage is the + * sum of every unit's buffer and total generation the sum of every unit's (sky-/weather-/dimension- + * scaled) output. The pooled energy is kept balanced across the unit buffers, so a pipe pulling from + * ANY panel face drains the whole array. + * + *

Built by flood-fill across all connected same-tier cells; membership is rebuilt lazily so placing + * or breaking a panel needs no explicit hooks. Only the SAME {@link SolarTier} is adopted — different + * tiers stay separate arrays.

+ * + *

Cross-loader port: identical to the standalone {@code solar.SolarArray} except the per-unit energy + * store is the multiloader {@link za.co.neroland.nerospace.energy.EnergyBuffer} (raw int accessors) + * rather than the NeoForge transfer handler.

+ */ +public final class SolarArray { + + private static final int MAX_CELLS = 16_384; + + private final SolarTier tier; + /** Member anchor positions (one per unit; every 1×1 cell is its own anchor). */ + private final List anchors; + private boolean valid = true; + private long lastTick = -1L; + + private SolarArray(SolarTier tier, List anchors) { + this.tier = tier; + this.anchors = anchors; + } + + public boolean isValid() { + return this.valid; + } + + public SolarTier tier() { + return this.tier; + } + + /** Number of pooled units in the array. */ + public int size() { + return this.anchors.size(); + } + + /** Flood-fill the connected same-tier cells from {@code seed}, collect the distinct unit anchors. */ + public static SolarArray getOrBuild(ServerLevel level, BlockPos seed, SolarTier tier) { + List anchors = new ArrayList<>(); + LongOpenHashSet anchorSet = new LongOpenHashSet(); + LongOpenHashSet seen = new LongOpenHashSet(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(seed); + seen.add(seed.asLong()); + + int visited = 0; + while (!queue.isEmpty() && visited < MAX_CELLS) { + BlockPos pos = queue.poll(); + if (!(level.getBlockEntity(pos) instanceof SolarPanelBlockEntity cell) || cell.tier() != tier) { + continue; + } + visited++; + BlockPos anchor = cell.anchorPos(); + if (anchorSet.add(anchor.asLong())) { + anchors.add(anchor); + } + for (Direction dir : Direction.values()) { + BlockPos np = pos.relative(dir); + if (seen.add(np.asLong()) + && level.getBlockEntity(np) instanceof SolarPanelBlockEntity neighbour + && neighbour.tier() == tier) { + queue.add(np); + } + } + } + + SolarArray array = new SolarArray(tier, anchors); + for (BlockPos anchor : anchors) { + if (level.getBlockEntity(anchor) instanceof SolarPanelBlockEntity a) { + a.adopt(array); + } + } + return array; + } + + /** Generate this tick's pooled energy and re-balance the buffers. Runs once per game tick. */ + public void tick(ServerLevel level) { + long gameTime = level.getGameTime(); + if (gameTime == this.lastTick) { + return; + } + this.lastTick = gameTime; + + List units = new ArrayList<>(this.anchors.size()); + for (BlockPos anchor : this.anchors) { + if (level.getBlockEntity(anchor) instanceof SolarPanelBlockEntity a + && a.isAnchor() && a.tier() == this.tier) { + units.add(a); + } else { + this.valid = false; // a unit vanished/changed — members rebuild next tick + return; + } + } + if (units.isEmpty()) { + this.valid = false; + return; + } + + // Each unit contributes its own daylight-scaled output; the sum is the array's generation. Add + // into the per-unit buffers, then balance them into one pool. + long total = 0L; + for (SolarPanelBlockEntity unit : units) { + unit.generate(unit.generationThisTick(level)); + total += unit.energy().getRaw(); + } + int n = units.size(); + int base = (int) (total / n); + int remainder = (int) (total % n); + for (int i = 0; i < n; i++) { + units.get(i).energy().setRaw(base + (i < remainder ? 1 : 0)); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java index 5cc74ca..488e2ee 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java @@ -1,8 +1,10 @@ package za.co.neroland.nerospace.machine; import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.RenderShape; @@ -15,13 +17,33 @@ import za.co.neroland.nerospace.registry.ModBlockEntities; -/** Solar Panel block — ticks its {@link SolarPanelBlockEntity}. GUI-less, single-tier. */ +/** + * A solar panel that pools with adjacent same-tier panels into a {@link SolarArray}. Each tier is its + * own registered block with its own output/buffer; energy is exposed on every side (output ports), so + * any face feeds a pipe or machine from the shared array pool. GUI-less; emits a comparator signal + * proportional to its stored energy. + * + *

Cross-loader port: tier-aware (the standalone single-tier block is generalised). The N×N + * multiblock footprint + the tilting sun-tracking deck renderer are a deferred enhancement — every + * panel is a 1×1 block here.

+ */ public class SolarPanelBlock extends BaseEntityBlock { - public static final MapCodec CODEC = simpleCodec(SolarPanelBlock::new); + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + instance.group( + SolarTier.CODEC.fieldOf("tier").forGetter(SolarPanelBlock::tier), + propertiesCodec() + ).apply(instance, SolarPanelBlock::new)); - public SolarPanelBlock(Properties properties) { + private final SolarTier tier; + + public SolarPanelBlock(SolarTier tier, Properties properties) { super(properties); + this.tier = tier; + } + + public SolarTier tier() { + return this.tier; } @Override @@ -49,4 +71,14 @@ public BlockEntityTicker getTicker(Level level, Block return createTickerHelper(type, ModBlockEntities.SOLAR_PANEL.get(), (lvl, pos, st, be) -> be.tick(lvl, pos, st)); } + + @Override + protected boolean hasAnalogOutputSignal(BlockState state) { + return true; + } + + @Override + protected int getAnalogOutputSignal(BlockState state, Level level, BlockPos pos, Direction direction) { + return level.getBlockEntity(pos) instanceof SolarPanelBlockEntity panel ? panel.comparatorSignal() : 0; + } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java index 1391678..373c164 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java @@ -1,64 +1,158 @@ package za.co.neroland.nerospace.machine; +import java.util.Set; + import net.minecraft.core.BlockPos; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.storage.ValueInput; import net.minecraft.world.level.storage.ValueOutput; -import za.co.neroland.nerospace.config.NerospaceConfig; +import org.jetbrains.annotations.Nullable; + import za.co.neroland.nerospace.energy.EnergyBuffer; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModDimensions; /** - * Solar Panel — a daylight generator: while it is day and the panel can see the sky it trickles energy - * into its buffer (reduced in rain). Exposes the energy capability (extract-only, drained by the pipe - * network). Single-tier and GUI-less here; the root project's tiered sun-tracking array + renderer are - * a deferred enhancement. + * One solar panel. Adjacent same-tier panels pool into a {@link SolarArray}: total storage and + * generation are the sums across every member, balanced each tick so a pipe on ANY panel drains the + * whole pool. Generation scales with the sun's height (peaks at noon, zero at night), needs a clear + * view of the sky, is cut by rain/thunder, and is doubled in the mod's airless dimensions (permanent + * sun). The cap is exposed on every side (extract-only) via the energy seam. + * + *

Cross-loader port: rebuilt on the multiloader {@link EnergyBuffer} (the NeoForge transfer + * {@code SimpleEnergyHandler} isn't ported); daylight uses vanilla {@code getDayTime} rather than the + * NeoForge-only dimension clock, and the airless 2× bonus keys off {@link ModDimensions} (no + * {@code ModDimensionTypes}). Every panel is a 1×1 self-anchor here; the N×N multiblock + sun-tracking + * renderer are a deferred enhancement.

*/ public class SolarPanelBlockEntity extends BlockEntity { - public static final int CAPACITY = 40_000; - public static final int MAX_EXTRACT = 256; - public static final int FE_PER_TICK = 16; + /** The mod's airless planets/station — permanent sun, no weather (the solar 2× bonus). */ + private static final Set> AIRLESS = Set.of( + ModDimensions.GREENXERTZ_LEVEL, ModDimensions.CINDARA_LEVEL, + ModDimensions.STATION_LEVEL, ModDimensions.GLACIRA_LEVEL); + + private final SolarTier tier; + private final EnergyBuffer energy; + + /** This cell's unit anchor. Always self for a 1×1 panel; kept for forward-compatible multiblocks. */ + private BlockPos anchorPos; - private final EnergyBuffer energy = new EnergyBuffer(CAPACITY, 0, MAX_EXTRACT, this::setChanged); + /** Transient: the array this unit belongs to, lazily (re)built and shared with all members. */ + @Nullable + private SolarArray array; public SolarPanelBlockEntity(BlockPos pos, BlockState state) { super(ModBlockEntities.SOLAR_PANEL.get(), pos, state); + this.tier = state.getBlock() instanceof SolarPanelBlock panel ? panel.tier() : SolarTier.TIER_1; + this.energy = new EnergyBuffer(this.tier.buffer(), 0, this.tier.maxExtract(), this::setChanged); + this.anchorPos = pos; + } + + public SolarTier tier() { + return this.tier; + } + + public BlockPos anchorPos() { + return this.anchorPos; } + public boolean isAnchor() { + return this.anchorPos.equals(this.worldPosition); + } + + /** Exposed to the energy capability on every face (extract-only pool). */ public NerospaceEnergyStorage getEnergy() { return this.energy; } + /** The raw buffer — used by {@link SolarArray} to balance the pool. */ + EnergyBuffer energy() { + return this.energy; + } + + /** Number of pooled units in this panel's array (1 until resolved). */ + public int arraySize() { + SolarArray net = this.array; + return net == null ? 1 : net.size(); + } + + /** Called by {@link SolarArray#getOrBuild} so every member shares the one array instance. */ + void adopt(SolarArray net) { + this.array = net; + } + + /** Drop the cached array so the next tick rebuilds it (placement / break / neighbour change). */ + public void invalidateArray() { + this.array = null; + } + + /** Raise the buffer (generation path; bypasses the zero external-receive limit). */ + void generate(int amount) { + this.energy.generate(amount); + } + + public int comparatorSignal() { + int cap = (int) this.energy.getCapacity(); + int stored = (int) this.energy.getAmount(); + return (cap <= 0 || stored <= 0) ? 0 : 1 + (int) (stored / (double) cap * 14.0D); + } + public void tick(Level level, BlockPos pos, BlockState state) { - if (level.isClientSide()) { + if (!(level instanceof ServerLevel server) || !isAnchor()) { return; } - // getSkyDarken() is 0 in full daylight and ramps toward ~11 at night (and in storms); a low - // value plus an open sky above means the panel is illuminated. - if (level.getSkyDarken() >= 4 || !level.canSeeSky(pos.above())) { - return; + SolarArray net = this.array; // local so the null/valid check holds for the analyzer + if (net == null || !net.isValid()) { + net = SolarArray.getOrBuild(server, pos, this.tier); + this.array = net; + } + net.tick(server); + } + + /** FE this unit adds this tick = its tier's peak output × the daylight/weather/dimension factor. */ + public int generationThisTick(ServerLevel level) { + return Math.round(this.tier.fePerTick() * solarFactor(level, this.worldPosition)); + } + + /** + * Daylight factor in [0, 2]: a daylight curve (0 at night / when roofed over), and doubled in the + * airless dimensions (permanent sun, no weather). + * + *

Cross-loader note: derived from vanilla {@code getSkyDarken()} (the NeoForge dimension clock + * used by the standalone mod isn't on the de-obf classpath). {@code getSkyDarken()} is 0 at full + * daylight and ramps toward ~11 at night — and rises during rain/thunder, so weather is already + * folded in (no separate multiplier needed).

+ */ + private static float solarFactor(ServerLevel level, BlockPos pos) { + if (AIRLESS.contains(level.dimension())) { + return 2.0F; // permanent sun in orbit / on an airless moon, no weather — the 2x bonus } - int rate = FE_PER_TICK; - if (level.isRaining() || level.isThundering()) { - rate /= 2; + if (!level.canSeeSky(pos.above())) { + return 0.0F; // roofed over — no sun reaches the panel } - this.energy.generate(NerospaceConfig.scale(rate, NerospaceConfig.energyRateMultiplier())); + int darken = level.getSkyDarken(); + return Mth.clamp((10 - darken) / 10.0F, 0.0F, 1.0F); } @Override protected void saveAdditional(ValueOutput output) { super.saveAdditional(output); output.putInt("Energy", this.energy.getRaw()); + output.putLong("Anchor", this.anchorPos.asLong()); } @Override protected void loadAdditional(ValueInput input) { super.loadAdditional(input); this.energy.setRaw(input.getIntOr("Energy", 0)); + this.anchorPos = BlockPos.of(input.getLongOr("Anchor", this.worldPosition.asLong())); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarTier.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarTier.java new file mode 100644 index 0000000..b9197e8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarTier.java @@ -0,0 +1,62 @@ +package za.co.neroland.nerospace.machine; + +import com.mojang.serialization.Codec; + +import net.minecraft.util.StringRepresentable; + +import za.co.neroland.nerospace.config.NerospaceConfig; + +/** + * The three solar-panel tiers. Each tier is its own registered block and only ever pools energy with + * panels of the SAME tier (a Tier 1 array and a Tier 2 array never merge — see {@link SolarArray}). + * Generation and storage are config-scaled through {@code energyRateMultiplier}, so modpacks tune the + * whole family at once. + * + *

Cross-loader port note: footprints are carried for parity with the standalone mod's N×N multiblock + * tiers, but this slice registers every tier as a single 1×1 block (array pooling already gives the + * "combine panels" feel); the N×N footprint + the sun-tracking renderer are a deferred enhancement. Base + * values are inlined here (the standalone {@code Tuning} table isn't ported) and scaled at read time.

+ */ +public enum SolarTier implements StringRepresentable { + TIER_1("tier_1", 1, 1, 20, 50_000), + TIER_2("tier_2", 2, 2, 100, 250_000), + TIER_3("tier_3", 3, 3, 400, 1_000_000); + + public static final Codec CODEC = StringRepresentable.fromEnum(SolarTier::values); + + private final String name; + /** 1-based tier number. */ + public final int tier; + /** Footprint edge length in blocks: T1 = 1 (1×1), T2 = 2, T3 = 3 (parity; placement is 1×1 for now). */ + public final int footprint; + private final int baseFePerTick; + private final int baseBuffer; + + SolarTier(String name, int tier, int footprint, int baseFePerTick, int baseBuffer) { + this.name = name; + this.tier = tier; + this.footprint = footprint; + this.baseFePerTick = baseFePerTick; + this.baseBuffer = baseBuffer; + } + + /** Peak FE/tick this single panel adds at full sun (config-scaled). */ + public int fePerTick() { + return NerospaceConfig.scale(this.baseFePerTick, NerospaceConfig.energyRateMultiplier()); + } + + /** This panel's own FE buffer; an array's total storage is the sum across its members. */ + public int buffer() { + return NerospaceConfig.scale(this.baseBuffer, NerospaceConfig.energyRateMultiplier()); + } + + /** Per-tier extract throughput (well above the peak output, so a pipe never bottlenecks the array). */ + public int maxExtract() { + return Math.max(256, fePerTick() * 16); + } + + @Override + public String getSerializedName() { + return this.name; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index e771356..b8c54ee 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -87,7 +87,8 @@ public final class ModBlockEntities { public static final RegistryEntry> SOLAR_PANEL = BLOCK_ENTITIES.register("solar_panel", - key -> new BlockEntityType<>(SolarPanelBlockEntity::new, java.util.Set.of(ModBlocks.SOLAR_PANEL.get()))); + key -> new BlockEntityType<>(SolarPanelBlockEntity::new, java.util.Set.of( + ModBlocks.SOLAR_PANEL.get(), ModBlocks.SOLAR_PANEL_T2.get(), ModBlocks.SOLAR_PANEL_T3.get()))); public static final RegistryEntry> FUEL_TANK = BLOCK_ENTITIES.register("fuel_tank", diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java index 5f0a2eb..2d0d2de 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -230,10 +230,20 @@ public final class ModBlocks { .requiresCorrectToolForDrops().lightLevel(s -> 7).sound(SoundType.METAL))); public static final RegistryEntry SOLAR_PANEL = BLOCKS.register("solar_panel", - key -> new SolarPanelBlock(BlockBehaviour.Properties.of() + key -> new SolarPanelBlock(za.co.neroland.nerospace.machine.SolarTier.TIER_1, BlockBehaviour.Properties.of() .setId(key).mapColor(MapColor.COLOR_BLUE).strength(2.0F, 6.0F) .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + public static final RegistryEntry SOLAR_PANEL_T2 = BLOCKS.register("solar_panel_t2", + key -> new SolarPanelBlock(za.co.neroland.nerospace.machine.SolarTier.TIER_2, BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.COLOR_MAGENTA).strength(2.5F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + + public static final RegistryEntry SOLAR_PANEL_T3 = BLOCKS.register("solar_panel_t3", + key -> new SolarPanelBlock(za.co.neroland.nerospace.machine.SolarTier.TIER_3, BlockBehaviour.Properties.of() + .setId(key).mapColor(MapColor.GOLD).strength(3.0F, 6.0F) + .requiresCorrectToolForDrops().sound(SoundType.METAL).noOcclusion())); + // --- Fuel machines ------------------------------------------------------ public static final RegistryEntry FUEL_TANK = BLOCKS.register("fuel_tank", key -> new FuelTankBlock(BlockBehaviour.Properties.of() diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index d7d51db..fde0d13 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -95,6 +95,8 @@ public final class ModItems { public static final RegistryEntry HYDRATION_MODULE_ITEM = blockItem("hydration_module", ModBlocks.HYDRATION_MODULE); public static final RegistryEntry TERRAFORM_MONITOR_ITEM = blockItem("terraform_monitor", ModBlocks.TERRAFORM_MONITOR); public static final RegistryEntry SOLAR_PANEL_ITEM = blockItem("solar_panel", ModBlocks.SOLAR_PANEL); + public static final RegistryEntry SOLAR_PANEL_T2_ITEM = blockItem("solar_panel_t2", ModBlocks.SOLAR_PANEL_T2); + public static final RegistryEntry SOLAR_PANEL_T3_ITEM = blockItem("solar_panel_t3", ModBlocks.SOLAR_PANEL_T3); public static final RegistryEntry ROCKET_LAUNCH_PAD_ITEM = blockItem("rocket_launch_pad", ModBlocks.ROCKET_LAUNCH_PAD); public static final RegistryEntry LAUNCH_GANTRY_ITEM = blockItem("launch_gantry", ModBlocks.LAUNCH_GANTRY); public static final RegistryEntry FUEL_TANK_ITEM = blockItem("fuel_tank", ModBlocks.FUEL_TANK); @@ -305,7 +307,7 @@ public static Map, List> creativeTabItems OXYGEN_SUIT_HEAT_HELMET.get(), OXYGEN_SUIT_HEAT_CHESTPLATE.get(), OXYGEN_SUIT_HEAT_LEGGINGS.get(), OXYGEN_SUIT_HEAT_BOOTS.get(), OXYGEN_SUIT_COLD_HELMET.get(), OXYGEN_SUIT_COLD_CHESTPLATE.get(), OXYGEN_SUIT_COLD_LEGGINGS.get(), OXYGEN_SUIT_COLD_BOOTS.get()), CreativeModeTabs.FUNCTIONAL_BLOCKS, - List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), HYDRATION_MODULE_ITEM.get(), TERRAFORM_MONITOR_ITEM.get(), + List.of(ITEM_STORE_ITEM.get(), BATTERY_ITEM.get(), FLUID_TANK_ITEM.get(), COMBUSTION_GENERATOR_ITEM.get(), NEROSIUM_GRINDER_ITEM.get(), PASSIVE_GENERATOR_ITEM.get(), UNIVERSAL_PIPE_ITEM.get(), TRASH_CAN_ITEM.get(), CREATIVE_BATTERY_ITEM.get(), GAS_TANK_ITEM.get(), OXYGEN_GENERATOR_ITEM.get(), SOLAR_PANEL_ITEM.get(), SOLAR_PANEL_T2_ITEM.get(), SOLAR_PANEL_T3_ITEM.get(), ROCKET_LAUNCH_PAD_ITEM.get(), LAUNCH_GANTRY_ITEM.get(), FUEL_TANK_ITEM.get(), FUEL_REFINERY_ITEM.get(), QUARRY_CONTROLLER_ITEM.get(), QUARRY_LANDMARK_ITEM.get(), TERRAFORMER_ITEM.get(), HYDRATION_MODULE_ITEM.get(), TERRAFORM_MONITOR_ITEM.get(), SPEED_MODULE.get(), EFFICIENCY_MODULE.get(), FORTUNE_MODULE.get(), SILK_TOUCH_MODULE.get(), CREATIVE_FLUID_TANK_ITEM.get(), CREATIVE_GAS_TANK_ITEM.get(), CREATIVE_ITEM_STORE_ITEM.get(), STAR_GUIDE_ITEM.get())); diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json new file mode 100644 index 0000000..ba785d8 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/solar_panel_t2" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json new file mode 100644 index 0000000..bfd7411 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json @@ -0,0 +1,7 @@ +{ + "variants": { + "": { + "model": "nerospace:block/solar_panel_t3" + } + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t2.json b/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t2.json new file mode 100644 index 0000000..1b4cbb5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t2.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/solar_panel_t2" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t3.json b/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t3.json new file mode 100644 index 0000000..443688e --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/items/solar_panel_t3.json @@ -0,0 +1,6 @@ +{ + "model": { + "type": "minecraft:model", + "model": "nerospace:block/solar_panel_t3" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index 75f80ab..f593f12 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -50,6 +50,8 @@ "block.nerospace.rocket_launch_pad.report.t3_not_ready": "Tier 3 needs a Station Wall ring or a Heavy Launch Complex", "block.nerospace.rocket_launch_pad.report.t3_ready": "Tier 3 ready: Station Wall ring or Heavy complex present", "block.nerospace.solar_panel": "Solar Panel", + "block.nerospace.solar_panel_t2": "Solar Panel (Tier 2)", + "block.nerospace.solar_panel_t3": "Solar Panel (Tier 3)", "block.nerospace.star_guide": "Star Guide", "block.nerospace.station_core": "Station Core", "block.nerospace.station_core.bound": "Station Core: %s", diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t2.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t2.json new file mode 100644 index 0000000..7aa50c5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t2.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cube_bottom_top", + "textures": { + "top": "nerospace:block/solar_panel_t2", + "bottom": "nerospace:block/solar_panel_t2_base", + "side": "nerospace:block/solar_panel_t2_base" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t3.json b/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t3.json new file mode 100644 index 0000000..e27b6b5 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/models/block/solar_panel_t3.json @@ -0,0 +1,8 @@ +{ + "parent": "minecraft:block/cube_bottom_top", + "textures": { + "top": "nerospace:block/solar_panel_t3", + "bottom": "nerospace:block/solar_panel_t3_base", + "side": "nerospace:block/solar_panel_t3_base" + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png new file mode 100644 index 0000000000000000000000000000000000000000..80f6b25159ff489f7070a6e5a00eef49efe3a02c GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`*F0SuLn`JZ?^&?vp*=HeLu+GW zqvE0G{D*fNS4-@Rxp{2CM*R+;SmVRC%T{bVTkLjN^!JH3Pw3=6yqMLs!1VJW8HpJZ zKyYvF&BG5uFSD7Op19D+7#nlqNbA8vhdG^jAG1#7{lE6!lZj9NBr8o;(7SL$oTYqP zjFUVw&>&_I02v74?~+O>R4ba(tK%oD|DdSs>Tl18xxH)Tl*J~@-Tv+mZaW09l7y-r2CRy;<0+y)n>UQSfn2PlUqNn-Uuxru|;Xz~FSZqe1wc<6)o=89ZJ6 KT-G@yGywp(5^RM4 literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png new file mode 100644 index 0000000000000000000000000000000000000000..f82e4caf92ad191602fb838061716d8efce7c358 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`TRdGHLn`JZ$830bsh*9Cft#6` z`S2u5;S-nsygI7PrY5ocd7J!2sfkxN9gt9!nk^Bxx8lfw!-tQ5czRlWPf6!7wH=H; zC)9)&|2#e2Ut?XOt-{yEAQh;!JZ3f}43{ zB`zFo^@)+tv6*9V#o*u02OsC3e<&4s?th5`_amm>T1OtK-%S!A@I>gBL`BISFdIaJ z_!q7!bQ@26oRd=cs)BL)=GFK7553As`6{uo;mw}X=llt!W}B*BRW#1H;8td~NlLBc zxuHPtNkjnZK#wtay85}Sb4q9e0Cz8D A#Q*>R literal 0 HcmV?d00001 diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png new file mode 100644 index 0000000000000000000000000000000000000000..81d869a11bb35db4d259778faccff6c8c0105404 GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`TRdGHLn`JZ$830bsh*9Cft#6` z`S2u5;S-nsygI7PrY4c@e%g4EsnY7yMnjvdB@%IaD~=pEeE9f>r>E8TY;o@uxx=8A zGP(2OA0S`k`UZvKeKkLi1cY2}zy5Qxn#lvRhW7UUh3l7gEa2Wz!Lp&cQR4aM4<8op zXn16>;0CKi;)JjUDUTNm)-UBy Date: Mon, 22 Jun 2026 00:05:13 +0200 Subject: [PATCH 77/82] Update MULTILOADER_PORT_CHECKLIST.md --- docs/MULTILOADER_PORT_CHECKLIST.md | 196 ++++++++++++++++------------- 1 file changed, 110 insertions(+), 86 deletions(-) diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index f8821f6..a2e02b7 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -46,6 +46,7 @@ build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **2026-06-21 update — Star Guide slice 2b (hologram BER + reusable BER seam).** All 4 cells compile green > (26.1.2 + 26.2). Added the first cross-loader block-entity-renderer seam (`ClientBlockEntityRenderers.Sink`) +> > + the Star Guide pedestal hologram renderer (spinning next-step icon). **26.x gotcha: `BlockEntityRendererProvider` > is 2-type-param ``.** The seam is reusable for future BERs (solar > sun-tracking deck, quarry drill head, etc.). @@ -63,6 +64,7 @@ build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > `item/StarGuideBookItem` + `client/StarGuideScreen` (built on the existing `TexturedContainerScreen` + > `SpaceButton` — near-verbatim since the root already uses the 26.x submission model). Registered block + > block-item + book item + BE + menu + per-loader screen; copied block/GUI/book assets + models + blockstate +> > + loot table + **98 lang keys** (full chapter/step text). The guide opens from the **Star Guide Book** (in > hand) or a **Star Guide pedestal** (install the book). **No `ModCriteria` needed** — the guide just reads > advancement completion (missing advancements read as incomplete), sidestepping the 26.1↔26.2 criterion @@ -84,6 +86,7 @@ build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > **2026-06-21 update — advanced pipes slice A (per-face configuration layer).** All 4 cells green > (compile on 26.1.2 + 26.2; full `:neoforge:build`+`:fabric:build` on 26.2). Added `pipe/PipeIoMode` +> > + `pipe/PipeResourceType` (pure-vanilla enums) and the three pipe tools — `item/ConfiguratorItem` > (cycle selected layer + cycle a face's I/O mode), `item/PipeFilterItem` (ItemResource→vanilla > **ItemStack** filter), `item/PipeUpgradeItem` (speed/capacity ×2). Extended `UniversalPipeBlockEntity` @@ -104,6 +107,7 @@ build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. > task. > **2026-06-21 update — oxygen field client visuals.** All 4 cells green. Added `network/OxygenFieldSyncPayload` +> > + `client/{ClientOxygenField, ClientOxygenVisuals}`; the field now syncs to nearby clients and renders as > drifting GLOW particles + a boundary sound — the breathable volume is finally visible. 2nd networking-seam > consumer. The haze fog-tint layer is deferred (NeoForge-only fog event; no portable Fabric counterpart). @@ -236,123 +240,127 @@ checked by a headless build). ## ✅ Done (cross-loader, all 4 cells green) -- [x] **Platform seams** — `Services`/`IPlatformHelper`, `RegistrationProvider` (+ per-loader factories), ++ [x] **Platform seams** — `Services`/`IPlatformHelper`, `RegistrationProvider` (+ per-loader factories), capability seams for item / energy / fluid / gas (expose + query), `FluidFactory` seam. -- [x] **Registries** — blocks, items, block-entities, menu types, entities, sounds, dimension keys, ++ [x] **Registries** — blocks, items, block-entities, menu types, entities, sounds, dimension keys, entity attributes (subset that's ported). -- [x] **Logistics** — energy / fluid / gas / **item** transport; the universal pipe relays all four. -- [x] **Machines / storage** — combustion + passive + solar generators, oxygen generator, nerosium ++ [x] **Logistics** — energy / fluid / gas / **item** transport; the universal pipe relays all four. ++ [x] **Machines / storage** — combustion + passive + solar generators, oxygen generator, nerosium grinder (+ 3 GUIs), item store, battery, creative battery, fluid tank, gas tank, trash can. -- [x] **Rocket-fuel fluid** — `BaseFlowingFluid`/`FluidType` (NeoForge) vs hand-written `FlowingFluid` ++ [x] **Rocket-fuel fluid** — `BaseFlowingFluid`/`FluidType` (NeoForge) vs hand-written `FlowingFluid` (Fabric), liquid block + bucket; NeoForge in-world render. (Fabric in-world render = follow-up.) -- [x] **All 10 mobs** — xertz stalker, quartz crawler, greenling, ruin warden, cinder/frost striders, ++ [x] **All 10 mobs** — xertz stalker, quartz crawler, greenling, ruin warden, cinder/frost striders, 3 terraform livestock, alien villager (full Merchant trading + reputation). Models, renderers, glow layers, sounds, `village` trade tables. -- [x] **Planet dimensions** — Greenxertz / Cindara / Glacira / Station (datapack data + `space` ++ [x] **Planet dimensions** — Greenxertz / Cindara / Glacira / Station (datapack data + `space` dimension_type + planet biomes that spawn the mobs and generate the ores). -- [x] **Overworld nerosium ore** worldgen (NeoForge biome modifier + Fabric biome API). ++ [x] **Overworld nerosium ore** worldgen (NeoForge biome modifier + Fabric biome API). --- ## 🚧 Remaining subsystems ### Rockets & travel (`rocket/` 11 + client + items) — **DONE (4 cells green); item-cap proxy deferred** -- [x] `RocketTier`, `Destinations` (ported; `Tuning` values inlined as identity-multiplier base values). -- [~] `RocketEntity` — rebuilt on the cross-loader `FluidTank` + a plain `SimpleContainer(1)` intake + + ++ [x] `RocketTier`, `Destinations` (ported; `Tuning` values inlined as identity-multiplier base values). ++ [~] `RocketEntity` — rebuilt on the cross-loader `FluidTank` + a plain `SimpleContainer(1)` intake + vanilla `ServerPlayer.teleportTo`. **Per-station selection DONE:** `DATA_STATION` synced slot (−1 = origin), `cycleStation()` cycles origin → each founded station (founding order) → origin via `StationRegistry`, and `completeLaunch()` docks the rider at the selected station's `center()` (else the origin platform); the slot persists in `addAdditionalSaveData` (`StationSlot`). **Deferred:** the NeoForge-transfer entity item-capability **automation proxy** (pipe/hopper → docked rocket). Risk: travel/teleport unverifiable headlessly — compile-verified only. -- [x] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen`. Menu is **non-extended** (no loader-divergent ++ [x] `RocketItem` ×4 tiers, `RocketMenu` + `RocketScreen`. Menu is **non-extended** (no loader-divergent extended-menu API); buttons route via `clickMenuButton`. **Station selection DONE:** `BUTTON_CYCLE_STATION` + a synced `[5]=stationSlot` data value + a `RocketScreen` "Dock:" cycler shown only when the Orbital Station is the chosen destination. **Real charter names** ride a small clientbound `StationSyncPayload` (slot→name parallel arrays, POPIA-clean — no player identity) pushed when the player opens a rocket and cached in `client/ClientStations`; the cycler shows the live name (falling back to "Station N"/"Origin Platform") since the int-only `ContainerData` can't carry strings. The standalone FOUND row stays dropped — founding is charter-driven. -- [x] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer` (bakes each tier layer directly — no ++ [x] `RocketModel` (+ `RocketT2/T3/T4Model`), `RocketRenderer` (bakes each tier layer directly — no model-layer registry), `RocketRenderState`; entity + item textures copied. -- [x] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). -- [x] **Station founding DONE (4 cells green).** `StationCoreBlock`(+BE), `StationRegistry` (SavedData, ++ [x] Launch pad / gantry: `RocketLaunchPadBlock`, `LaunchGantryBlock`, `LaunchPadMultiblock` (multiblock gating). ++ [x] **Station founding DONE (4 cells green).** `StationCoreBlock`(+BE), `StationRegistry` (SavedData, POPIA-clean), and a new `StationCharterItem` — right-click the charter to found a station (slot + 7×7 pad + bound Core in the void station dim) and travel there; breaking the Core unregisters + pops the named charter; `guide/station_charter` is code-granted on founding (routes around `ModCriteria`). Founding is **charter-driven** rather than via the rocket FOUND row; the rocket's **per-station selection is now DONE** (see `RocketEntity`/`RocketMenu` above). ### Quarry (`machine/quarry/` 11 + client) — **DONE (4 cells green); modules + BER deferred** -- [x] Area miner ported: `QuarryControllerBlock`(+BE) + `QuarryMenu`/`QuarryScreen`, `QuarryFrameBlock`, + ++ [x] Area miner ported: `QuarryControllerBlock`(+BE) + `QuarryMenu`/`QuarryScreen`, `QuarryFrameBlock`, `QuarryLandmarkBlock`(+BE, client laser ticker), `QuarryRegion`, `MinerTier`, `OutputFilter`, `PlanetMiningProfile`. The dig (landmarks → frame ring → layer-by-layer excavation → drops buffered/ auto-ejected, source fluids sucked) runs server-side; Energy/Item/Fluid caps on both loaders. -- [~] **Chunk-loading**: `QuarryChunkLoader` (NeoForge `TicketController`) replaced by vanilla ++ [~] **Chunk-loading**: `QuarryChunkLoader` (NeoForge `TicketController`) replaced by vanilla `ServerLevel.setChunkForced` (works on both loaders; one chunk pinned at a time, persisted + released on removal) — no cross-loader ticket seam needed. -- [~] **Deferred**: upgrade modules (controller runs at ×1.0 speed/energy, no Silk/Fortune, no module ++ [~] **Deferred**: upgrade modules (controller runs at ×1.0 speed/energy, no Silk/Fortune, no module slots — depends on the `module/` batch); the moving drill-head BER (`QuarryControllerRenderer`); and fluid **auto-eject** (the fluid buffer is drained by pipes instead). `Tuning` values inlined. ### Fuel machines (`machine/Fuel*` — depends on the ported rocket-fuel fluid) — **DONE (4 cells green)** -- [x] `FuelTankBlock`(+BE +menu +screen): stores `rocket_fuel`, accepts buckets/canisters, auto-fuels a + ++ [x] `FuelTankBlock`(+BE +menu +screen): stores `rocket_fuel`, accepts buckets/canisters, auto-fuels a rocket on an adjacent pad (4x on a full 3x3, 12x on a Heavy complex), comparator out. Rebuilt on the shared `FluidTank`; canister slot is a vanilla `WorldlyContainer` (Item cap on both loaders); Fluid cap exposed for pipe filling. Pump FX uses a vanilla sound (root's `ModSounds.FUEL_TANK_PUMP` alias not ported). -- [x] `FuelRefineryBlock`(+BE +menu +screen): coal/charcoal + blaze powder + grid power → liquid ++ [x] `FuelRefineryBlock`(+BE +menu +screen): coal/charcoal + blaze powder + grid power → liquid `rocket_fuel` over a work cycle; Energy (insert-only) + Fluid (extract) + Item caps on both loaders. Rebuilt on `EnergyBuffer` + `FluidTank` + a vanilla `WorldlyContainer`; `Tuning` values inlined. Assets (textures, models, blockstates, loot, recipes) + 4 lang keys copied. ### Atmosphere / terraforming (`world/Oxygen*`, `world/Terraform*`, `machine/Terraform*`, `HydrationModule`) -- [~] **Oxygen survival core DONE (4 cells green)** — `OxygenManager` (per-player O2 drain/suffocate/refill, + ++ [~] **Oxygen survival core DONE (4 cells green)** — `OxygenManager` (per-player O2 drain/suffocate/refill, air-supply-bar mirror, full-suit detection) on a new **data-attachment seam**: `IPlatformHelper.get/setOxygen` backed by NeoForge `AttachmentType` (`NeoForgeAttachments`) and Fabric `AttachmentRegistry` (`FabricAttachments`); ticked per-loader (NeoForge `PlayerTickEvent`, Fabric `ServerTickEvents.END_SERVER_TICK`). Breathable = the diffusion field **or** near a Launch Pad (safe-zone radius). -- [x] **Oxygen diffusion field — server half DONE (4 cells green).** `world/{OxygenField (tag-based - sealing classifier — `OXYGEN_SEALING`/`OXYGEN_LEAKS`, doors/trapdoors, full-cube fallback), ++ [x] **Oxygen diffusion field — server half DONE (4 cells green).** `world/{OxygenField (tag-based + sealing classifier —`OXYGEN_SEALING`/`OXYGEN_LEAKS`, doors/trapdoors, full-cube fallback), OxygenFieldManager (SavedData; sparse fastutil concentration field + source set; per-pass flood-fill detects sealed-vs-leaky/open volumes → sealed rooms fill to MAX, open space pressurises only a bubble; - slow evaporation), OxygenFieldEvents (cross-loader `tick(MinecraftServer)`, throttled sim pass)}`. + slow evaporation), OxygenFieldEvents (cross-loader`tick(MinecraftServer)`, throttled sim pass)}`. Wired into both server-tick hooks alongside the meteor driver; `OxygenManager.isBreathable` now reads the field; the **Oxygen Generator registers itself as a field source**, draining `EMIT_MB_PER_TICK` from its tank while sourcing (and clears on `setRemoved`). Sealed bases are now genuinely breathable. ~9 field config keys inlined. -- [x] **Oxygen field client visuals DONE (4 cells green).** `network/OxygenFieldSyncPayload` (range snapshot, ++ [x] **Oxygen field client visuals DONE (4 cells green).** `network/OxygenFieldSyncPayload` (range snapshot, long[]/byte[]) registered clientbound and pushed from `OxygenFieldEvents` every 10t to nearby players; `client/ClientOxygenField` (data holder) + `client/ClientOxygenVisuals` (client-tick: drifting GLOW particles in breathable cells + a boundary-crossing sound). 2nd networking-seam consumer. **Deferred: the haze fog-tint layer** — rode a NeoForge-only `ViewportEvent.ComputeFogColor` with no portable Fabric counterpart. -- [x] **Hazard shields DONE (4 cells green).** `OxygenManager` now applies a per-planet hazard (Cindara HEAT / ++ [x] **Hazard shields DONE (4 cells green).** `OxygenManager` now applies a per-planet hazard (Cindara HEAT / Glacira COLD): ×4 oxygen drain unless a full set of the matching `HazardShield` suit variant is worn (mixed set = no shield). Adds `hazardFor`/`hazardShield`/`pieceVariant`/`hazardDrainMultiplier` + thematic feedback (frost vignette on cold, smoke shimmer on hot — no extra damage path). **Makes the already-ported thermal/cryo suit variants functional.** -- [ ] **Deferred**: terraform-breathability advancement criteria, gas-tank airlock refill (needs the gas-cap ++ [ ] **Deferred**: terraform-breathability advancement criteria, gas-tank airlock refill (needs the gas-cap lookup; the field/pad/terraformed already refill). -- **Terraforming** (signature endgame) — sliced; **slice 1 DONE (4 cells green)**, rest sequenced: - - [x] **Slice 1 — per-chunk data-attachment seam.** `IPlatformHelper.is/setTerraformed` + ++ **Terraforming** (signature endgame) — sliced; **slice 1 DONE (4 cells green)**, rest sequenced: + + [x] **Slice 1 — per-chunk data-attachment seam.** `IPlatformHelper.is/setTerraformed` + `get/setTerraformStage(LevelChunk)` backed by NeoForge `AttachmentType` (chunk `getData`/`setData`) and Fabric `AttachmentRegistry` (chunk `getAttachedOrCreate`/`setAttached`) — same registries as the player oxygen attachment, just a `LevelChunk` target (no new API surface). Wired into `OxygenManager.isBreathable` (terraformed chunk ⇒ breathable). Critical-path foundation for everything below. - - [x] **Slice 2 — biome + tag data.** `world/ModBiomes` (4 terraformed `ResourceKey` constants — + + [x] **Slice 2 — biome + tag data.** `world/ModBiomes` (4 terraformed `ResourceKey` constants — the multiloader ships biomes as committed datapack JSON, so no datagen bootstrap needed) + copied the 4 terraformed biome JSON (`terraformed`/`_meadow`/`_savanna`/`_tundra`, feature-free / runtime-written) + copied the 2 terraform block-tag JSON (`TERRAFORM_TO_GRASS`/`_DIRT` — TagKey constants already in `ModTags`). All 4 cells green; JSON python-validated. (Inert until slice 3 consumes them.) - - [x] **Slice 3 — conversion engine.** `machine/TerraformConversion` (staged column conversion: stage 1 + + [x] **Slice 3 — conversion engine.** `machine/TerraformConversion` (staged column conversion: stage 1 Rooted = terrain→grass/dirt via `TERRAFORM_TO_GRASS/DIRT` tags + breathable flag + `TERRAFORMED` biome + plants/ore; stage 2 Hydrated = basin water fill; stage 3 Living = mature biome + trees + herds — stage bookkeeping rewired onto the `Services.PLATFORM` chunk seam), `machine/TerraformResources` (inlined ore list), `world/TerraformFauna` (inlined herd config). Worldgen APIs (`TreeFeatures`, `ConfiguredFeature.place`, `PalettedContainer` biome write, `EntityType.spawn`) all resolve on common. ~7 config/tuning keys inlined. All 4 cells green. (Inert until slice 4's machine + manager drive it.) - - [x] **Slice 4a — TerraformManager + chunk-load seam.** `world/TerraformManager` (3rd `SavedData`, + + [x] **Slice 4a — TerraformManager + chunk-load seam.** `world/TerraformManager` (3rd `SavedData`, 4-arg `SavedDataType`; tracks per-terraformer stage radii; `onChunkLoaded` replays staged conversion on in-range columns of newly-loaded chunks + biome-sync packet). Per-loader chunk-load hook: NeoForge `ChunkEvent.Load` (filter `ServerLevel`+`LevelChunk`), Fabric `ServerChunkEvents.CHUNK_LOAD` (**3-param SAM `(ServerLevel, LevelChunk, boolean newlyGenerated)`** — probed). All 4 cells green. (Inert until 4b.) - - [x] **Slice 4b — Terraformer machine DONE (4 cells green).** `machine/{MachineRedstone, TerraformerBlock, + + [x] **Slice 4b — Terraformer machine DONE (4 cells green).** `machine/{MachineRedstone, TerraformerBlock, TerraformerBlockEntity}` + `menu/TerraformerMenu` + `client/TerraformerScreen`. BE rewritten onto `EnergyBuffer` + a vanilla `WorldlyContainer`/`NonNullList` upgrade slot (dropped NeoForge `SimpleEnergyHandler`/`MachineItemHandler`/`ResourceHandler`); **force-load deferred** (unloaded columns @@ -360,52 +368,54 @@ checked by a headless build). `TerraformManager.update` + biome-sync packet. Registered block/item/BE/menu + per-loader screen + energy/item caps; copied block (3 textures, FACING blockstate, multi-tex model) + GUI texture + 9 lang keys. **Placing a Terraformer now greens the planet outward (Rooted→Hydrated→Living).** - - [x] **Slice 5 — Hydration Module DONE (4 cells green).** `machine/{HydrationModuleBlock, + + [x] **Slice 5 — Hydration Module DONE (4 cells green).** `machine/{HydrationModuleBlock, HydrationModuleBlockEntity}` + `menu/HydrationModuleMenu` + `client/HydrationModuleScreen`. Melts glacite (the `hydration_input` tag) from a `WorldlyContainer`/`NonNullList` slot into a TOUCHING Terraformer's hydration buffer (`acceptHydration`); no energy of its own. Registered block/item/BE/menu + per-loader screen + item cap; copied block (3 tex, FACING blockstate, model) + GUI + loot table + `hydration_input` tag JSON + 5 lang keys. **Also fixed: the Terraformer block was missing its loot table from 4b (added).** - - [x] **Slice 6a — Terraform Monitor DONE (4 cells green).** `machine/{TerraformMonitorBlock, + + [x] **Slice 6a — Terraform Monitor DONE (4 cells green).** `machine/{TerraformMonitorBlock, TerraformMonitorBlockEntity}` + `menu/TerraformMonitorMenu` + `client/TerraformMonitorScreen`. Pure readout (no inventory — `MenuProvider` + `ContainerData`): finds the nearest Terraformer via `TerraformManager`, shows stage radii / hydration / stall + the local column's stage on a comparator. Registered + per-loader screen + assets + loot table + 9 lang keys. No caps (no inventory). - - [x] **Slice 6b — `TerraformDrift` DONE (4 cells green).** `world/TerraformDrift` — idle ground-cover + + [x] **Slice 6b — `TerraformDrift` DONE (4 cells green).** `world/TerraformDrift` — idle ground-cover garnish on settled terraformed land, near players, on a per-second budget; cross-loader `tick(MinecraftServer)` wired into both server-tick hooks (alongside meteor + oxygen-field). Config inlined. - - [ ] **Remaining (optional, low value):** `TerraformChunkLoader` (the deferred opt-in active force-loader — + + [ ] **Remaining (optional, low value):** `TerraformChunkLoader` (the deferred opt-in active force-loader — needs a chunk-force-ticket seam; off by default so the chunk-load catch-up covers it). `world/GreenxertzAtmosphere` is **NOT terraforming** — it's the root's full oxygen-survival class, already superseded by the ported `OxygenManager` + diffusion field + terraformed-flag. Its only unported extras (hazard shields heat/cold, gas-tank airlock refill) are a separate **oxygen enhancement**, tracked below. ### Structures (`world/*Feature`, `village/VillageCore*`, station core, `ModFeatures`) — **DONE (4 cells green)** -- [x] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + `ModFeatures` + ++ [x] `HamletFeature`, `MegaCityFeature`, `RuinFeature`, `AlienBuild`, `StructureSpacing` + `ModFeatures` (registers the 3 `Feature` types via `RegistrationProvider` over `FEATURE`). Copied the configured/placed-feature JSON and **re-added the 3 placed features to the Greenxertz biome JSON** (`greenxertz.json` feature step 6) — since Greenxertz is our own datapack biome, no biome-modifier seam needed. Mega-city spawns the (ported) Ruin Warden boss; ruin/mega-city fill vanilla loot chests. -- [~] `VillageCoreBlock` — ported as a **plain decorative centerpiece block** (the structures' anchor). ++ [~] `VillageCoreBlock` — ported as a **plain decorative centerpiece block** (the structures' anchor). The interactive controller (`VillageCoreBlockEntity`: claim → teach-and-grow construction, fetch quests, night raids) is **deferred** — it pulls in `VillageBuildings` + the config seam. ### Meteor events (`meteor/` 8 + client) -- [x] **Creative slice** — `FallingMeteorEntity` (+ `FallingMeteorModel`/`FallingMeteorRenderer`/ + ++ [x] **Creative slice** — `FallingMeteorEntity` (+ `FallingMeteorModel`/`FallingMeteorRenderer`/ `FallingMeteorRenderState`, bake-direct), `MeteorCallerItem` (creative-only), `MeteorCoreBlock`(+BE, break-to-loot), `MeteorLoot`. Meteor Caller → falling meteor → crater of `meteor_rock` around a loot-bearing `meteor_core`. `METEOR_ROCK` + loot items (`alien_*`, raw ores) already existed; added `FALLING_METEOR` entity, `METEOR_CORE` block+BE (no block item — world-gen only), `METEOR_CALLER` item (TOOLS tab) + renderer; copied 3 textures + 4 asset JSON + 4 lang keys. Config meteor keys inlined (crater radius 3, bonus rolls 3). All 4 cells green. -- [x] **Natural showers (scheduler)** — `MeteorSite` + `MeteorEventManager` (the multiloader's first ++ [x] **Natural showers (scheduler)** — `MeteorSite` + `MeteorEventManager` (the multiloader's first `SavedData`) + cross-loader `MeteorEvents.tick(MinecraftServer)` driving the per-level scheduler on the 4 surface dims (overworld + Greenxertz + Cindara + Glacira); wired into NeoForge `ServerTickEvent.Post` and Fabric `END_SERVER_TICK`; `FallingMeteorEntity` re-wired to call `onImpact`. Meteor pacing inlined (avg 9000s, warn 30s, 200–500 blocks, ≤4 active). **26.x gotcha: `SavedDataType` on pure-vanilla NeoForm has only the 4-arg ctor `(Identifier, Supplier, Codec, DataFixTypes)`** — the standalone mod's 3-arg call is a NeoForge convenience; pass `null` DataFixTypes (new mod data, no datafixer schema). All 4 cells green. -- [x] **Tracker HUD** — `ModItems.METEOR_TRACKER` item + `network/MeteorSyncPayload` (the multiloader's ++ [x] **Tracker HUD** — `ModItems.METEOR_TRACKER` item + `network/MeteorSyncPayload` (the multiloader's **first networking payload** — registered clientbound in `ModNetwork.init()`, auto-wired by both loader seams) pushed to tracker holders every 10t from `MeteorEvents` + `client/ClientMeteorTracker` (data holder) + `client/MeteorTrackerHud` (action-bar readout via `Player.sendOverlayMessage`) driven by @@ -414,11 +424,12 @@ checked by a headless build). use `Player.sendOverlayMessage(Component)`** (probed). Proves the networking seam end-to-end. All 4 cells green. ### Star Guide / progression (`progression/` 5 + client + item) — **slice 1 DONE (4 cells green)** -- [x] **Slice 1 — browsable guide.** `progression/{StarGuide, StarGuideProgress, StarGuideBlock, + ++ [x] **Slice 1 — browsable guide.** `progression/{StarGuide, StarGuideProgress, StarGuideBlock, StarGuideBlockEntity, StarGuideMenu}` + `item/StarGuideBookItem` + `client/StarGuideScreen`. Registered block/block-item/book/BE/menu + per-loader screen + assets + 98 lang keys. Opens from the book (in hand) or the pedestal (install the book). Reads advancement completion — **no `ModCriteria` dependency**. -- [x] **Slice 2a — advancement DATA DONE (4 cells green).** Copied all 42 nerospace advancements; **39 use ++ [x] **Slice 2a — advancement DATA DONE (4 cells green).** Copied all 42 nerospace advancements; **39 use pure vanilla triggers** (`inventory_changed` / `changed_dimension` / `bred_animals`) and track real completion immediately. The **3 custom-trigger ones** (`terraformed_ground`/`living_world`/`station_charter`, which need the deferred `ModCriteria` whose `PlayerTrigger` base moved packages 26.1↔26.2) were rewritten to @@ -426,7 +437,7 @@ checked by a headless build). are not orphaned) — they display but stay incomplete until granted. Repointed 2 display icons off unported items (`station_charter`→`station_floor`, `new_life`→`meadow_loper_spawn_egg`). **The guide now tracks live progress.** All JSON parse-validated; item predicates + the 4 `changed_dimension` targets all resolve. -- [x] **Slice 2b — hologram BER DONE (4 cells green).** Added a reusable cross-loader BER seam ++ [x] **Slice 2b — hologram BER DONE (4 cells green).** Added a reusable cross-loader BER seam `client/ClientBlockEntityRenderers` (`Sink` mirrors `ClientEntityRenderers` — NeoForge `RegisterRenderers.registerBlockEntityRenderer`, Fabric `BlockEntityRendererRegistry.register`) + `client/{StarGuideHologramRenderer, StarGuideHologramRenderState}` (verbatim 26.x BER submission). The @@ -434,50 +445,53 @@ checked by a headless build). type params ``** (probed via build error) — the Sink carries both. The seam now unblocks future BERs (solar sun-tracking, quarry drill, etc.). (Fabric `BlockEntityRendererRegistry` is soft-deprecated — works; a later switch to vanilla `BlockEntityRenderers.register` is optional.) -- [x] **Slice 2c — seen-pulse DONE (4 cells green).** Added a `List` `STAR_GUIDE_SEEN` player ++ [x] **Slice 2c — seen-pulse DONE (4 cells green).** Added a `List` `STAR_GUIDE_SEEN` player attachment through the existing data-attachment seam (`IPlatformHelper.get/setStarGuideSeen` + `NeoForgeAttachments`/`FabricAttachments`, `Codec.INT.listOf()`, copy-on-death). Restored the menu's seen masks (`DATA_COUNT = CHAPTER_COUNT*2`, `clickMenuButton` marks seen via `Services.PLATFORM`) + the screen's completed-but-unseen pulse (clicking a step acknowledges it). **The Star Guide is now feature-complete** (browse + live progress + hologram + seen-pulse). 26.x gotcha: NeoForge `AttachmentType.builder(...)` is overloaded, so the default must be a lambda `() -> List.of()` (not the `List::of` method ref — ambiguous). -- [x] **Slice 2d — terraform advancements code-granted (4 cells green).** `progression/StarGuideGrants` ++ [x] **Slice 2d — terraform advancements code-granted (4 cells green).** `progression/StarGuideGrants` (driven from the per-player server tick, beside `OxygenManager.tick`) awards the impossible-criterion `guide/terraformed_ground` (chunk stage ≥ 1) and `guide/living_world` (stage ≥ 3) directly when the player stands on terraformed / fully-living ground — replicating the standalone mod's `PlayerTrigger` **without** `ModCriteria`. **41 of 42 advancements now track real completion.** 26.x: award via `getOrStartProgress(holder).getRemainingCriteria()` → `PlayerAdvancements.award(holder, criterion)`. -- [x] **Slice 2e — DONE via station founding.** `guide/station_charter` is now code-granted when a station is ++ [x] **Slice 2e — DONE via station founding.** `guide/station_charter` is now code-granted when a station is founded (the charter item), and its Star-Guide step + advancement icons point at the now-real `station_charter` item. **All 42 advancements track real completion.** Only the `new_life` guide-step icon stays substituted (Meadow Loper spawn egg) until `LOPER_HAUNCH` is ported — purely cosmetic. ### Pipes — advanced (`pipe/` + items + payload + renderer; basic pipe already ported) — **slice A DONE (4 cells green)** -- [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla + ++ [x] **Slice A — per-face configuration layer.** `pipe/PipeIoMode` + `pipe/PipeResourceType` (vanilla enums); `item/{ConfiguratorItem, PipeFilterItem (vanilla ItemStack filter), PipeUpgradeItem ×2}`. `UniversalPipeBlockEntity` extended with per-face×per-type modes (packed long) + per-face item filters + speed/capacity upgrades; the energy/gas/item relay honours `canPull`/`canPush`/`OFF` + filters + speed throughput; `UniversalPipeBlock` sneak-empty-hand pops upgrades. Items registered (TOOLS tab) + assets + 20 lang keys. -- [x] **Fluid relay** — added the `platform/FluidLookup` query seam (common + both loaders + services) and a ++ [x] **Fluid relay** — added the `platform/FluidLookup` query seam (common + both loaders + services) and a `FluidTank` + `relayFluid()` to the pipe BE (honours the FLUID face-mode + speed); the pipe's fluid handler is exposed as the FLUID cap on both loaders. The pipe now carries all four layers; the FLUID face-mode is live (e.g. Refinery → pipe → Fuel Tank). -- [ ] **Slice B (deferred) — graph + visuals + GUI.** `PipeNetwork` (591-line graph; NeoForge-transfer- ++ [ ] **Slice B (deferred) — graph + visuals + GUI.** `PipeNetwork` (591-line graph; NeoForge-transfer- coupled), `TravellingItem` (animated stacks; ItemResource→ItemStack), `UniversalPipeRenderer` + `UniversalPipeRenderState` (stream + travelling-item visuals), `PipeConfigScreen` + `PipeConfigOpenHandler` + `network/SetPipeModePayload` (the per-face×per-type config GUI — needs a cross-loader client-screen-open seam). Cosmetic / convenience; the slice-A in-world cycling already configures pipes fully. ### Machine modules / upgrades (`module/` 3) — **DONE (4 cells green)** -- [x] `ModuleType`, `UpgradeModuleItem` (4 items: speed / efficiency / fortune / silk-touch) + `MachineModules` + ++ [x] `ModuleType`, `UpgradeModuleItem` (4 items: speed / efficiency / fortune / silk-touch) + `MachineModules` (rebuilt on a `NonNullList` instead of the root's `MachineItemHandler`). **Re-enabled in the quarry**: module slots restored in the controller's combined `WorldlyContainer` view + `QuarryMenu`, and the speed / energy / Silk-Touch / Fortune multipliers now drive the dig (the quarry's earlier `×1.0` deferral is resolved). Assets + 4 lang keys copied. ### Solar — tiers/array/BER (`machine/Solar*`) — **slice 1 DONE (4 cells green); multiblock + BER deferred** -- [x] **Tiers + array pooling DONE.** `SolarTier` (T1/T2/T3, config-scaled FE/buffer via `NerospaceConfig`) + ++ [x] **Tiers + array pooling DONE.** `SolarTier` (T1/T2/T3, config-scaled FE/buffer via `NerospaceConfig`) + `SolarArray` (flood-fill same-tier pooling, rebalanced each tick so a pipe on ANY panel drains the whole run) + tier-aware `SolarPanelBlock` (comparator output) + `SolarPanelBlockEntity` rebuilt on the multiloader `EnergyBuffer` (the NeoForge transfer `SimpleEnergyHandler` isn't ported). `solar_panel` @@ -486,33 +500,36 @@ checked by a headless build). no per-loader change. Daylight uses vanilla `getSkyDarken()` (the NeoForge dimension clock / `getDayTime()` / `LevelData.getDayTime()` aren't on the de-obf classpath); airless dims get the 2× sun bonus via `ModDimensions` keys. Assets: tier textures copied from root + hand-authored block/item/loot JSON; 2 lang keys. -- [~] **Deferred (slice 2):** the N×N multiblock footprint (every tier is 1×1 for now — `SolarTier.footprint` ++ [~] **Deferred (slice 2):** the N×N multiblock footprint (every tier is 1×1 for now — `SolarTier.footprint` is carried but unused for placement) and the tilting sun-tracking deck renderer (the BER seam is ready). ### Creative storage variants (`storage/Creative*`) — **DONE (4 cells green)** -- [x] `AbstractStorageBlock` (shared base) + `CreativeFluidTank` (endless rocket_fuel), `CreativeGasTank` + ++ [x] `AbstractStorageBlock` (shared base) + `CreativeFluidTank` (endless rocket_fuel), `CreativeGasTank` (endless oxygen), `CreativeItemStore` (right-click to set an endless item source). Fluid/gas mirror the ported `CreativeBattery`'s infinite pattern on the cross-loader storage interfaces; the item store exposes its endless source through a vanilla `Container` (no NeoForge `InfiniteResourceHandler`). Fluid/Gas/Item caps wired on both loaders; assets + lang copied. ### Utility items (`item/`) — **partly DONE (4 cells green)** -- [x] `NerospaceSpawnEggItem` (+ **9 spawn eggs**: xertz stalker, quartz crawler, greenling, alien + ++ [x] `NerospaceSpawnEggItem` (+ **9 spawn eggs**: xertz stalker, quartz crawler, greenling, alien villager, cinder stalker, frost strider, meadow loper, ember strutter, woolly drift — ruin warden is summon-only). Lazy `EntityType` supplier (vanilla `SpawnEggItem` binds too early); SPAWN_EGGS tab. -- [x] `DestinationCompassItem` (×4: station/greenxertz/cindara/glacira) + `GreenxertzNavigatorItem` — ++ [x] `DestinationCompassItem` (×4: station/greenxertz/cindara/glacira) + `GreenxertzNavigatorItem` — creative-only travel devices; TOOLS_AND_UTILITIES tab. Assets + 17 lang keys copied. -- [x] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` — DONE (advanced-pipes slice A; TOOLS tab). -- [ ] `StarGuideBookItem` (depends on **star guide**). -- [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. ++ [x] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` — DONE (advanced-pipes slice A; TOOLS tab). ++ [ ] `StarGuideBookItem` (depends on **star guide**). ++ [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. ### Cross-cutting registries (`registry/`) -- [x] `ModTags` — pure `TagKey` constants (block + item; c:material + nerospace oxygen/terraform tags), + ++ [x] `ModTags` — pure `TagKey` constants (block + item; c:material + nerospace oxygen/terraform tags), ported verbatim (no registration; tag membership is data). -- [x] `ModDataComponents` — `SELECTED_PIPE_TYPE` (int) + `FILTER_ITEM` (vanilla `ItemStack` instead of the ++ [x] `ModDataComponents` — `SELECTED_PIPE_TYPE` (int) + `FILTER_ITEM` (vanilla `ItemStack` instead of the root's NeoForge `ItemResource`), via `RegistrationProvider` over `DATA_COMPONENT_TYPE`. Consumed by the advanced-pipe configurator/filter (advanced pipes batch). -- [~] `ModCriteria` (`terraformed_ground`/`living_ground`/`founded_station` `PlayerTrigger`s) — **deferred: ++ [~] `ModCriteria` (`terraformed_ground`/`living_ground`/`founded_station` `PlayerTrigger`s) — **deferred: confirmed cross-version vanilla package move** (probed 2026-06-21): on **26.1.2** the classes are `net.minecraft.advancements.CriterionTrigger` + `net.minecraft.advancements.criterion.PlayerTrigger`; on **26.2** both are under `net.minecraft.advancements.triggers`. A single shared `import` can't satisfy both @@ -520,11 +537,11 @@ checked by a headless build). star guide / terraform) lands: (a) drop the custom advancement triggers (they're cosmetic — the systems work without firing them); (b) reflection (resolve `PlayerTrigger` by per-version FQN); or (c) add version-split source sets. Orphan until then. -- [ ] `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge attachments vs Fabric ++ [ ] `ModAttachments` (data attachments — needs a cross-loader seam: NeoForge attachments vs Fabric component/attachment API), `ModFeatures`, `ModConfiguredFeatures`/`ModPlacedFeatures`/`ModBiomes`/ `ModBiomeModifiers` (datagen bootstraps — mostly superseded by the copied JSON), `ModDimensionTypes` (space type — JSON already copied). -- [x] `ModCreativeModeTabs` → ported as `ModCreativeTab`: a **dedicated "Nerospace" tab** registered via ++ [x] `ModCreativeModeTabs` → ported as `ModCreativeTab`: a **dedicated "Nerospace" tab** registered via the cross-loader `RegistrationProvider` over the vanilla `CREATIVE_MODE_TAB` registry, listing all items (`ModItems.creativeContents()`). **Fixes a latent runtime bug**: the earlier per-loader injection into vanilla tabs (`BuildCreativeModeTabContentsEvent` / `CreativeModeTabEvents`) never populated the @@ -532,7 +549,8 @@ checked by a headless build). `CreativeModeTab.builder(Row, column)` (the no-arg overload + `withTabsBefore` are NeoForge-only). ### Networking (`network/` 5) — **SEAM DONE (4 cells green); payloads ship with their consumers** -- [x] Cross-loader packet seam: common `network/ModNetwork` (payload registry: `clientbound`/`serverbound` + ++ [x] Cross-loader packet seam: common `network/ModNetwork` (payload registry: `clientbound`/`serverbound` lists + `sendToPlayer`/`sendToServer`) + `platform/NetworkPlatform` send seam. NeoForge `NeoForgeNetwork` registers via `RegisterPayloadHandlersEvent` (`playToClient`/`playToServer`) and sends via `PacketDistributor.sendToPlayer` / **`ClientPacketDistributor.sendToServer`** (client-only). Fabric @@ -543,27 +561,29 @@ checked by a headless build). serverbound(...)`). Client-safety contract documented in `ModNetwork`. ### Commands & compat -- [x] `command/NerospaceCommands` — **DONE (4 cells green).** `/nerospace gallery` [clear] creative showcase + ++ [x] `command/NerospaceCommands` — **DONE (4 cells green).** `/nerospace gallery` [clear] creative showcase builder, behind a cross-loader `register(CommandDispatcher)` seam (NeoForge `RegisterCommandsEvent`, Fabric `CommandRegistrationCallback`). Cross-loader/version adaptations: iterate `BuiltInRegistries.BLOCK` filtered to the mod namespace (the `RegistrationProvider` has no entry iteration); single `SOLAR_PANEL` (tiers unported); spawn the armor stands via the `ArmorStand` constructor (the de-obf `EntityType.ARMOR_STAND` constant isn't on the 26.2 classpath); dropped the unported `quarry.stageDisplay` preview + the Creative Fluid Tank `setSource` (fixed rocket_fuel here). -- [ ] `compat/jei/*` — recipe-viewer integration. NeoForge = JEI; Fabric would use REI/EMI. Cross-mod, low priority. ++ [ ] `compat/jei/*` — recipe-viewer integration. NeoForge = JEI; Fabric would use REI/EMI. Cross-mod, low priority. ### Config / tuning — **DONE (4 cells green): all 5 multipliers wired, cross-loader seam complete** -- [x] **Slice 1 — config seam + energy multiplier.** Extended the properties-based `config/NerospaceConfig` + ++ [x] **Slice 1 — config seam + energy multiplier.** Extended the properties-based `config/NerospaceConfig` (no NeoForge `ModConfigSpec` — the cross-loader seam is the properties file the telemetry batch added) with `energyRateMultiplier` (clamp 0.1×..10×, default 1) + a `scale(base, mult)` helper (min-1 clamp, mirroring the root `Tuning` contract); wired it into the Combustion / Passive / Solar generator FE-per-tick. Loads at mod init (before ticking). This proves the cross-loader balance-config pattern beyond the telemetry toggle. -- [x] **Slice 2 — oxygen multipliers.** Added `oxygenDrainMultiplier` + `oxygenCapacityMultiplier` to ++ [x] **Slice 2 — oxygen multipliers.** Added `oxygenDrainMultiplier` + `oxygenCapacityMultiplier` to `NerospaceConfig`; wired into `OxygenManager` (per-check drain + player/suit air capacity, both `scale`-clamped; the attachment default self-corrects on the first tick). **3 of the root's 5 multipliers now wired.** -- [x] **Slice 3 — fuelCostMultiplier.** Added `fuelCostMultiplier`; wired into `RocketTier.fuelPerLaunch()` ++ [x] **Slice 3 — fuelCostMultiplier.** Added `fuelCostMultiplier`; wired into `RocketTier.fuelPerLaunch()` (scaled, still clamped to the tank so a launch is always possible). **4 of the root's 5 multipliers wired.** -- [x] **Slice 4 — machineSpeedMultiplier (last multiplier; config seam COMPLETE).** Added `machineSpeedMultiplier` ++ [x] **Slice 4 — machineSpeedMultiplier (last multiplier; config seam COMPLETE).** Added `machineSpeedMultiplier` + a `scaleInterval` helper (inverse, clamped ≥1 tick); wired into the grinder + refinery (progress thresholds, both the completion check and the synced max-progress data), the hydration module + terraformer (modulo work intervals), and the quarry (folded into the mining rate). **All 5 of the root's balance multipliers are now @@ -572,7 +592,8 @@ checked by a headless build). class is optional). ### Spawn rules -- [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures + ++ [x] `registry/ModSpawnPlacements` — natural-spawn placement rules for the 9 spawnable creatures (6× `ON_GROUND` light-independent; 3× terraform livestock gated on `GRASS_BLOCK`). Cross-loader spawn-placement seam (`ModSpawnPlacements.Sink`): NeoForge `RegisterSpawnPlacementsEvent` (`Operation.REPLACE`) vs Fabric vanilla `SpawnPlacements.register`. Both stable on 26.1.2 + 26.2. @@ -581,37 +602,39 @@ checked by a headless build). --- ## 📡 Sentry / telemetry (`telemetry/`) — **POPIA/GDPR-sensitive** — DONE (4 cells green) -- [x] `telemetry/NerospaceTelemetry` — the Sentry client: captures Nerospace exceptions/crashes, with + ++ [x] `telemetry/NerospaceTelemetry` — the Sentry client: captures Nerospace exceptions/crashes, with **PII scrubbing** (no IP/identity/hostname; OS-account names scrubbed from file paths via the `USER_PATH` regex incl. `C:\Users\\...`), **nerospace-only `beforeSend` filter**, **de-dup + 10/session cap**. Parameterised off `Services.PLATFORM` (mod version, loader name, dist) instead of FML. -- [x] `telemetry/SentryLogAppender` — Log4j2 appender selecting ERROR/FATAL events touching Nerospace code. -- [x] `config/NerospaceConfig` — minimal properties config (`config/nerospace.properties`); **`telemetryEnabled` ++ [x] `telemetry/SentryLogAppender` — Log4j2 appender selecting ERROR/FATAL events touching Nerospace code. ++ [x] `config/NerospaceConfig` — minimal properties config (`config/nerospace.properties`); **`telemetryEnabled` default ON, opt-out** (user decision 2026-06-21). Config-dir via new `IPlatformHelper.getConfigDir()` seam. -- [x] **Sentry SDK bundled per-loader** — common `compileOnly`, NeoForge `jarJar`, Fabric Loom `include` ++ [x] **Sentry SDK bundled per-loader** — common `compileOnly`, NeoForge `jarJar`, Fabric Loom `include` (both bundling tasks ran green). `NerospaceTelemetry.init()` called at each loader's bootstrap; **only initialises in a production (non-dev) environment**. -- [ ] **Deferred**: `SentryTestBlock` (debug block) — minor dev tool. -- ⚠️ **Runtime-verify on a shipped build**: the 4-cell compile + the jarJar/include tasks are green, but ++ [ ] **Deferred**: `SentryTestBlock` (debug block) — minor dev tool. ++ ⚠️ **Runtime-verify on a shipped build**: the 4-cell compile + the jarJar/include tasks are green, but Sentry initialisation, the nerospace-only filter, and path scrubbing have NOT been runtime-tested here (dev-gated + sandbox mount lag). Confirm on a production jar before relying on it. DSN = root's EU ingest. --- ## 🛠️ Tools / sync engines (`tools/`) — currently target the **root** mod only + These are dev-time generators, not shipped code. They write to the root's `src/main/resources` paths, so they must be pointed at (or duplicated for) `multiloader/common/src/main/resources` to drive the multiloader's assets instead of the current copy-from-root approach. -- [ ] `model_sync.py` — **entity-model sync engine** (Blockbench `.bbmodel` ⇄ Java `LayerDefinition`, ++ [ ] `model_sync.py` — **entity-model sync engine** (Blockbench `.bbmodel` ⇄ Java `LayerDefinition`, Y-flip, mtime-directional). Wire to the multiloader's `client/*Model.java` + `art/blockbench/entity`. -- [ ] `gen_textures.py` — procedural 16×16 texture generator (additive). Repoint output dir. -- [ ] `gen_bbmodels.py` — Blockbench source generator for block/item textures. Repoint. -- [ ] `gen_logo.py` — CurseForge logo + in-game mods-list icon. Repoint / re-emit per loader. -- [ ] `check_assets.py` — "every model resolves" validator. Repoint at the multiloader resource roots. -- [ ] `render_contact_sheets.py` / `render_entity_previews.py` — QA atlases. Repoint. -- [x] `gradle-mcp` (server.js) — the agent build server; already used to verify all 4 cells. -- [x] `fix_markdown.py` / `markdown_check` — docs linting; loader-agnostic. ++ [ ] `gen_textures.py` — procedural 16×16 texture generator (additive). Repoint output dir. ++ [ ] `gen_bbmodels.py` — Blockbench source generator for block/item textures. Repoint. ++ [ ] `gen_logo.py` — CurseForge logo + in-game mods-list icon. Repoint / re-emit per loader. ++ [ ] `check_assets.py` — "every model resolves" validator. Repoint at the multiloader resource roots. ++ [ ] `render_contact_sheets.py` / `render_entity_previews.py` — QA atlases. Repoint. ++ [x] `gradle-mcp` (server.js) — the agent build server; already used to verify all 4 cells. ++ [x] `fix_markdown.py` / `markdown_check` — docs linting; loader-agnostic. > Note: so far the multiloader reuses the root's already-generated JSON/textures by copying them. The > tools only need porting if the multiloader becomes the source of truth (i.e. when the root mod is retired). @@ -619,6 +642,7 @@ multiloader's assets instead of the current copy-from-root approach. --- ## Recommended order + rockets → fuel machines → quarry → atmosphere/terraforming → structures → meteor events → star guide → advanced pipes → modules → networking seam (unblocks oxygen HUD / meteors / pipe modes) → config seam → spawn rules → telemetry (after compliance sign-off) → creative variants / utility items / JEI → tools repoint. From 58f9466d90a6fd4bc87b7331ded91d1bfe448379 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:07:00 +0200 Subject: [PATCH 78/82] Add ecjCheck task to run Eclipse analyzer Add an ECJ-based verification task (ecjCheck) to the NeoForge Gradle build. This creates an 'ecj' configuration and dependency on org.eclipse.jdt:ecj, and registers a JavaExec task that runs the Eclipse compiler/IDE analyzer over the common + neoforge main sources using the repo-root tools/ecj.prefs. The task assembles an analysis classpath from sourceSets, reports IDE-style diagnostics (null analysis, unused members, unnecessary suppressions, etc.) to the log, and only fails on real errors; it depends on compileJava and is marked incompatible with the configuration cache. --- multiloader/neoforge/build.gradle | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle index 312f902..1fba922 100644 --- a/multiloader/neoforge/build.gradle +++ b/multiloader/neoforge/build.gradle @@ -82,3 +82,38 @@ neoForge { } } } + +// ecjCheck: run the Eclipse compiler (the SAME analyzer VS Code's Java extension uses) over the shared +// common + neoforge main sources, with the repo-root tools/ecj.prefs mirroring the IDE's diagnostic +// settings. Surfaces Problems-panel diagnostics (null analysis, unused members, unnecessary +// suppressions, ...) from the command line / gradle MCP. Warnings are reported in the log; the task only +// FAILS on real errors. The NeoForge module is analysed because its compile classpath resolves the full +// vanilla + NeoForge API (common alone is raw vanilla and won't resolve loader-facing references), and +// its main sourceSet already includes the common sources (srcDir above). Mirrors the repo-root ecjCheck. +configurations { + ecj +} +dependencies { + ecj 'org.eclipse.jdt:ecj:+' +} +tasks.register('ecjCheck', JavaExec) { + group = 'verification' + description = 'Run the Eclipse (IDE) analyzer over common + neoforge main sources and report its diagnostics.' + notCompatibleWithConfigurationCache('assembles the analysis classpath at execution time') + dependsOn tasks.named('compileJava') + classpath = configurations.ecj + mainClass = 'org.eclipse.jdt.internal.compiler.batch.Main' + doFirst { + def analysisCp = (sourceSets.main.compileClasspath.files + sourceSets.main.output.files) + .findAll { it.exists() } + .join(File.pathSeparator) + def srcDirs = sourceSets.main.java.srcDirs.findAll { it.exists() }.collect { it.absolutePath } + args = [ + '--release', '25', + '-proc:none', + '-d', 'none', + '-properties', rootProject.file('../tools/ecj.prefs').absolutePath, + '-classpath', analysisCp, + ] + srcDirs + } +} From a924d5bdab1594d139db39076c8a84f56f11e7ba Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:13:11 +0200 Subject: [PATCH 79/82] Add multiblock solar panel rendering & logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce multiblock solar panels with client-side deck rendering and placement/teardown logic. Adds SolarPanelRenderer and SolarPanelRenderState and registers the renderer in ClientBlockEntityRenderers; the renderer draws a sun-tracking deck (tier-aware, centered on an anchor for N×N tiers) and is compatible cross-loader. SolarPanelBlock gains an ANCHOR boolean property, default state, blockstate variants, and multiblock placement (fills N×N footprint) plus teardown that removes siblings and returns a single item with a reentrancy guard. SolarPanelBlockEntity now supports setAnchor/anchorEntity forwarding so filler cells forward energy to the anchor, and includes isAirless helper used by rendering/solar logic. Update docs checklist to mark the solar slice done. Minor: per-face connector stubs were dropped from the cross-loader port. --- docs/MULTILOADER_PORT_CHECKLIST.md | 13 +- .../client/ClientBlockEntityRenderers.java | 2 + .../client/SolarPanelRenderState.java | 26 +++ .../nerospace/client/SolarPanelRenderer.java | 207 ++++++++++++++++++ .../nerospace/machine/SolarPanelBlock.java | 113 +++++++++- .../machine/SolarPanelBlockEntity.java | 29 ++- .../nerospace/blockstates/solar_panel.json | 5 +- .../nerospace/blockstates/solar_panel_t2.json | 5 +- .../nerospace/blockstates/solar_panel_t3.json | 5 +- 9 files changed, 390 insertions(+), 15 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index a2e02b7..d53ae27 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -489,7 +489,7 @@ checked by a headless build). speed / energy / Silk-Touch / Fortune multipliers now drive the dig (the quarry's earlier `×1.0` deferral is resolved). Assets + 4 lang keys copied. -### Solar — tiers/array/BER (`machine/Solar*`) — **slice 1 DONE (4 cells green); multiblock + BER deferred** +### Solar — tiers/array/BER (`machine/Solar*` + `client/SolarPanel*`) — **DONE (4 cells green)** + [x] **Tiers + array pooling DONE.** `SolarTier` (T1/T2/T3, config-scaled FE/buffer via `NerospaceConfig`) + `SolarArray` (flood-fill same-tier pooling, rebalanced each tick so a pipe on ANY panel drains the @@ -500,8 +500,15 @@ checked by a headless build). no per-loader change. Daylight uses vanilla `getSkyDarken()` (the NeoForge dimension clock / `getDayTime()` / `LevelData.getDayTime()` aren't on the de-obf classpath); airless dims get the 2× sun bonus via `ModDimensions` keys. Assets: tier textures copied from root + hand-authored block/item/loot JSON; 2 lang keys. -+ [~] **Deferred (slice 2):** the N×N multiblock footprint (every tier is 1×1 for now — `SolarTier.footprint` - is carried but unused for placement) and the tilting sun-tracking deck renderer (the BER seam is ready). ++ [x] **Slice 2 DONE — multiblock + sun-tracking BER.** `SolarPanelBlock` gained the `ANCHOR` property + + N×N placement/teardown (T2 2×2, T3 3×3 — clicked min-corner is the anchor, fillers forward their energy + to it via `SolarPanelBlockEntity.getEnergy()` → `anchorEntity()`); blockstates carry `anchor=true|false`. + `client/SolarPanelRenderer` + `SolarPanelRenderState` draw the tilting sun-tracking deck (one big deck per + multiblock, on the anchor) via the BER seam — ported from the root's submission-model geometry + (`submitCustomGeometry` + `RenderTypes.entityCutout` + raw `VertexConsumer`), **compiles on both 26.1.2 and + 26.2**. Cross-loader adaptations: deck angle from vanilla `getGameTime()` (no NeoForge dimension clock), + airless 2× via `SolarPanelBlockEntity.isAirless`. Dropped (minor): the per-face connector stubs (needed + client-side energy-cap queries). The solar subsystem is now feature-complete. ### Creative storage variants (`storage/Creative*`) — **DONE (4 cells green)** diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java index faaec0b..6897976 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java @@ -27,5 +27,7 @@ private ClientBlockEntityRenderers() { public static void registerAll(Sink sink) { // Star Guide pedestal: the floating, spinning next-step hologram above a loaded pedestal. sink.register(ModBlockEntities.STAR_GUIDE.get(), context -> new StarGuideHologramRenderer()); + // Solar panels: the sun-tracking deck above each tier's housing (one big deck per multiblock). + sink.register(ModBlockEntities.SOLAR_PANEL.get(), context -> new SolarPanelRenderer()); } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java new file mode 100644 index 0000000..da8483d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderState.java @@ -0,0 +1,26 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState; + +/** + * Render state for a single solar panel: the tilt angle of its deck (sun-tracking by day, folded flat + * at night), its tier (selects the surface texture), and its footprint + whether this cell is the + * anchor (so a multiblock draws one big deck only on its min-corner cell). + * + *

Cross-loader port: identical to the standalone mod minus the per-face connector stubs (dropped for + * the cross-loader slice — they needed client-side energy-cap queries).

+ */ +public class SolarPanelRenderState extends BlockEntityRenderState { + + /** Surface tilt in degrees about the east-west (Z) axis: 0 = flat (folded / noon), ±tilt by day. */ + public float angle; + + /** 1-based tier (selects the surface texture). */ + public int tier = 1; + + /** Footprint edge length (1 = T1 single tracker, >1 = N×N multiblock — one big deck on a mast). */ + public int footprint = 1; + + /** Whether this cell is its unit's anchor — only the anchor draws the deck. */ + public boolean anchor = true; +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java new file mode 100644 index 0000000..c4235b2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/SolarPanelRenderer.java @@ -0,0 +1,207 @@ +package za.co.neroland.nerospace.client; + +import com.mojang.blaze3d.vertex.PoseStack; +import com.mojang.blaze3d.vertex.VertexConsumer; +import com.mojang.math.Axis; + +import net.minecraft.client.renderer.SubmitNodeCollector; +import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import net.minecraft.client.renderer.rendertype.RenderType; +import net.minecraft.client.renderer.rendertype.RenderTypes; +import net.minecraft.client.renderer.state.level.CameraRenderState; +import net.minecraft.client.renderer.texture.OverlayTexture; +import net.minecraft.resources.Identifier; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; + +import za.co.neroland.nerospace.NerospaceCommon; +import za.co.neroland.nerospace.machine.SolarPanelBlock; +import za.co.neroland.nerospace.machine.SolarPanelBlockEntity; + +/** + * Draws the moving solar-panel deck above its static housing model. Every tier uses the SAME animation: + * a deck that pitches east-west to track the sun and folds flat at night. Tier 1 is a single 1×1 deck on + * the model's pole; Tier 2/3 are N×N multiblocks drawn as ONE big deck on a central mast (only the + * anchor cell renders it), with the tilt scaled down as the footprint grows so the wider deck's + * descending edge never dips below the housings. + * + *

Cross-loader port: the standalone renderer minus the per-face connector stubs (they needed + * client-side energy-cap queries — dropped for this slice). The deck angle is driven by vanilla + * {@code getGameTime()} (the NeoForge dimension clock {@code getDefaultClockTime()} isn't on the de-obf + * classpath) and the airless 2× "permanent sun" case keys off {@link SolarPanelBlockEntity#isAirless}.

+ */ +public class SolarPanelRenderer + implements BlockEntityRenderer { + + /** Pivot height = the cross-bar top (the T-pole). */ + private static final float POLE_TOP = 9.0F / 16.0F; + /** Static housing top (3px) — the floor the deck must stay clear of when tilted. */ + private static final float HOUSING_TOP = 3.0F / 16.0F; + /** Deck thickness: a real 1px slab. */ + private static final float THICK = 1.0F / 16.0F; + /** Max east-west tracking tilt; capped so the deck clears the torque tube. */ + private static final float MAX_TILT = 40.0F; + /** Central-mast dimensions for the multiblock deck. */ + private static final float MAST_HALF = 1.0F / 16.0F; + private static final float POST_TOP = 7.0F / 16.0F; + private static final float TUBE_TOP = 8.0F / 16.0F; + private static final float TUBE_HALF = 4.0F / 16.0F; + + @Override + public SolarPanelRenderState createRenderState() { + return new SolarPanelRenderState(); + } + + @Override + public void extractRenderState(SolarPanelBlockEntity panel, SolarPanelRenderState state, + float partialTick, Vec3 cameraPos, ModelFeatureRenderer.CrumblingOverlay breakProgress) { + BlockEntityRenderer.super.extractRenderState(panel, state, partialTick, cameraPos, breakProgress); + Level level = panel.getLevel(); + if (level == null) { + return; + } + state.tier = panel.tier().tier; + state.footprint = panel.tier().footprint; + state.anchor = panel.getBlockState().getValue(SolarPanelBlock.ANCHOR); + + float openness; + float track; + if (SolarPanelBlockEntity.isAirless(level)) { + openness = 1.0F; // permanent sun in orbit / on an airless moon + track = 0.0F; + } else { + // Vanilla getGameTime() as the day-cycle proxy (the NeoForge dimension clock isn't available + // cross-loader). 0 sunrise, 6000 noon, 18000 midnight. + long tod = (level.getGameTime() + (long) partialTick) % 24000L; + float sun = Mth.cos((float) ((tod - 6000L) / 24000.0 * 2.0 * Math.PI)); // +1 noon, -1 midnight + openness = Mth.clamp((sun + 0.05F) / 0.3F, 0.0F, 1.0F); // eases to 0 at night → folds flat + track = (float) ((tod - 6000L) / 24000.0) * 360.0F; // -90 sunrise .. 0 noon .. +90 sunset + } + state.angle = openness * Mth.clamp(track, -MAX_TILT, MAX_TILT); + } + + @Override + public void submit(SolarPanelRenderState state, PoseStack poseStack, SubmitNodeCollector collector, + CameraRenderState cameraState) { + int light = state.lightCoords; + Identifier texture = Identifier.fromNamespaceAndPath( + NerospaceCommon.MOD_ID, "textures/block/solar_panel" + tierSuffix(state.tier) + ".png"); + + if (state.footprint > 1) { + if (!state.anchor) { + return; // one big panel per multiblock — only the anchor (min-corner) draws the deck + } + submitMultiblockDeck(state, poseStack, collector, light, texture); + return; + } + + // Tier 1: a centred 1×1 deck on the model's T-pole, pitching east-west to follow the sun. + poseStack.pushPose(); + poseStack.translate(0.5F, POLE_TOP, 0.5F); + poseStack.mulPose(Axis.ZP.rotationDegrees(state.angle)); + collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> box(consumer, pose, light, + -0.5F, -THICK / 2.0F, -0.5F, 0.5F, THICK / 2.0F, 0.5F, + 0.0F, 0.0F, 1.0F, 1.0F)); + poseStack.popPose(); + } + + /** + * Tier 2/3: ONE big N×N deck on a central mast, pitching east-west like Tier 1, drawn in the anchor's + * (min-corner) space. The tilt is reduced as the footprint grows ({@link #maxTiltFor}) so the wider + * deck's descending edge clears the housings. + */ + private void submitMultiblockDeck(SolarPanelRenderState state, PoseStack poseStack, + SubmitNodeCollector collector, int light, Identifier texture) { + int n = state.footprint; + float centre = n / 2.0F; + float cap = maxTiltFor(n); + float angle = Mth.clamp(state.angle, -cap, cap); + + // Central mast (post + N-S torque tube) on the `_base` sprite, supporting the deck at the centre. + Identifier baseTexture = Identifier.fromNamespaceAndPath( + NerospaceCommon.MOD_ID, "textures/block/solar_panel" + tierSuffix(state.tier) + "_base.png"); + RenderType baseRt = RenderTypes.entityCutout(baseTexture); + collector.order(1).submitCustomGeometry(poseStack, baseRt, (pose, consumer) -> { + box(consumer, pose, light, centre - MAST_HALF, HOUSING_TOP, centre - MAST_HALF, + centre + MAST_HALF, POST_TOP, centre + MAST_HALF, 0.25F, 0.25F, 0.75F, 0.75F); + box(consumer, pose, light, centre - MAST_HALF, POST_TOP, centre - TUBE_HALF, + centre + MAST_HALF, TUBE_TOP, centre + TUBE_HALF, 0.25F, 0.25F, 0.75F, 0.75F); + }); + + // The single deck, pivoting east-west about the central mast; fills the footprint edge-to-edge. + float half = centre; + poseStack.pushPose(); + poseStack.translate(centre, POLE_TOP, centre); + poseStack.mulPose(Axis.ZP.rotationDegrees(angle)); + collector.order(1).submitCustomGeometry(poseStack, RenderTypes.entityCutout(texture), + (pose, consumer) -> box(consumer, pose, light, + -half, -THICK / 2.0F, -half, half, THICK / 2.0F, half, + 0.0F, 0.0F, 1.0F, 1.0F)); + poseStack.popPose(); + } + + /** Texture suffix per tier: T1 reuses the base "solar_panel" sprite, T2/T3 use "_t2"/"_t3". */ + private static String tierSuffix(int tier) { + return tier <= 1 ? "" : "_t" + tier; + } + + /** + * The east-west tilt cap for a footprint. Tier 1 gets the full {@link #MAX_TILT}; wider multiblock + * decks are capped so the descending edge's underside never drops below the housing top. + */ + private static float maxTiltFor(int footprint) { + if (footprint <= 1) { + return MAX_TILT; + } + double half = footprint / 2.0; + double sin = (POLE_TOP - HOUSING_TOP - THICK / 2.0F) / half; + return (float) Math.toDegrees(Math.asin(Math.min(1.0, sin))); + } + + /** + * A 1px-thick textured deck box. The PV sprite maps across the top/bottom by {@code x -> [u0,u1]} and + * {@code z -> [v0,v1]}; every face is double-sided so it shows from any angle through the cutout cull. + */ + private static void box(VertexConsumer c, PoseStack.Pose pose, int light, + float x0, float y0, float z0, float x1, float y1, float z1, + float u0, float v0, float u1, float v1) { + // top (+Y) / bottom (-Y) + face(c, pose, light, 0, 1, 0, x0, y1, z0, u0, v0, x0, y1, z1, u0, v1, x1, y1, z1, u1, v1, x1, y1, z0, u1, v0); + face(c, pose, light, 0, -1, 0, x0, y0, z0, u0, v0, x1, y0, z0, u1, v0, x1, y0, z1, u1, v1, x0, y0, z1, u0, v1); + // north (-Z) / south (+Z) + face(c, pose, light, 0, 0, -1, x0, y0, z0, u0, v0, x0, y1, z0, u0, v0, x1, y1, z0, u1, v0, x1, y0, z0, u1, v0); + face(c, pose, light, 0, 0, 1, x1, y0, z1, u1, v1, x1, y1, z1, u1, v1, x0, y1, z1, u0, v1, x0, y0, z1, u0, v1); + // west (-X) / east (+X) + face(c, pose, light, -1, 0, 0, x0, y0, z1, u0, v1, x0, y1, z1, u0, v1, x0, y1, z0, u0, v0, x0, y0, z0, u0, v0); + face(c, pose, light, 1, 0, 0, x1, y0, z0, u1, v0, x1, y1, z0, u1, v0, x1, y1, z1, u1, v1, x1, y0, z1, u1, v1); + } + + /** Emit a quad both ways (front with the given normal, back reversed) so it shows from both sides. */ + private static void face(VertexConsumer c, PoseStack.Pose pose, int light, float nx, float ny, float nz, + float ax, float ay, float az, float au, float av, + float bx, float by, float bz, float bu, float bv, + float cx, float cy, float cz, float cu, float cv, + float dx, float dy, float dz, float du, float dv) { + vertex(c, pose, ax, ay, az, au, av, light, nx, ny, nz); + vertex(c, pose, bx, by, bz, bu, bv, light, nx, ny, nz); + vertex(c, pose, cx, cy, cz, cu, cv, light, nx, ny, nz); + vertex(c, pose, dx, dy, dz, du, dv, light, nx, ny, nz); + vertex(c, pose, dx, dy, dz, du, dv, light, -nx, -ny, -nz); + vertex(c, pose, cx, cy, cz, cu, cv, light, -nx, -ny, -nz); + vertex(c, pose, bx, by, bz, bu, bv, light, -nx, -ny, -nz); + vertex(c, pose, ax, ay, az, au, av, light, -nx, -ny, -nz); + } + + private static void vertex(VertexConsumer c, PoseStack.Pose pose, float x, float y, float z, + float u, float v, int light, float nx, float ny, float nz) { + c.addVertex(pose, x, y, z) + .setColor(255, 255, 255, 255) + .setUv(u, v) + .setOverlay(OverlayTexture.NO_OVERLAY) + .setLight(light) + .setNormal(pose, nx, ny, nz); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java index 488e2ee..21afa84 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java @@ -5,27 +5,34 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; import org.jetbrains.annotations.Nullable; import za.co.neroland.nerospace.registry.ModBlockEntities; /** - * A solar panel that pools with adjacent same-tier panels into a {@link SolarArray}. Each tier is its - * own registered block with its own output/buffer; energy is exposed on every side (output ports), so - * any face feeds a pipe or machine from the shared array pool. GUI-less; emits a comparator signal - * proportional to its stored energy. + * A solar panel that pools with adjacent same-tier panels into a {@link SolarArray}. Tier 1 is a single + * 1×1 block; Tier 2 (2×2) and Tier 3 (3×3) are multiblocks: placing the item fills the whole N×N + * footprint with one {@link #ANCHOR} cell at the clicked min-corner plus filler cells, all powered as a + * single unit (the renderer draws one big sun-tracking deck on the anchor). Breaking any cell tears the + * whole unit down and returns one item. Energy is exposed on every side (filler cells forward to the + * anchor's buffer), so any face feeds a pipe or machine from the shared array pool. * - *

Cross-loader port: tier-aware (the standalone single-tier block is generalised). The N×N - * multiblock footprint + the tilting sun-tracking deck renderer are a deferred enhancement — every - * panel is a 1×1 block here.

+ *

Cross-loader port: tier-aware (the standalone single-tier block is generalised); the multiblock + * placement/teardown is plain vanilla block API.

*/ public class SolarPanelBlock extends BaseEntityBlock { @@ -35,11 +42,20 @@ public class SolarPanelBlock extends BaseEntityBlock { propertiesCodec() ).apply(instance, SolarPanelBlock::new)); + /** True on the unit's min-corner cell — the only cell that drops the item and renders the deck. */ + public static final BooleanProperty ANCHOR = BooleanProperty.create("anchor"); + + /** Re-entrancy guard so cascading a multiblock teardown doesn't recurse or double-drop. */ + private static boolean tearingDown = false; + private final SolarTier tier; + @SuppressWarnings("this-escape") // registerDefaultState is the idiomatic constructor wiring public SolarPanelBlock(SolarTier tier, Properties properties) { super(properties); this.tier = tier; + // Default to an anchor so a raw setBlock places a working unit; fillers are set false on placement. + registerDefaultState(this.stateDefinition.any().setValue(ANCHOR, true)); } public SolarTier tier() { @@ -51,6 +67,11 @@ protected MapCodec codec() { return CODEC; } + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ANCHOR); + } + @Override protected RenderShape getRenderShape(BlockState state) { return RenderShape.MODEL; @@ -72,6 +93,84 @@ public BlockEntityTicker getTicker(Level level, Block (lvl, pos, st, be) -> be.tick(lvl, pos, st)); } + // --- Multiblock placement ------------------------------------------------- + + @Override + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + BlockState anchor = defaultBlockState().setValue(ANCHOR, true); + int n = this.tier.footprint; + if (n <= 1) { + return anchor; + } + // The clicked cell is validated by the item; require the rest of the N×N footprint clear. + Level level = context.getLevel(); + BlockPos origin = context.getClickedPos(); + for (int dx = 0; dx < n; dx++) { + for (int dz = 0; dz < n; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + if (!level.getBlockState(origin.offset(dx, 0, dz)).canBeReplaced(context)) { + return null; // footprint blocked — cancel the placement + } + } + } + return anchor; + } + + @Override + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { + super.onPlace(state, level, pos, oldState, movedByPiston); + if (level.isClientSide() || this.tier.footprint <= 1 || !state.getValue(ANCHOR)) { + return; + } + int n = this.tier.footprint; + BlockState part = defaultBlockState().setValue(ANCHOR, false); + for (int dx = 0; dx < n; dx++) { + for (int dz = 0; dz < n; dz++) { + BlockPos cell = pos.offset(dx, 0, dz); + if (!cell.equals(pos)) { + BlockState existing = level.getBlockState(cell); + if (existing.getBlock() != this && existing.canBeReplaced()) { + level.setBlock(cell, part, Block.UPDATE_CLIENTS); + } + } + if (level.getBlockEntity(cell) instanceof SolarPanelBlockEntity be) { + be.setAnchor(pos); // anchor cell -> self; fillers -> the anchor + } + } + } + } + + // --- Multiblock teardown -------------------------------------------------- + + @Override + public BlockState playerWillDestroy(Level level, BlockPos pos, BlockState state, Player player) { + if (this.tier.footprint > 1 && !level.isClientSide() && !tearingDown) { + BlockPos anchor = level.getBlockEntity(pos) instanceof SolarPanelBlockEntity be ? be.anchorPos() : pos; + tearingDown = true; + try { + int n = this.tier.footprint; + for (int dx = 0; dx < n; dx++) { + for (int dz = 0; dz < n; dz++) { + BlockPos cell = anchor.offset(dx, 0, dz); + if (!cell.equals(pos) && level.getBlockState(cell).getBlock() == this) { + level.removeBlock(cell, false); // sibling cell — no drops + } + } + } + // One item back for the whole unit (the broken cell's own loot is empty for multiblocks). + if (player == null || !player.getAbilities().instabuild) { + Block.popResource(level, anchor, new ItemStack(this)); + } + } finally { + tearingDown = false; + } + } + return super.playerWillDestroy(level, pos, state, player); + } + @Override protected boolean hasAnalogOutputSignal(BlockState state) { return true; diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java index 373c164..cf002b4 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java @@ -68,9 +68,29 @@ public boolean isAnchor() { return this.anchorPos.equals(this.worldPosition); } - /** Exposed to the energy capability on every face (extract-only pool). */ + /** Point a filler cell at its anchor (set during multiblock placement). */ + public void setAnchor(BlockPos anchor) { + this.anchorPos = anchor; + setChanged(); + } + + /** The anchor's BE (this one if it is the anchor), or {@code null} if the anchor is gone. */ + @Nullable + private SolarPanelBlockEntity anchorEntity() { + if (isAnchor()) { + return this; + } + return this.level != null && this.level.getBlockEntity(this.anchorPos) instanceof SolarPanelBlockEntity a + ? a : null; + } + + /** + * Exposed to the energy capability on every face. Filler cells forward to the anchor's buffer, so the + * whole multiblock reads as one extract-only pool from any side. + */ public NerospaceEnergyStorage getEnergy() { - return this.energy; + SolarPanelBlockEntity anchor = anchorEntity(); + return anchor != null ? anchor.energy : this.energy; } /** The raw buffer — used by {@link SolarArray} to balance the pool. */ @@ -131,6 +151,11 @@ public int generationThisTick(ServerLevel level) { * daylight and ramps toward ~11 at night — and rises during rain/thunder, so weather is already * folded in (no separate multiplier needed).

*/ + /** True in the mod's airless dimensions (permanent sun, no weather) — drives the solar 2× bonus. */ + public static boolean isAirless(Level level) { + return AIRLESS.contains(level.dimension()); + } + private static float solarFactor(ServerLevel level, BlockPos pos) { if (AIRLESS.contains(level.dimension())) { return 2.0F; // permanent sun in orbit / on an airless moon, no weather — the 2x bonus diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json index cae7fc5..cbc6443 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json @@ -1,6 +1,9 @@ { "variants": { - "": { + "anchor=true": { + "model": "nerospace:block/solar_panel" + }, + "anchor=false": { "model": "nerospace:block/solar_panel" } } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json index ba785d8..8be8067 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json @@ -1,6 +1,9 @@ { "variants": { - "": { + "anchor=true": { + "model": "nerospace:block/solar_panel_t2" + }, + "anchor=false": { "model": "nerospace:block/solar_panel_t2" } } diff --git a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json index bfd7411..97742a7 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json @@ -1,6 +1,9 @@ { "variants": { - "": { + "anchor=true": { + "model": "nerospace:block/solar_panel_t3" + }, + "anchor=false": { "model": "nerospace:block/solar_panel_t3" } } From 6a80050b4c826ff52f44b1687e11bd52d7ca2432 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:29:10 +0200 Subject: [PATCH 80/82] Add per-face pipe config menu and screen Introduce a server-side, slot-less pipe configuration UI: PipeConfigMenu (container) and PipeConfigScreen (client). PipeConfigMenu syncs 7 data values ([0]=selected layer, [1..6]=face modes) and handles cycle buttons via clickMenuButton so no custom packet is needed. UniversalPipeBlockEntity now implements MenuProvider, exposes transient configType + ContainerData, provides createMenu/getDisplayName and a cycleConfigType method. ConfiguratorItem sneak+right-click opens the menu on the server. Register the PIPE_CONFIG menu type and wire the screen registration for Fabric and NeoForge clients. Update docs to mark Slice B1 (per-face config GUI) as done. --- docs/MULTILOADER_PORT_CHECKLIST.md | 20 +++- .../nerospace/client/PipeConfigScreen.java | 77 +++++++++++++++ .../nerospace/item/ConfiguratorItem.java | 7 +- .../nerospace/menu/PipeConfigMenu.java | 98 +++++++++++++++++++ .../pipe/UniversalPipeBlockEntity.java | 54 +++++++++- .../nerospace/registry/ModMenuTypes.java | 5 + .../fabric/NerospaceFabricClient.java | 2 + .../neoforge/NeoForgeClientSetup.java | 2 + 8 files changed, 257 insertions(+), 8 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/client/PipeConfigScreen.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PipeConfigMenu.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index d53ae27..3d33f8b 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -475,11 +475,21 @@ checked by a headless build). `FluidTank` + `relayFluid()` to the pipe BE (honours the FLUID face-mode + speed); the pipe's fluid handler is exposed as the FLUID cap on both loaders. The pipe now carries all four layers; the FLUID face-mode is live (e.g. Refinery → pipe → Fuel Tank). -+ [ ] **Slice B (deferred) — graph + visuals + GUI.** `PipeNetwork` (591-line graph; NeoForge-transfer- - coupled), `TravellingItem` (animated stacks; ItemResource→ItemStack), `UniversalPipeRenderer` + - `UniversalPipeRenderState` (stream + travelling-item visuals), `PipeConfigScreen` + `PipeConfigOpenHandler` - + `network/SetPipeModePayload` (the per-face×per-type config GUI — needs a cross-loader client-screen-open - seam). Cosmetic / convenience; the slice-A in-world cycling already configures pipes fully. ++ [x] **Slice B1 DONE — per-face config GUI.** A slot-less `PipeConfigMenu` (`menu/`) + `PipeConfigScreen` + (`client/`, plain hull panel, no texture asset, SpaceButtons) let the player edit one resource layer at a + time across all six faces: 7 synced data values ([0]=layer, [1..6]=each face's mode), a layer cycler + + one cycler per face, all routed through `clickMenuButton` (no packet). `UniversalPipeBlockEntity` now + implements `MenuProvider` (+ a transient `configType` + `configData` `ContainerData`); the **Configurator's + sneak+right-click on a pipe opens it** via the vanilla `openMenu` path. **Cross-loader adaptation:** uses a + server-authoritative menu instead of the standalone mod's client-`PipeConfigScreen` + `SetPipeModePayload` + + `PipeConfigOpenHandler`, so **no client-screen-open seam is needed** (menus + their screens already + register cross-loader). Menu type registered + screen registered on both loaders; reuses the existing + `pipe.nerospace.mode.*` lang. ++ [ ] **Slice B2 (deferred) — travelling-item visuals.** `TravellingItem` (animated in-transit stacks) + + `UniversalPipeRenderer` + `UniversalPipeRenderState` (items visibly flow through the pipe), and optionally + the `PipeNetwork` 591-line routing graph. Purely cosmetic (the relay already moves resources); the BER + rendering API is now proven cross-version (see solar slice 2), so the renderer itself is de-risked — the + remaining work is making the item relay animation-aware (tracking + syncing in-transit stacks). ### Machine modules / upgrades (`module/` 3) — **DONE (4 cells green)** diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/client/PipeConfigScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/PipeConfigScreen.java new file mode 100644 index 0000000..f33b45b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/PipeConfigScreen.java @@ -0,0 +1,77 @@ +package za.co.neroland.nerospace.client; + +import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; + +import za.co.neroland.nerospace.menu.PipeConfigMenu; + +/** + * The Universal Pipe configuration GUI (advanced-pipes slice B). Slot-less: shows the selected resource + * layer and each of the six faces' I/O mode for that layer, with a {@link SpaceButton} to cycle the + * layer and one per face to cycle its mode. Buttons route to the server menu via + * {@code handleInventoryButtonClick} (no custom packet). Drawn on a plain hull panel (no texture + * asset) using the shared {@link SpaceButton}; the live mode/layer text is drawn each frame from the + * synced menu data. + */ +public class PipeConfigScreen extends AbstractContainerScreen { + + private static final int ACCENT = 0xFF5AC8E0; // pipe cyan + private static final int PANEL = 0xF00B1119; // dark hull + private static final int TITLE = 0xFFD6ECFF; + private static final int SUBTLE = 0xFF8DA0B4; + + private static final String[] FACE_NAMES = {"Down", "Up", "North", "South", "West", "East"}; + private static final int FIRST_ROW_Y = 40; + private static final int ROW_STEP = 18; + + public PipeConfigScreen(PipeConfigMenu menu, Inventory playerInventory, Component title) { + super(menu, playerInventory, title, 176, 152); + this.titleLabelX = 8; + this.titleLabelY = 6; + } + + @Override + protected void init() { + super.init(); + // Layer cycler. + this.addRenderableWidget(new SpaceButton(this.leftPos + 108, this.topPos + 18, 60, 14, + Component.literal("Layer ▸"), ACCENT, b -> sendButton(PipeConfigMenu.BUTTON_CYCLE_TYPE))); + // One cycler per face. + for (int i = 0; i < 6; i++) { + final int face = i; + this.addRenderableWidget(new SpaceButton(this.leftPos + 128, this.topPos + FIRST_ROW_Y + i * ROW_STEP, 40, 14, + Component.literal("▸"), ACCENT, b -> sendButton(PipeConfigMenu.FACE_BASE + face))); + } + } + + @Override + public void extractContents(GuiGraphicsExtractor extractor, int mouseX, int mouseY, float partialTick) { + // Plain hull panel + top accent line (no texture asset). + extractor.fill(this.leftPos, this.topPos, this.leftPos + this.imageWidth, this.topPos + this.imageHeight, PANEL); + extractor.fill(this.leftPos, this.topPos, this.leftPos + this.imageWidth, this.topPos + 1, ACCENT); + super.extractContents(extractor, mouseX, mouseY, partialTick); + + // Selected layer. + extractor.text(this.font, Component.literal("Layer: ").append(this.menu.getSelectedType().label()), + this.leftPos + 8, this.topPos + 22, TITLE, false); + // Per-face mode for the selected layer. + for (int i = 0; i < 6; i++) { + Component mode = Component.translatable("pipe.nerospace.mode." + this.menu.getFaceMode(i).getSerializedName()); + Component line = Component.literal(FACE_NAMES[i] + ": ").append(mode); + extractor.text(this.font, line, this.leftPos + 8, this.topPos + FIRST_ROW_Y + 3 + i * ROW_STEP, SUBTLE, false); + } + } + + @Override + protected void extractLabels(GuiGraphicsExtractor extractor, int mouseX, int mouseY) { + extractor.text(this.font, this.title, this.titleLabelX, this.titleLabelY, TITLE, false); + } + + private void sendButton(int id) { + if (this.minecraft != null && this.minecraft.gameMode != null) { + this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, id); + } + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java index 8488800..a4dff99 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java @@ -55,8 +55,11 @@ public InteractionResult useOn(UseOnContext context) { Player player = context.getPlayer(); if (player != null && player.isShiftKeyDown()) { - if (level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity) { - // Reserved for the config GUI (client slice); only cycle when sneaking on other blocks. + if (level.getBlockEntity(pos) instanceof UniversalPipeBlockEntity pipe) { + // Sneak + right-click a pipe: open the per-face configuration GUI. + if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { + serverPlayer.openMenu(pipe); + } return InteractionResult.SUCCESS; } if (!level.isClientSide() && player instanceof ServerPlayer serverPlayer) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PipeConfigMenu.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PipeConfigMenu.java new file mode 100644 index 0000000..9ef3742 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/menu/PipeConfigMenu.java @@ -0,0 +1,98 @@ +package za.co.neroland.nerospace.menu; + +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; +import net.minecraft.world.inventory.SimpleContainerData; +import net.minecraft.world.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.pipe.PipeIoMode; +import za.co.neroland.nerospace.pipe.PipeResourceType; +import za.co.neroland.nerospace.pipe.UniversalPipeBlockEntity; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * The Universal Pipe configuration menu (advanced-pipes slice B). A slot-less menu that edits one + * resource layer at a time across the pipe's six faces: seven synced data values ([0]=selected layer, + * [1..6]=each face's I/O mode for that layer). Buttons route through {@link #clickMenuButton} so no + * custom packet is needed — the cycle-layer and per-face cycle buttons are handled server-side where + * the menu holds the {@link UniversalPipeBlockEntity}. + * + *

Cross-loader note: a plain (non-extended) menu opened via the vanilla {@code openMenu} path, so it + * needs no loader-specific extended-menu API and no client-screen-open seam — the standalone mod's + * client-screen + {@code SetPipeModePayload} approach is replaced by this server-authoritative menu.

+ */ +public class PipeConfigMenu extends AbstractContainerMenu { + + public static final int DATA_COUNT = 7; + public static final int BUTTON_CYCLE_TYPE = 0; + /** Cycle face {@code n} (0..5 by {@link Direction#get3DDataValue()}) via button id {@code FACE_BASE + n}. */ + public static final int FACE_BASE = 1; + + @Nullable + private final UniversalPipeBlockEntity pipe; + private final ContainerData data; + + public PipeConfigMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, null, new SimpleContainerData(DATA_COUNT)); + } + + @SuppressWarnings("this-escape") // idiomatic Minecraft constructor wiring + public PipeConfigMenu(int containerId, Inventory playerInventory, + @Nullable UniversalPipeBlockEntity pipe, ContainerData data) { + super(ModMenuTypes.PIPE_CONFIG.get(), containerId); + checkContainerDataCount(data, DATA_COUNT); + this.pipe = pipe; + this.data = data; + this.addDataSlots(data); + } + + @Override + public boolean clickMenuButton(Player player, int id) { + UniversalPipeBlockEntity current = this.pipe; // local copy so the null check holds for the analyzer + if (current == null) { + return false; + } + if (id == BUTTON_CYCLE_TYPE) { + current.cycleConfigType(); + return true; + } + if (id >= FACE_BASE && id < FACE_BASE + 6) { + current.cycleMode(Direction.from3DDataValue(id - FACE_BASE), getSelectedType()); + return true; + } + return false; + } + + @Override + public boolean stillValid(Player player) { + UniversalPipeBlockEntity current = this.pipe; + if (current == null) { + return true; + } + return !current.isRemoved() && player.distanceToSqr( + current.getBlockPos().getX() + 0.5, current.getBlockPos().getY() + 0.5, + current.getBlockPos().getZ() + 0.5) < 64.0; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; // slot-less config readout — nothing to move + } + + // --- Screen helpers ----------------------------------------------------- + + /** The resource layer currently being edited. */ + public PipeResourceType getSelectedType() { + return PipeResourceType.VALUES[Math.floorMod(this.data.get(0), PipeResourceType.VALUES.length)]; + } + + /** The I/O mode of face {@code faceIndex} (0..5 by {@link Direction#get3DDataValue()}) for the layer. */ + public PipeIoMode getFaceMode(int faceIndex) { + return PipeIoMode.VALUES[Math.floorMod(this.data.get(1 + faceIndex), PipeIoMode.VALUES.length)]; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java index 2b73271..b3dd1d0 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -4,12 +4,17 @@ import net.minecraft.core.Direction; import net.minecraft.core.NonNullList; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.Container; import net.minecraft.world.Containers; +import net.minecraft.world.MenuProvider; import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.ContainerData; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.BlockEntity; @@ -29,6 +34,7 @@ import za.co.neroland.nerospace.gas.GasTank; import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.item.PipeUpgradeItem; +import za.co.neroland.nerospace.menu.PipeConfigMenu; import za.co.neroland.nerospace.platform.EnergyLookup; import za.co.neroland.nerospace.platform.FluidLookup; import za.co.neroland.nerospace.platform.GasLookup; @@ -43,7 +49,7 @@ * as the item capability and chains pipe-to-pipe. Item flow is directed: pull only from non-pipe * containers, push to any neighbour — sources feed the line, the line feeds sinks. */ -public class UniversalPipeBlockEntity extends BlockEntity implements WorldlyContainer { +public class UniversalPipeBlockEntity extends BlockEntity implements WorldlyContainer, MenuProvider { public static final int CAPACITY = 8_000; public static final int MAX_IO = 1_000; @@ -112,6 +118,52 @@ public void setMode(Direction dir, PipeResourceType type, PipeIoMode mode) { setChanged(); } + // --- Config GUI (Configurator sneak-use) --------------------------------- + + /** Transient UI state: which resource layer the open config menu edits (not saved). */ + private int configType = 0; + + /** Synced to the config menu: [0]=configType, [1..6]=each face's mode ordinal for that layer. */ + private final ContainerData configData = new ContainerData() { + @Override + public int get(int index) { + if (index == 0) { + return configType; + } + Direction dir = Direction.from3DDataValue(index - 1); + return faceModes[dir.get3DDataValue()][configType].ordinal(); + } + + @Override + public void set(int index, int value) { + // read-only from the client + } + + @Override + public int getCount() { + return 7; + } + }; + + public int configType() { + return this.configType; + } + + /** Cycle which resource layer the config menu edits (menu button). */ + public void cycleConfigType() { + this.configType = Math.floorMod(this.configType + 1, PipeResourceType.VALUES.length); + } + + @Override + public Component getDisplayName() { + return Component.translatable("block.nerospace.universal_pipe"); + } + + @Override + public AbstractContainerMenu createMenu(int containerId, Inventory inventory, Player player) { + return new PipeConfigMenu(containerId, inventory, this, this.configData); + } + // --- Per-face item filter (Pipe Filter) ---------------------------------- public ItemStack filter(Direction dir) { diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java index 2239475..1e55016 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -12,6 +12,7 @@ import za.co.neroland.nerospace.menu.HydrationModuleMenu; import za.co.neroland.nerospace.machine.quarry.QuarryMenu; import za.co.neroland.nerospace.menu.PassiveGeneratorMenu; +import za.co.neroland.nerospace.menu.PipeConfigMenu; import za.co.neroland.nerospace.menu.TerraformMonitorMenu; import za.co.neroland.nerospace.menu.TerraformerMenu; import za.co.neroland.nerospace.progression.StarGuideMenu; @@ -32,6 +33,10 @@ public final class ModMenuTypes { MENUS.register("nerosium_grinder", key -> new MenuType<>(NerosiumGrinderMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> PIPE_CONFIG = + MENUS.register("pipe_config", + key -> new MenuType<>(PipeConfigMenu::new, FeatureFlags.VANILLA_SET)); + public static final RegistryEntry> PASSIVE_GENERATOR = MENUS.register("passive_generator", key -> new MenuType<>(PassiveGeneratorMenu::new, FeatureFlags.VANILLA_SET)); diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java index 5fc8a44..e6bdfce 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -23,6 +23,7 @@ import za.co.neroland.nerospace.client.FuelRefineryScreen; import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.PipeConfigScreen; import za.co.neroland.nerospace.client.HydrationModuleScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; @@ -41,6 +42,7 @@ public void onInitializeClient() { MenuScreens.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); MenuScreens.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); MenuScreens.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); + MenuScreens.register(ModMenuTypes.PIPE_CONFIG.get(), PipeConfigScreen::new); MenuScreens.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); MenuScreens.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); MenuScreens.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java index cbac6eb..d00c2de 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -28,6 +28,7 @@ import za.co.neroland.nerospace.client.FuelRefineryScreen; import za.co.neroland.nerospace.client.FuelTankScreen; import za.co.neroland.nerospace.client.PassiveGeneratorScreen; +import za.co.neroland.nerospace.client.PipeConfigScreen; import za.co.neroland.nerospace.client.HydrationModuleScreen; import za.co.neroland.nerospace.client.QuarryScreen; import za.co.neroland.nerospace.client.RocketScreen; @@ -74,6 +75,7 @@ private static void onRegisterScreens(RegisterMenuScreensEvent event) { event.register(ModMenuTypes.COMBUSTION_GENERATOR.get(), CombustionGeneratorScreen::new); event.register(ModMenuTypes.NEROSIUM_GRINDER.get(), NerosiumGrinderScreen::new); event.register(ModMenuTypes.PASSIVE_GENERATOR.get(), PassiveGeneratorScreen::new); + event.register(ModMenuTypes.PIPE_CONFIG.get(), PipeConfigScreen::new); event.register(ModMenuTypes.ROCKET.get(), RocketScreen::new); event.register(ModMenuTypes.FUEL_TANK.get(), FuelTankScreen::new); event.register(ModMenuTypes.FUEL_REFINERY.get(), FuelRefineryScreen::new); From 0862d99dddb75a4fb16360b9c2c326fc450ca9ad Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:47:09 +0200 Subject: [PATCH 81/82] Port Village Core interactive controller Add the Village Core gameplay controller and building catalogue: new VillageBuildings (building/quest catalogue + placement generator) and VillageCoreBlockEntity (claiming, nerosteel stockpile, reputation-gated teach-and-grow staged placement, passive production, fetch quests, config-gated night raids, and ValueInput/ValueOutput persistence). Convert VillageCoreBlock to a BaseEntityBlock with interaction handlers and server ticker hookup. Register VILLAGE_CORE block-entity type in ModBlockEntities, add two lang messages, and update NerospaceConfig with an alienRaidsEnabled property (read/write, accessor and docs entry). Also update the port checklist doc. Cross-version adaptations: raids use Level.getSkyDarken() and read the properties config seam rather than a NeoForge ModConfigSpec. --- docs/MULTILOADER_PORT_CHECKLIST.md | 28 +- .../nerospace/config/NerospaceConfig.java | 14 +- .../nerospace/registry/ModBlockEntities.java | 5 + .../nerospace/village/VillageBuildings.java | 116 ++++++ .../nerospace/village/VillageCoreBlock.java | 119 +++++- .../village/VillageCoreBlockEntity.java | 382 ++++++++++++++++++ .../assets/nerospace/lang/en_us.json | 2 + 7 files changed, 655 insertions(+), 11 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageBuildings.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlockEntity.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 3d33f8b..3bdbef7 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -4,6 +4,20 @@ Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still n cross-loader `multiloader/` project. As of this audit: **~218 classes ported, ~46 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-22 update — Village Core interactive controller ported (closes the last big gameplay gap).** +> All 4 cells green (full `:neoforge:build`+`:fabric:build` on **both** 26.2 and 26.1.2; ecjCheck 0 errors / +> 21 baseline warnings, 0 new). The decorative `VillageCoreBlock` stub is now the root's full teach-and-grow +> engine: ported `village/VillageBuildings` (building catalogue + quest table + box-structure generator) + +> `village/VillageCoreBlockEntity` (373-line controller: claim, nerosteel stockpile, reputation-gated build +> jobs with staged block-by-block placement, passive production, fetch quests, config-gated night raids, +> `ValueInput`/`ValueOutput` persistence) and replaced the block with the interactive `BaseEntityBlock`. +> Registered the `VILLAGE_CORE` block-entity type + 2 message lang keys; added an `alienRaidsEnabled` +> opt-out (default ON) to the properties `NerospaceConfig`. **Cross-version adaptations:** the after-dark +> raid gate uses vanilla `Level.getSkyDarken()` (not `isBrightOutside()`, which diverges 26.1.2↔26.2), and +> raids read the properties config seam rather than a NeoForge `ModConfigSpec`. Reuses the already-ported +> `AlienVillager` reputation API. The structures place the same block, so alien hamlets / ruins / mega-cities +> now ship a live, claimable, growable Village Core. **~222 classes ported.** + > **2026-06-21 update — /nerospace commands ported.** All 4 cells compile green. `command/NerospaceCommands` > (the `/nerospace gallery` creative showcase) behind a cross-loader `register(CommandDispatcher)` seam > (NeoForge `RegisterCommandsEvent` / Fabric `CommandRegistrationCallback`). Adapted: block iteration via @@ -395,9 +409,17 @@ checked by a headless build). configured/placed-feature JSON and **re-added the 3 placed features to the Greenxertz biome JSON** (`greenxertz.json` feature step 6) — since Greenxertz is our own datapack biome, no biome-modifier seam needed. Mega-city spawns the (ported) Ruin Warden boss; ruin/mega-city fill vanilla loot chests. -+ [~] `VillageCoreBlock` — ported as a **plain decorative centerpiece block** (the structures' anchor). - The interactive controller (`VillageCoreBlockEntity`: claim → teach-and-grow construction, fetch - quests, night raids) is **deferred** — it pulls in `VillageBuildings` + the config seam. ++ [x] **`VillageCoreBlock` interactive controller DONE (4 cells green).** The decorative stub is now the + full root controller: `VillageBuildings` (HUT@T2 / WORKSHOP@T3 catalogue + box-structure generator + + fetch-quest table) + `VillageCoreBlockEntity` (claim → nerosteel stockpile → rep-gated teach-and-grow + staged block placement → passive production → fetch quests → config-gated night raids; `ValueInput`/ + `ValueOutput` NBT) + the interactive `BaseEntityBlock` (deposit/claim/quest-handin/collect via vanilla + `useItemOn`/`useWithoutItem` + the `createTickerHelper` seam). Registered the `VILLAGE_CORE` BE type + (bound to the existing block) + 2 message lang keys; structures keep placing the same block and it is now + live. **Cross-version adaptations:** raids read `NerospaceConfig.alienRaidsEnabled()` (the properties + config seam, default ON/opt-out — no NeoForge `ModConfigSpec`), and the after-dark gate uses the + long-standing vanilla `Level.getSkyDarken()` instead of `isBrightOutside()` (the de-obf day/night helpers + diverge 26.1.2↔26.2). Reuses the already-ported `AlienVillager` rep API (`getTier`/`addReputation`). ### Meteor events (`meteor/` 8 + client) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java index c8b282d..2fda5c2 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -27,6 +27,7 @@ public final class NerospaceConfig { private static final String KEY_OXYGEN_CAPACITY = "oxygenCapacityMultiplier"; private static final String KEY_FUEL_COST = "fuelCostMultiplier"; private static final String KEY_MACHINE_SPEED = "machineSpeedMultiplier"; + private static final String KEY_ALIEN_RAIDS = "alienRaidsEnabled"; /** Multiplier range (mirrors the root config spec): 0.1× .. 10×. */ private static final double MULT_MIN = 0.1D; @@ -44,6 +45,8 @@ public final class NerospaceConfig { private static volatile double fuelCostMultiplier = 1.0D; /** Scales machine work speed (inverse: higher ⇒ shorter work intervals). Clamped 0.1×..10×. */ private static volatile double machineSpeedMultiplier = 1.0D; + /** Whether alien villages can be raided by hostile mobs at night. ON by default; players opt out. */ + private static volatile boolean alienRaidsEnabled = true; private static volatile boolean loaded; private NerospaceConfig() { @@ -73,6 +76,11 @@ public static double machineSpeedMultiplier() { return machineSpeedMultiplier; } + /** Whether config-gated night raids on alien villages are enabled (default true; opt-out). */ + public static boolean alienRaidsEnabled() { + return alienRaidsEnabled; + } + /** * Inverse-scales a base work interval by the machine-speed multiplier: a higher speed yields a * SHORTER interval, clamped to ≥1 tick (so 10× can't produce a zero-tick interval). Mirrors the root @@ -123,6 +131,8 @@ public static synchronized void load() { props.getProperty(KEY_FUEL_COST), fuelCostMultiplier)); machineSpeedMultiplier = clampMultiplier(parseDouble( props.getProperty(KEY_MACHINE_SPEED), machineSpeedMultiplier)); + alienRaidsEnabled = Boolean.parseBoolean( + props.getProperty(KEY_ALIEN_RAIDS, Boolean.toString(alienRaidsEnabled)).trim()); } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not read {}; using defaults.", FILE_NAME, e); } @@ -152,6 +162,7 @@ private static void write(Path file) { props.setProperty(KEY_OXYGEN_CAPACITY, Double.toString(oxygenCapacityMultiplier)); props.setProperty(KEY_FUEL_COST, Double.toString(fuelCostMultiplier)); props.setProperty(KEY_MACHINE_SPEED, Double.toString(machineSpeedMultiplier)); + props.setProperty(KEY_ALIEN_RAIDS, Boolean.toString(alienRaidsEnabled)); try { Files.createDirectories(file.getParent()); try (OutputStream out = Files.newOutputStream(file)) { @@ -163,7 +174,8 @@ private static void write(Path file) { + "scales how fast air drains. oxygenCapacityMultiplier: scales air capacity. " + "fuelCostMultiplier: scales fuel burned per rocket launch. " + "machineSpeedMultiplier: scales machine work speed (higher = faster). " - + "All multipliers 0.1..10, default 1."); + + "All multipliers 0.1..10, default 1. alienRaidsEnabled: allow hostile mobs to " + + "raid alien villages at night (true by default; set false to opt out)."); } } catch (IOException e) { NerospaceCommon.LOGGER.warn("[Nerospace] Could not write {}; using defaults.", FILE_NAME, e); diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java index b8c54ee..35c8af7 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -30,6 +30,7 @@ import za.co.neroland.nerospace.storage.BatteryBlockEntity; import za.co.neroland.nerospace.storage.FluidTankBlockEntity; import za.co.neroland.nerospace.storage.ItemStoreBlockEntity; +import za.co.neroland.nerospace.village.VillageCoreBlockEntity; /** * Block-entity types, shared by both loaders via {@link RegistrationProvider} over the vanilla @@ -142,6 +143,10 @@ public final class ModBlockEntities { BLOCK_ENTITIES.register("station_core", key -> new BlockEntityType<>(StationCoreBlockEntity::new, java.util.Set.of(ModBlocks.STATION_CORE.get()))); + public static final RegistryEntry> VILLAGE_CORE = + BLOCK_ENTITIES.register("village_core", + key -> new BlockEntityType<>(VillageCoreBlockEntity::new, java.util.Set.of(ModBlocks.VILLAGE_CORE.get()))); + private ModBlockEntities() { } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageBuildings.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageBuildings.java new file mode 100644 index 0000000..55c9acb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageBuildings.java @@ -0,0 +1,116 @@ +package za.co.neroland.nerospace.village; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.world.item.Item; +import net.minecraft.world.item.Items; + +import za.co.neroland.nerospace.registry.ModItems; + +/** + * The Village Core's building catalogue + quest table (ALIEN_VILLAGERS_DESIGN.md §4, §7). The + * teach-and-grow loop raises building shells; completed buildings produce goods, the village posts + * fetch quests, and (in the core) config-gated raids test the settlement. + * + *

Cross-loader port: pure vanilla + {@link ModItems}; identical to the standalone mod.

+ */ +public final class VillageBuildings { + + public enum Kind { WALL, ROOF, LIGHT, AIR } + + public record Placement(int dx, int dy, int dz, Kind kind) { + } + + /** A teachable building: required reputation tier, nerosteel cost, footprint size and wall height. */ + public enum Type { + HUT(2, 32, 5, 4), + WORKSHOP(3, 48, 7, 5); + + public final int reqTier; + public final int cost; + public final int size; + public final int height; + + Type(int reqTier, int cost, int size, int height) { + this.reqTier = reqTier; + this.cost = cost; + this.size = size; + this.height = height; + } + + public static Type byOrdinalOrNull(int i) { + Type[] v = values(); + return (i >= 0 && i < v.length) ? v[i] : null; + } + } + + /** A fetch quest the village posts: bring N of an item for a reputation + emerald reward. */ + public enum Quest { + XERTZ_QUARTZ(8, 2), + RAW_NEROSTEEL(12, 3), + ALIEN_FRAGMENT(4, 4); + + public final int count; + public final int reward; // emeralds paid + reputation granted + + Quest(int count, int reward) { + this.count = count; + this.reward = reward; + } + + public Item item() { + return switch (this) { + case XERTZ_QUARTZ -> ModItems.XERTZ_QUARTZ.get(); + case RAW_NEROSTEEL -> ModItems.RAW_NEROSTEEL.get(); + case ALIEN_FRAGMENT -> ModItems.ALIEN_FRAGMENT.get(); + }; + } + + public Item rewardItem() { + return Items.EMERALD; + } + + public static Quest byOrdinalOrNull(int i) { + Quest[] v = values(); + return (i >= 0 && i < v.length) ? v[i] : null; + } + } + + /** Buildings are constructed in this order as the village grows. */ + public static Type[] order() { + return new Type[] {Type.HUT, Type.WORKSHOP}; + } + + private VillageBuildings() { + } + + /** + * The ordered block placements (relative to the plot origin) for a building, bottom layer first so + * it visibly rises. WALL/ROOF map to nerosteel, LIGHT to a glow source, AIR clears headroom. + */ + public static List build(Type t) { + List out = new ArrayList<>(); + int r = t.size / 2; + for (int dy = 0; dy <= t.height; dy++) { + for (int dx = -r; dx <= r; dx++) { + for (int dz = -r; dz <= r; dz++) { + boolean perimeter = Math.abs(dx) == r || Math.abs(dz) == r; + if (dy == t.height) { + out.add(new Placement(dx, dy, dz, Kind.ROOF)); + } else if (perimeter) { + boolean door = dz == -r && dx == 0 && dy <= 1; + if (!door) { + out.add(new Placement(dx, dy, dz, Kind.WALL)); + } + } else if (dy == 0 && dx == 0 && dz == 0) { + out.add(new Placement(0, 0, 0, Kind.LIGHT)); + } else { + out.add(new Placement(dx, dy, dz, Kind.AIR)); + } + } + } + } + return out; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java index d889545..ec3efe7 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java @@ -1,18 +1,123 @@ package za.co.neroland.nerospace.village; -import net.minecraft.world.level.block.Block; +import com.mojang.serialization.MapCodec; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.RenderShape; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +import org.jetbrains.annotations.Nullable; + +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModBlocks; /** - * Village Core — the glowing centerpiece of alien hamlets / ruins / mega-cities. + * Village Core (ALIEN_VILLAGERS_DESIGN.md §4.1) — the glowing centerpiece of alien hamlets / ruins / + * mega-cities, and the village's controller. Right-click to claim / teach the next building; + * right-click with Nerosteel to stock materials, or with a quest item to hand it in; sneak-right-click + * to collect produced goods and read the village's current task. It ticks construction, production and + * raids via {@link VillageCoreBlockEntity#serverTick}. * - *

Cross-loader port note: ported here as a plain decorative block. The root's interactive controller - * (claim → teach-and-grow village construction, fetch quests, night raids) is a deep gameplay subsystem - * (`VillageCoreBlockEntity` + `VillageBuildings` + config gates) deferred to its own batch; the structures - * place this as their anchor centerpiece meanwhile.

+ *

Cross-loader port: the root's interactive controller block, on vanilla interactions + * ({@code useItemOn}/{@code useWithoutItem}) + the shared {@code BaseEntityBlock} ticker seam. Replaces + * the earlier decorative-only stub; structures keep placing it as their anchor and it is now live.

*/ -public class VillageCoreBlock extends Block { +public class VillageCoreBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = simpleCodec(VillageCoreBlock::new); public VillageCoreBlock(Properties properties) { super(properties); } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected RenderShape getRenderShape(BlockState state) { + return RenderShape.MODEL; + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new VillageCoreBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, + BlockEntityType type) { + if (level.isClientSide()) { + return null; + } + return createTickerHelper(type, ModBlockEntities.VILLAGE_CORE.get(), + (lvl, pos, st, be) -> be.serverTick(lvl, pos, st)); + } + + @Override + protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, + Player player, InteractionHand hand, BlockHitResult hit) { + if (!(level.getBlockEntity(pos) instanceof VillageCoreBlockEntity core)) { + return super.useItemOn(stack, state, level, pos, player, hand, hit); + } + // Consume the interaction on the client (server is authoritative for deposits / quest hand-ins). + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (stack.is(ModBlocks.NEROSTEEL_BLOCK.get().asItem())) { + if (core.isClaimed() && !core.isOwner(player)) { + player.sendSystemMessage(Component.translatable( + "message.nerospace.village_core.owned", core.getOwnerName())); + } else { + if (!core.isClaimed()) { + core.claim(player); + } + core.deposit(player, stack); + } + return InteractionResult.SUCCESS; + } + if (core.tryCompleteQuest(player, stack)) { + return InteractionResult.SUCCESS; + } + return super.useItemOn(stack, state, level, pos, player, hand, hit); + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, + BlockHitResult hit) { + if (!(level.getBlockEntity(pos) instanceof VillageCoreBlockEntity core)) { + return InteractionResult.PASS; + } + if (level.isClientSide()) { + return InteractionResult.SUCCESS; + } + if (player.isShiftKeyDown()) { + core.collectAndStatus(player); + return InteractionResult.SUCCESS; + } + if (!core.isClaimed()) { + core.claim(player); + player.sendSystemMessage(Component.translatable("message.nerospace.village_core.claimed")); + } else if (core.isOwner(player)) { + core.onUse(player); + } else { + player.sendSystemMessage(Component.translatable( + "message.nerospace.village_core.owned", core.getOwnerName())); + } + return InteractionResult.SUCCESS; + } } diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlockEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlockEntity.java new file mode 100644 index 0000000..6b3741d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlockEntity.java @@ -0,0 +1,382 @@ +package za.co.neroland.nerospace.village; + +import java.util.List; +import java.util.UUID; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import net.minecraft.world.phys.AABB; + +import za.co.neroland.nerospace.config.NerospaceConfig; +import za.co.neroland.nerospace.entity.AlienVillager; +import za.co.neroland.nerospace.registry.ModBlockEntities; +import za.co.neroland.nerospace.registry.ModBlocks; +import za.co.neroland.nerospace.registry.ModEntities; +import za.co.neroland.nerospace.village.VillageBuildings.Placement; +import za.co.neroland.nerospace.village.VillageBuildings.Quest; +import za.co.neroland.nerospace.village.VillageBuildings.Type; + +/** + * Village Core controller (ALIEN_VILLAGERS_DESIGN.md §4). Claimable; a teach-and-grow engine (feed + * Nerosteel, then teach the village to raise the next building); and a functional hub — completed + * buildings produce goods the owner collects, the village posts fetch quests for reputation, and + * (config-gated) hostile raids test the settlement at night. + * + *

Cross-loader port: the root's interactive controller, ported verbatim onto the shared + * persistence/entity APIs. Two adaptations for cross-version safety: the raid gate reads + * {@link NerospaceConfig#alienRaidsEnabled()} (the properties config seam, not the NeoForge + * {@code ModConfigSpec}), and the after-dark check uses the long-standing vanilla + * {@link Level#getSkyDarken()} instead of {@code isBrightOutside()} (the de-obf day/night helpers + * diverge 26.1.2↔26.2).

+ */ +public class VillageCoreBlockEntity extends BlockEntity { + + private static final int BUILD_INTERVAL = 5; + private static final int PRODUCE_INTERVAL = 1200; // ~1 min between yields + private static final int RAID_INTERVAL = 2400; // ~2 min between raid checks + private static final double SCAN_RADIUS = 32.0; + private static final double RAID_RANGE = 48.0; + private static final int OUTPUT_CAP = 64; + /** getSkyDarken() ≥ this ⇒ night/dusk (0 = noon, ~11 = full dark); raids only fire after dark. */ + private static final int DARK_THRESHOLD = 4; + private static final int[][] PLOT_OFFSETS = {{8, 0}, {-8, 0}, {0, 8}, {0, -8}, {8, 8}, {-8, -8}}; + + private UUID owner; + private String ownerName = ""; + + private int stockpile; + private int builtCount; + + private Type jobType; + private int progress; + private int plotX; + private int plotY; + private int plotZ; + private int buildTick; + private transient List jobPlacements; + + // Production output, quest, timers. + private int outBread; + private int outIngot; + private int produceTick; + private int raidTick; + private int questOrdinal = -1; + + public VillageCoreBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.VILLAGE_CORE.get(), pos, state); + } + + // --- Claiming ------------------------------------------------------------- + + public boolean isClaimed() { + return this.owner != null; + } + + public boolean isOwner(Player player) { + return player.getUUID().equals(this.owner); + } + + public String getOwnerName() { + return this.ownerName; + } + + public void claim(Player player) { + this.owner = player.getUUID(); + this.ownerName = player.getName().getString(); + if (this.questOrdinal < 0) { + rollQuest(); + } + setChanged(); + } + + // --- Teach-and-grow ------------------------------------------------------- + + public void deposit(Player player, ItemStack stack) { + int add = stack.getCount(); + if (add <= 0) { + return; + } + stack.shrink(add); + this.stockpile += add; + player.sendSystemMessage(Component.literal("Stockpile: " + this.stockpile + " Nerosteel.")); + setChanged(); + } + + public void onUse(Player player) { + if (this.jobType != null) { + int total = placements().size(); + int pct = total == 0 ? 100 : (int) (100.0 * this.progress / total); + player.sendSystemMessage(Component.literal("Constructing " + label(this.jobType) + "… " + pct + "%")); + return; + } + Type next = Type.byOrdinalOrNull(this.builtCount); + if (next == null) { + player.sendSystemMessage(Component.literal("The village is fully built. The aliens are grateful.")); + return; + } + int tier = villageTier(player); + if (tier < next.reqTier) { + player.sendSystemMessage(Component.literal( + "The villagers won't follow your plans yet — reach trust tier " + next.reqTier + + " (you are tier " + tier + ").")); + return; + } + if (this.stockpile < next.cost) { + player.sendSystemMessage(Component.literal( + "Teaching the " + label(next) + " needs " + next.cost + " Nerosteel in the stockpile (have " + + this.stockpile + ").")); + return; + } + this.stockpile -= next.cost; + int[] off = PLOT_OFFSETS[Math.min(this.builtCount, PLOT_OFFSETS.length - 1)]; + this.plotX = this.worldPosition.getX() + off[0]; + this.plotZ = this.worldPosition.getZ() + off[1]; + this.plotY = this.level != null + ? this.level.getHeight(Heightmap.Types.WORLD_SURFACE, this.plotX, this.plotZ) + : this.worldPosition.getY(); + this.jobType = next; + this.progress = 0; + this.buildTick = 0; + this.jobPlacements = VillageBuildings.build(next); + player.sendSystemMessage(Component.literal("Construction begins: " + label(next) + ".")); + setChanged(); + } + + // --- Production + quests + raids ------------------------------------------ + + /** Sneak-right-click: collect produced goods and read the current quest. */ + public void collectAndStatus(Player player) { + int bread = this.outBread; + int ingot = this.outIngot; + if (bread > 0) { + give(player, new ItemStack(Items.BREAD, bread)); + } + if (ingot > 0) { + give(player, new ItemStack(ModBlocks.NEROSTEEL_BLOCK.get().asItem(), ingot)); + } + this.outBread = 0; + this.outIngot = 0; + if (bread > 0 || ingot > 0) { + player.sendSystemMessage(Component.literal("Collected " + bread + " bread, " + ingot + " nerosteel.")); + } + Quest q = Quest.byOrdinalOrNull(this.questOrdinal); + if (q != null) { + player.sendSystemMessage(Component.literal( + "Village task: bring " + q.count + "x " + itemLabel(q) + " for " + q.reward + " emeralds + trust.")); + } + setChanged(); + } + + /** Right-click with the quest item: hand it in for the reward. */ + public boolean tryCompleteQuest(Player player, ItemStack stack) { + Quest q = Quest.byOrdinalOrNull(this.questOrdinal); + if (q == null || !stack.is(q.item()) || stack.getCount() < q.count) { + return false; + } + stack.shrink(q.count); + give(player, new ItemStack(q.rewardItem(), q.reward)); + grantVillageReputation(player, q.reward); + player.sendSystemMessage(Component.literal("The villagers thank you. (+" + q.reward + " trust)")); + rollQuest(); + setChanged(); + return true; + } + + public void serverTick(Level level, BlockPos pos, BlockState state) { + // Construction. + if (this.jobType != null) { + tickConstruction(level); + } + if (!this.isClaimed()) { + return; + } + // Passive production from completed buildings. + if (++this.produceTick >= PRODUCE_INTERVAL) { + this.produceTick = 0; + if (this.builtCount >= 1) { + this.outBread = Math.min(OUTPUT_CAP, this.outBread + 1); // Hut → food + } + if (this.builtCount >= 2) { + this.outIngot = Math.min(OUTPUT_CAP, this.outIngot + 1); // Workshop → nerosteel + } + setChanged(); + } + // Config-gated night raids. + if (++this.raidTick >= RAID_INTERVAL) { + this.raidTick = 0; + maybeRaid(level, pos); + } + } + + private void tickConstruction(Level level) { + List list = placements(); + if (this.progress >= list.size()) { + finishJob(); + return; + } + if (++this.buildTick < BUILD_INTERVAL) { + return; + } + this.buildTick = 0; + Placement p = list.get(this.progress++); + BlockPos bp = new BlockPos(this.plotX + p.dx(), this.plotY + p.dy(), this.plotZ + p.dz()); + BlockState bs = switch (p.kind()) { + case WALL, ROOF -> ModBlocks.NEROSTEEL_BLOCK.get().defaultBlockState(); + case LIGHT -> Blocks.GLOWSTONE.defaultBlockState(); + case AIR -> Blocks.AIR.defaultBlockState(); + }; + level.setBlock(bp, bs, 3); + if (level instanceof ServerLevel server) { + server.sendParticles(ParticleTypes.HAPPY_VILLAGER, + bp.getX() + 0.5, bp.getY() + 0.5, bp.getZ() + 0.5, 2, 0.3, 0.3, 0.3, 0.0); + } + setChanged(); + if (this.progress >= list.size()) { + finishJob(); + } + } + + private void maybeRaid(Level level, BlockPos pos) { + if (!NerospaceConfig.alienRaidsEnabled() || !(level instanceof ServerLevel server)) { + return; + } + if (level.getSkyDarken() < DARK_THRESHOLD) { + return; // raids only after dark + } + Player near = level.getNearestPlayer(pos.getX(), pos.getY(), pos.getZ(), RAID_RANGE, false); + if (near == null) { + return; + } + RandomSource rand = level.getRandom(); + int waves = 1 + rand.nextInt(2); + for (int i = 0; i < waves; i++) { + int ox = pos.getX() + (rand.nextBoolean() ? 1 : -1) * (10 + rand.nextInt(8)); + int oz = pos.getZ() + (rand.nextBoolean() ? 1 : -1) * (10 + rand.nextInt(8)); + int oy = level.getHeight(Heightmap.Types.WORLD_SURFACE, ox, oz); + ModEntities.XERTZ_STALKER.get().spawn(server, new BlockPos(ox, oy, oz), EntitySpawnReason.EVENT); + } + } + + private void finishJob() { + this.builtCount++; + this.jobType = null; + this.jobPlacements = null; + this.progress = 0; + setChanged(); + } + + private List placements() { + if (this.jobPlacements == null && this.jobType != null) { + this.jobPlacements = VillageBuildings.build(this.jobType); + } + return this.jobPlacements == null ? List.of() : this.jobPlacements; + } + + private int villageTier(Player player) { + if (this.level == null) { + return 0; + } + AABB box = new AABB(this.worldPosition).inflate(SCAN_RADIUS); + int max = 0; + for (AlienVillager v : this.level.getEntitiesOfClass(AlienVillager.class, box)) { + max = Math.max(max, v.getTier(player)); + } + return max; + } + + private void grantVillageReputation(Player player, int amount) { + if (this.level == null) { + return; + } + AABB box = new AABB(this.worldPosition).inflate(SCAN_RADIUS); + for (AlienVillager v : this.level.getEntitiesOfClass(AlienVillager.class, box)) { + v.addReputation(player, amount); + } + } + + private void rollQuest() { + RandomSource rand = this.level != null ? this.level.getRandom() : RandomSource.create(); + this.questOrdinal = rand.nextInt(Quest.values().length); + } + + private void give(Player player, ItemStack stack) { + if (!player.addItem(stack)) { + player.drop(stack, false); + } + } + + private static String label(Type t) { + return switch (t) { + case HUT -> "Hut"; + case WORKSHOP -> "Workshop"; + }; + } + + private static String itemLabel(Quest q) { + return switch (q) { + case XERTZ_QUARTZ -> "Xertz Quartz"; + case RAW_NEROSTEEL -> "Raw Nerosteel"; + case ALIEN_FRAGMENT -> "Alien Fragment"; + }; + } + + // --- Persistence ---------------------------------------------------------- + + @Override + protected void saveAdditional(ValueOutput output) { + super.saveAdditional(output); + output.putString("Owner", this.owner == null ? "" : this.owner.toString()); + output.putString("OwnerName", this.ownerName); + output.putInt("Stockpile", this.stockpile); + output.putInt("BuiltCount", this.builtCount); + output.putInt("JobType", this.jobType == null ? -1 : this.jobType.ordinal()); + output.putInt("Progress", this.progress); + output.putInt("PlotX", this.plotX); + output.putInt("PlotY", this.plotY); + output.putInt("PlotZ", this.plotZ); + output.putInt("OutBread", this.outBread); + output.putInt("OutIngot", this.outIngot); + output.putInt("Quest", this.questOrdinal); + } + + @Override + protected void loadAdditional(ValueInput input) { + super.loadAdditional(input); + String stored = input.getStringOr("Owner", ""); + if (stored.isEmpty()) { + this.owner = null; + } else { + try { + this.owner = UUID.fromString(stored); + } catch (IllegalArgumentException ex) { + this.owner = null; + } + } + this.ownerName = input.getStringOr("OwnerName", ""); + this.stockpile = input.getIntOr("Stockpile", 0); + this.builtCount = input.getIntOr("BuiltCount", 0); + this.jobType = Type.byOrdinalOrNull(input.getIntOr("JobType", -1)); + this.progress = input.getIntOr("Progress", 0); + this.plotX = input.getIntOr("PlotX", 0); + this.plotY = input.getIntOr("PlotY", 0); + this.plotZ = input.getIntOr("PlotZ", 0); + this.outBread = input.getIntOr("OutBread", 0); + this.outIngot = input.getIntOr("OutIngot", 0); + this.questOrdinal = input.getIntOr("Quest", -1); + this.jobPlacements = null; + } +} diff --git a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json index f593f12..7ca1f7a 100644 --- a/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -300,6 +300,8 @@ "itemGroup.nerospace": "Nerospace", "message.nerospace.greenxertz.no_air": "You are out of oxygen — reach a launch pad or an Oxygen Generator!", "message.nerospace.star_guide.empty": "Place a Star Guide Book on the pedestal to open the guide", + "message.nerospace.village_core.claimed": "You claim this Village Core. The aliens take note.", + "message.nerospace.village_core.owned": "This Village Core belongs to %s.", "pipe.nerospace.face.down": "Bottom", "pipe.nerospace.face.east": "East", "pipe.nerospace.face.north": "North", From 1174f1fc85b99bfb8482a539b316e258dec3c0f9 Mon Sep 17 00:00:00 2001 From: Dario Maselli <117168592+Dario-Maselli@users.noreply.github.com> Date: Mon, 22 Jun 2026 00:54:22 +0200 Subject: [PATCH 82/82] Add Artificer gear: Xertz Resonator & fall-negate Port Artificer gear behaviour into the multiloader common code: add XertzResonatorItem (right-click ore ping using the common c:ores tag), add AlienGearAbilities.negatesFall predicate, and register the Xertz resonator item in ModItems. Add ModTags.Blocks.ORES as the shared ores convention tag. Wire loader-specific fall-damage handling: Fabric registers ServerLivingEntityEvents.ALLOW_DAMAGE to veto fall damage, and NeoForge listens to LivingFallEvent and sets damage multiplier to 0 when negatesFall is true. Update docs checklist to reflect the completed port and gameplay fixes (village trades / grav strider cushioning). --- docs/MULTILOADER_PORT_CHECKLIST.md | 17 ++++++- .../nerospace/gear/AlienGearAbilities.java | 29 +++++++++++ .../nerospace/gear/XertzResonatorItem.java | 50 +++++++++++++++++++ .../neroland/nerospace/registry/ModItems.java | 6 ++- .../neroland/nerospace/registry/ModTags.java | 3 ++ .../nerospace/fabric/NerospaceFabric.java | 7 +++ .../nerospace/neoforge/NerospaceNeoForge.java | 8 +++ 7 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/gear/AlienGearAbilities.java create mode 100644 multiloader/common/src/main/java/za/co/neroland/nerospace/gear/XertzResonatorItem.java diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md index 3bdbef7..f1300f5 100644 --- a/docs/MULTILOADER_PORT_CHECKLIST.md +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -4,6 +4,15 @@ Audit of what the standalone NeoForge mod (`src/main/java`, 264 classes) still n cross-loader `multiloader/` project. As of this audit: **~218 classes ported, ~46 remaining**, all four build cells (NeoForge + Fabric × MC 26.1.2 + 26.2) green. +> **2026-06-22 update — Artificer gear behaviour ported (the village's exclusive trades are now functional).** +> All 4 cells green (`:neoforge:build`+`:fabric:build` on both 26.2 and 26.1.2; ecjCheck 0 errors / 21 +> baseline warnings, 0 new). Added `gear/XertzResonatorItem` (right-click ore-ping over a new `c:ores` +> convention tag — `ModTags.Blocks.ORES`, the cross-loader replacement for NeoForge `Tags.Blocks.ORES`) + +> `gear/AlienGearAbilities` (shared `negatesFall` predicate). Grav Striders' fall-negate is wired through a +> small per-loader event seam: NeoForge `LivingFallEvent.setDamageMultiplier(0)`, Fabric +> `ServerLivingEntityEvents.ALLOW_DAMAGE` vetoing `DamageTypes.FALL`. This is the cross-loader stand-in for +> the root's NeoForge-only `@EventBusSubscriber` `AlienGearEvents`. **~224 classes ported.** + > **2026-06-22 update — Village Core interactive controller ported (closes the last big gameplay gap).** > All 4 cells green (full `:neoforge:build`+`:fabric:build` on **both** 26.2 and 26.1.2; ecjCheck 0 errors / > 21 baseline warnings, 0 new). The decorative `VillageCoreBlock` stub is now the root's full teach-and-grow @@ -559,7 +568,13 @@ checked by a headless build). creative-only travel devices; TOOLS_AND_UTILITIES tab. Assets + 17 lang keys copied. + [x] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` — DONE (advanced-pipes slice A; TOOLS tab). + [ ] `StarGuideBookItem` (depends on **star guide**). -+ [~] `gear/XertzResonatorItem` — ported as a **plain item**; real gear behaviour + `AlienGearEvents` pending. ++ [x] **Artificer gear behaviour DONE (4 cells green).** `gear/XertzResonatorItem` (right-click ore-ping — + reuses the new `c:ores` convention `TagKey` in `ModTags.Blocks.ORES` instead of NeoForge `Tags.Blocks.ORES`, + registered in place of the plain item) + `gear/AlienGearAbilities` (shared `negatesFall` predicate — the + cross-loader stand-in for the root's NeoForge `@EventBusSubscriber` `AlienGearEvents`). Grav Striders' + fall-negate is bound per loader: NeoForge `LivingFallEvent.setDamageMultiplier(0)`, Fabric + `ServerLivingEntityEvents.ALLOW_DAMAGE` vetoing a `DamageTypes.FALL` source (both stable on 26.1.2 + 26.2). + The village system's T4/T5 gear trades are now functional. ### Cross-cutting registries (`registry/`) diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/gear/AlienGearAbilities.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/gear/AlienGearAbilities.java new file mode 100644 index 0000000..17e7494 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/gear/AlienGearAbilities.java @@ -0,0 +1,29 @@ +package za.co.neroland.nerospace.gear; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +import za.co.neroland.nerospace.registry.ModItems; + +/** + * Shared ability logic for the exclusive Artificer gear (ALIEN_VILLAGERS_DESIGN.md §6.1). Loader-agnostic + * predicates the per-loader event hooks call; this is the cross-loader stand-in for the root's + * {@code gear/AlienGearEvents} (a NeoForge {@code @EventBusSubscriber}). Each loader binds its own + * fall-damage event and defers the decision here: + *
    + *
  • NeoForge — {@code LivingFallEvent.setDamageMultiplier(0)} when {@link #negatesFall} is true;
  • + *
  • Fabric — {@code ServerLivingEntityEvents.ALLOW_DAMAGE} cancels a {@code FALL} source when it is.
  • + *
+ */ +public final class AlienGearAbilities { + + private AlienGearAbilities() { + } + + /** Grav Striders: while carried anywhere in the inventory, alien grav-tech cushions the wearer's fall. */ + public static boolean negatesFall(Entity entity) { + return entity instanceof Player player + && player.getInventory().hasAnyMatching((ItemStack s) -> s.is(ModItems.GRAV_STRIDERS.get())); + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/gear/XertzResonatorItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/gear/XertzResonatorItem.java new file mode 100644 index 0000000..89d1ce7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/gear/XertzResonatorItem.java @@ -0,0 +1,50 @@ +package za.co.neroland.nerospace.gear; + +import net.minecraft.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.Item; +import net.minecraft.world.level.Level; + +import za.co.neroland.nerospace.registry.ModTags; + +/** + * Xertz Resonator (ALIEN_VILLAGERS_DESIGN.md §6.1) — an exclusive Artificer trade. Right-click to ping + * the surrounding stone: it reports how many ore blocks lie within range, a cross-mod prospecting aid. + * + *

Cross-loader port: identical to the standalone mod, except the ore test uses the common + * {@code c:ores} convention tag ({@link ModTags.Blocks#ORES}) instead of the NeoForge + * {@code Tags.Blocks.ORES} constant — so it still matches any mod's ores on both loaders.

+ */ +public class XertzResonatorItem extends Item { + + private static final int RADIUS = 8; + + public XertzResonatorItem(Properties properties) { + super(properties); + } + + @Override + public InteractionResult use(Level level, Player player, InteractionHand hand) { + if (!level.isClientSide()) { + BlockPos center = player.blockPosition(); + int count = 0; + BlockPos.MutableBlockPos m = new BlockPos.MutableBlockPos(); + for (int dx = -RADIUS; dx <= RADIUS; dx++) { + for (int dy = -RADIUS; dy <= RADIUS; dy++) { + for (int dz = -RADIUS; dz <= RADIUS; dz++) { + m.set(center.getX() + dx, center.getY() + dy, center.getZ() + dz); + if (level.getBlockState(m).is(ModTags.Blocks.ORES)) { + count++; + } + } + } + } + player.sendSystemMessage(Component.literal( + "Xertz resonance: " + count + " ore blocks within " + RADIUS + " blocks.")); + } + return InteractionResult.SUCCESS; + } +} diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java index fde0d13..a454bab 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -29,6 +29,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.fluid.ModFluids; +import za.co.neroland.nerospace.gear.XertzResonatorItem; import za.co.neroland.nerospace.item.ConfiguratorItem; import za.co.neroland.nerospace.item.DestinationCompassItem; import za.co.neroland.nerospace.item.GreenxertzNavigatorItem; @@ -123,8 +124,9 @@ public final class ModItems { public static final RegistryEntry FRAME_CASING = item("frame_casing"); public static final RegistryEntry GRAV_STRIDERS = item("grav_striders"); public static final RegistryEntry DRIFT_FLEECE = item("drift_fleece"); - /** Trade-only Artificer gear; ported as a plain item (its custom gear behaviour is deferred). */ - public static final RegistryEntry XERTZ_RESONATOR = item("xertz_resonator"); + /** Trade-only Artificer gear: right-click pings nearby ores ({@code c:ores}); see {@link XertzResonatorItem}. */ + public static final RegistryEntry XERTZ_RESONATOR = ITEMS.register("xertz_resonator", + key -> new XertzResonatorItem(new Item.Properties().setId(key))); // --- Universal Pipe tools (per-face I/O modes, item filters, throughput upgrades) ---- public static final RegistryEntry CONFIGURATOR = ITEMS.register("configurator", diff --git a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java index 78c55ad..ff0b21c 100644 --- a/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java @@ -29,6 +29,9 @@ public static final class Blocks { private Blocks() { } + /** The common "all ores" convention tag (any mod's ores) — used by the Xertz Resonator's ore ping. */ + public static final TagKey ORES = blockTag("c", "ores"); + public static final TagKey ORES_NEROSIUM = blockTag("c", "ores/nerosium"); public static final TagKey STORAGE_BLOCKS_NEROSIUM = blockTag("c", "storage_blocks/nerosium"); public static final TagKey STORAGE_BLOCKS_RAW_NEROSIUM = blockTag("c", "storage_blocks/raw_nerosium"); diff --git a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java index 406134f..252197c 100644 --- a/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -2,6 +2,7 @@ import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.biome.v1.BiomeModifications; @@ -13,6 +14,7 @@ import net.minecraft.core.Direction; import net.minecraft.core.registries.Registries; import net.minecraft.resources.Identifier; +import net.minecraft.world.damagesource.DamageTypes; import net.minecraft.resources.ResourceKey; import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.Mob; @@ -25,6 +27,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.command.NerospaceCommands; import za.co.neroland.nerospace.energy.NerospaceEnergyStorage; +import za.co.neroland.nerospace.gear.AlienGearAbilities; import za.co.neroland.nerospace.fluid.NerospaceFluidStorage; import za.co.neroland.nerospace.gas.NerospaceGasStorage; import za.co.neroland.nerospace.meteor.MeteorEvents; @@ -107,6 +110,10 @@ public void register(EntityType type, SpawnPlacementType plac // (Fabric's Load SAM passes a third "newly generated" flag, which we don't need.) ServerChunkEvents.CHUNK_LOAD.register((serverLevel, chunk, newlyGenerated) -> TerraformManager.get(serverLevel).onChunkLoaded(serverLevel, chunk)); + // Artificer gear: Grav Striders cushion the wearer — cancel fall damage while carried + // (counterpart to NeoForge's LivingFallEvent; returning false vetoes the damage). + ServerLivingEntityEvents.ALLOW_DAMAGE.register((entity, source, amount) -> + !(source.is(DamageTypes.FALL) && AlienGearAbilities.negatesFall(entity))); // Item-storage capability (Fabric Transfer API) — counterpart to NeoForge // Capabilities.Item.BLOCK; lets mod pipes move items in/out of the item store. diff --git a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java index 3a138f3..143e6f9 100644 --- a/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -8,6 +8,7 @@ import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.event.entity.EntityAttributeCreationEvent; import net.neoforged.neoforge.event.entity.RegisterSpawnPlacementsEvent; +import net.neoforged.neoforge.event.entity.living.LivingFallEvent; import net.neoforged.neoforge.event.RegisterCommandsEvent; import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.tick.PlayerTickEvent; @@ -23,6 +24,7 @@ import za.co.neroland.nerospace.NerospaceCommon; import za.co.neroland.nerospace.command.NerospaceCommands; +import za.co.neroland.nerospace.gear.AlienGearAbilities; import za.co.neroland.nerospace.meteor.MeteorEvents; import za.co.neroland.nerospace.telemetry.NerospaceTelemetry; import za.co.neroland.nerospace.platform.NeoForgeFluidFactory; @@ -70,6 +72,12 @@ public NerospaceNeoForge(IEventBus modEventBus, ModContainer modContainer) { OxygenFieldEvents.tick(event.getServer()); TerraformDrift.tick(event.getServer()); }); + // Artificer gear: Grav Striders cushion the wearer — negate fall damage while carried. + NeoForge.EVENT_BUS.addListener((LivingFallEvent event) -> { + if (AlienGearAbilities.negatesFall(event.getEntity())) { + event.setDamageMultiplier(0.0F); + } + }); // Creative debug commands (/nerospace gallery) — game-bus command registration. NeoForge.EVENT_BUS.addListener((RegisterCommandsEvent event) -> NerospaceCommands.register(event.getDispatcher()));