diff --git a/.github/workflows/multiloader.yml b/.github/workflows/multiloader.yml new file mode 100644 index 0000000..5b07d7b --- /dev/null +++ b/.github/workflows/multiloader.yml @@ -0,0 +1,77 @@ +name: Multiloader Build + +# 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. All four loader x version cells are verified buildable +# from public artifacts (gradle MCP, 2026-06-20): +# - neoforge @ 26.1.2 -> NeoForge 26.1.2.76 +# - 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.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). + +on: + push: + paths: + - "multiloader/**" + - ".github/workflows/multiloader.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.loader }} @ MC ${{ matrix.mc }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - loader: neoforge + mc: "26.1.2" + - loader: neoforge + mc: "26.2" + - loader: fabric + mc: "26.1.2" + - loader: fabric + mc: "26.2" + + 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 ./multiloader/gradlew + + - name: Build ${{ matrix.loader }} for MC ${{ matrix.mc }} + working-directory: multiloader + run: >- + ./gradlew :${{ 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/.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 b44c3d7..1565198 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,7 +1,6 @@ { - "//": "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 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", - "//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.", "configurations": [ { "type": "java", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d0d7997..3deea39 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.", + "//": "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": [ { @@ -37,6 +37,132 @@ "args": ["prepareDataRun"], "presentation": { "reveal": "silent", "panel": "shared", "clear": false }, "problemMatcher": [] + }, + + { + "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" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:runClient", "-Pminecraft_version=26.2"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "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:runClient", "-Pminecraft_version=26.1.2"], + "presentation": { "reveal": "always", "panel": "dedicated", "clear": true }, + "problemMatcher": [] + }, + { + "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: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": [] + }, + { + "label": "ML: Run NeoForge Client (pick MC)", + "type": "process", + "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}/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: Build both loaders (pick MC)", + "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 }, + "problemMatcher": [] + }, + { + "label": "ML: Build NeoForge (pick MC)", + "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 }, + "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: Generate Fabric VS Code run configs (pick MC)", + "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" }, + "options": { "cwd": "${workspaceFolder}/multiloader" }, + "args": [":fabric:vscode", "-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": [":fabric:build", "-Pminecraft_version=${input:mlMcVersion}", "--refresh-dependencies"], + "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.2" } ] } diff --git a/docs/MULTILOADER.md b/docs/MULTILOADER.md new file mode 100644 index 0000000..7fcb592 --- /dev/null +++ b/docs/MULTILOADER.md @@ -0,0 +1,204 @@ +# 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. + +--- + +## 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-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: 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-20).** The `multiloader/` scaffold now +uses the MultiLoader-Template layout (ModDevGradle `common` on NeoForm + Fabric +Loom `fabric` + ModDevGradle `neoforge`) — architectury-loom is gone. +`./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 + +- [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: + +- 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) diff --git a/docs/MULTILOADER_MIGRATION.md b/docs/MULTILOADER_MIGRATION.md new file mode 100644 index 0000000..9dae1a0 --- /dev/null +++ b/docs/MULTILOADER_MIGRATION.md @@ -0,0 +1,244 @@ +# 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-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 +> (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). +> +> **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.) +> +> **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** +> (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 +> `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 +> 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): **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. + +--- + +## 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). diff --git a/docs/MULTILOADER_PORT_CHECKLIST.md b/docs/MULTILOADER_PORT_CHECKLIST.md new file mode 100644 index 0000000..f1300f5 --- /dev/null +++ b/docs/MULTILOADER_PORT_CHECKLIST.md @@ -0,0 +1,702 @@ +# 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: **~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 +> 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 +> `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 +> 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 +> `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 +> `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. **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 +> 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: +> 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` +> 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 +> 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), +> 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` +> [`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` +> (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/ +> 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 +> 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` +> 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 + +> 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 +> 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 +> (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 +> `(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 +> 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). + +> **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 +> 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 +> 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 +> 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 → +> 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, +> 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 +> 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, +> 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). + +--- + +## ✅ 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) — **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`. **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. **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). ++ [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`, + `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 + 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 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), + 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. ++ [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 + 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.) + + [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`, + 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, + 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).** + + [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, + 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 + 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` + (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. ++ [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) + ++ [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 + `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 + **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) — **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**. ++ [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. ++ [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.) ++ [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` + (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 + 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 + 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 + `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). ++ [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)** + ++ [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*` + `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 + 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. ++ [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)** + ++ [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 + 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. ++ [x] `ConfiguratorItem`, `PipeFilterItem`, `PipeUpgradeItem` — DONE (advanced-pipes slice A; TOOLS tab). ++ [ ] `StarGuideBookItem` (depends on **star guide**). ++ [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/`) + ++ [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: + 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` + (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) — **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 + ++ [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** + ++ [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 + `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()` + (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` + + 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 + (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). + +--- + +## 📡 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. + +--- + +## 🛠️ 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. 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 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. diff --git a/multiloader/.vscode/launch.json b/multiloader/.vscode/launch.json new file mode 100644 index 0000000..de2e32f --- /dev/null +++ b/multiloader/.vscode/launch.json @@ -0,0 +1,126 @@ +{ + "//": "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": [ + { + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "projectName": "neoforge", + "preLaunchTask": "ml-prepare-neoforge-server-26.2", + "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/README.md b/multiloader/README.md new file mode 100644 index 0000000..41b96e7 --- /dev/null +++ b/multiloader/README.md @@ -0,0 +1,127 @@ +# Nerospace multiloader scaffold (MultiLoader-Template) + +A self-contained skeleton for building Nerospace on **both NeoForge and Fabric** +from one shared codebase, on the **de-obfuscated Minecraft 26.x** toolchain. + +> **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). + +## 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.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**. + +## Why this layout (not Architectury) + +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: + +- `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) + +Shared game logic lives **once** in `common`; its source is pulled into `fabric` +and `neoforge` (single copy → no drift). No Architectury API. + +## Layout + +```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 + +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 :common:build :fabric:build -Pminecraft_version=26.2 # verified green +./gradlew :neoforge:build -Pminecraft_version=26.1.2 # NeoForge on 26.1.x + +# default version is gradle.properties -> minecraft_version (26.2); +# -Pminecraft_version selects the matching *_ pins. +``` + +Jars land in `multiloader//build/libs/`. + +## All four cells build (was: NeoForge 26.2 pending) + +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 +git checkout 26.2.x +./gradlew :neoforge:publishToMavenLocal # JDK 25; publishes net.neoforged:neoforge: +``` + +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. + +## CI / VS Code + +- **`.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`). + +## Scope boundary + +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). + +## Promoting to the repo root + +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. + +## Sources + +- [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-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.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 +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/build.gradle b/multiloader/build.gradle new file mode 100644 index 0000000..f6adc4a --- /dev/null +++ b/multiloader/build.gradle @@ -0,0 +1,63 @@ +// ===================================================================== +// 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. +// ===================================================================== + +allprojects { + group = rootProject.mod_group_id + version = rootProject.mod_version +} + +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) + } + + tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + 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/' } + maven { url 'https://maven.neoforged.net/releases/' } + maven { name = 'BlameJared'; url = 'https://maven.blamejared.com/' } // JEI etc. (later) + } + + // 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 +// 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 new file mode 100644 index 0000000..884891c --- /dev/null +++ b/multiloader/common/build.gradle @@ -0,0 +1,28 @@ +// 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). + +plugins { + id 'net.neoforged.moddev' +} + +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}") + + def at = file('src/main/resources/META-INF/accesstransformer.cfg') + if (at.exists()) { + 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/NerospaceCommon.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java new file mode 100644 index 0000000..0515f07 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/NerospaceCommon.java @@ -0,0 +1,34 @@ +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()} 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 { + + 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()); + + // 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/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/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/ClientBlockEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java new file mode 100644 index 0000000..6897976 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientBlockEntityRenderers.java @@ -0,0 +1,33 @@ +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()); + // 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/ClientEntityRenderers.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java new file mode 100644 index 0000000..a38743b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/ClientEntityRenderers.java @@ -0,0 +1,69 @@ +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"))); + 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"))); + 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) { + 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..eb97d2c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/RocketScreen.java @@ -0,0 +1,147 @@ +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 SpaceButton stationButton; + 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; + } + + // 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); + } + + @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.stationButton != null) { + boolean stationDest = this.menu.isStationDestination(); + this.stationButton.visible = stationDest; + this.stationButton.active = stationDest; + if (stationDest) { + this.stationButton.setMessage( + Component.literal("Dock: " + ClientStations.name(this.menu.getStationSlot()))); + } + } + 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 onCycleStation() { + sendButton(RocketMenu.BUTTON_CYCLE_STATION); + } + + 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/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/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/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/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/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..3b49f32 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/StarGuideScreen.java @@ -0,0 +1,192 @@ +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; + // 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 + 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 (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 && (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); + } + } + + // 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/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/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/client/TexturedContainerScreen.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java new file mode 100644 index 0000000..feb5228 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/client/TexturedContainerScreen.java @@ -0,0 +1,139 @@ +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 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). + */ + 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/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/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/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/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..2fda5c2 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/config/NerospaceConfig.java @@ -0,0 +1,184 @@ +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"; + 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"; + 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; + 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; + /** 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; + /** 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; + /** 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() { + } + + public static boolean isTelemetryEnabled() { + return telemetryEnabled; + } + + public static double energyRateMultiplier() { + return energyRateMultiplier; + } + + public static double oxygenDrainMultiplier() { + return oxygenDrainMultiplier; + } + + public static double oxygenCapacityMultiplier() { + return oxygenCapacityMultiplier; + } + + public static double fuelCostMultiplier() { + return fuelCostMultiplier; + } + + 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 + * {@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). + */ + 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) { + 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()); + 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)); + fuelCostMultiplier = clampMultiplier(parseDouble( + 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); + } + } else { + write(file); + } + } + + /** 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)); + 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)); + props.setProperty(KEY_ALIEN_RAIDS, Boolean.toString(alienRaidsEnabled)); + 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. " + + "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. 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/energy/EnergyBuffer.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java new file mode 100644 index 0000000..5e82ec5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/energy/EnergyBuffer.java @@ -0,0 +1,78 @@ +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; + } + + /** 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(); + } + } + + /** 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; + } + + 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/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/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/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/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/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/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/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/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/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/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/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/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/item/ConfiguratorItem.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java new file mode 100644 index 0000000..a4dff99 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/item/ConfiguratorItem.java @@ -0,0 +1,98 @@ +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 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) { + 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/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/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/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/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/machine/CombustionGeneratorBlock.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.java new file mode 100644 index 0000000..6fba8d1 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlock.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; + +/** 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); + } + + @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) { + 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..e45590d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/CombustionGeneratorBlockEntity.java @@ -0,0 +1,223 @@ +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.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; +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.config.NerospaceConfig; +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; + +/** + * 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, MenuProvider { + + 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; + + /** 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); + } + + 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(NerospaceConfig.scale(FE_PER_TICK, NerospaceConfig.energyRateMultiplier())); + } + } 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)); + } + + // --- 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) { + 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/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..2dd08a0 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/FuelRefineryBlockEntity.java @@ -0,0 +1,268 @@ +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.config.NerospaceConfig; +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 -> NerospaceConfig.scaleInterval(WORK_TICKS, NerospaceConfig.machineSpeedMultiplier()); + 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 >= NerospaceConfig.scaleInterval(WORK_TICKS, NerospaceConfig.machineSpeedMultiplier())) { + 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/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/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..273189b --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/HydrationModuleBlockEntity.java @@ -0,0 +1,244 @@ +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.config.NerospaceConfig; +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() % NerospaceConfig.scaleInterval( + WORK_INTERVAL_TICKS, NerospaceConfig.machineSpeedMultiplier()) != 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/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/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..eec98d9 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/NerosiumGrinderBlockEntity.java @@ -0,0 +1,220 @@ +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.config.NerospaceConfig; +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 -> NerospaceConfig.scaleInterval(MAX_PROGRESS, NerospaceConfig.machineSpeedMultiplier()); + 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 >= NerospaceConfig.scaleInterval(MAX_PROGRESS, NerospaceConfig.machineSpeedMultiplier())) { + 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/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..7372e06 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/OxygenGeneratorBlockEntity.java @@ -0,0 +1,127 @@ +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; +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; +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). 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 { + + 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; + /** 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); + + 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; + } + // 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) { + 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); + } + } + } + + @Override + public void setRemoved() { + if (this.level instanceof ServerLevel serverLevel) { + OxygenFieldManager.get(serverLevel).removeSource(this.worldPosition); + } + super.setRemoved(); + } + + @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/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..386e788 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/PassiveGeneratorBlockEntity.java @@ -0,0 +1,193 @@ +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.config.NerospaceConfig; +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(NerospaceConfig.scale(FE_PER_TICK, NerospaceConfig.energyRateMultiplier())); + } + } + + @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/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 new file mode 100644 index 0000000..21afa84 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlock.java @@ -0,0 +1,183 @@ +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.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}. 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 multiblock + * placement/teardown is plain vanilla block API.

+ */ +public class SolarPanelBlock extends BaseEntityBlock { + + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + instance.group( + SolarTier.CODEC.fieldOf("tier").forGetter(SolarPanelBlock::tier), + 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() { + return this.tier; + } + + @Override + protected MapCodec codec() { + return CODEC; + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(ANCHOR); + } + + @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)); + } + + // --- 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; + } + + @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 new file mode 100644 index 0000000..cf002b4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/SolarPanelBlockEntity.java @@ -0,0 +1,183 @@ +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 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; + +/** + * 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 { + + /** 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; + + /** 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); + } + + /** 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() { + SolarPanelBlockEntity anchor = anchorEntity(); + return anchor != null ? anchor.energy : 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 instanceof ServerLevel server) || !isAnchor()) { + 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).

+ */ + /** 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 + } + if (!level.canSeeSky(pos.above())) { + return 0.0F; // roofed over — no sun reaches the panel + } + 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/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/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/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/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..8c41853 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/TerraformerBlockEntity.java @@ -0,0 +1,525 @@ +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.config.NerospaceConfig; +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() % NerospaceConfig.scaleInterval( + WORK_INTERVAL_TICKS, NerospaceConfig.machineSpeedMultiplier()) == 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/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..ec62f12 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryControllerBlockEntity.java @@ -0,0 +1,800 @@ +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.core.registries.Registries; +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.item.enchantment.Enchantments; +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.config.NerospaceConfig; +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.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; + +/** + * 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 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); + /** 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; + 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; + } + }; + + @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.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 --------------------------------------------------- + + 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 = quarryEnergyPerBlock(); + ItemStack tool = miningTool(level); + 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() * this.modules.speedMultiplier() * planet + * NerospaceConfig.machineSpeedMultiplier(); + 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<>(); + 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)); + } + this.modules.save(output); + 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.modules.load(input); + 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, this.moduleSlots); + } + + // --- 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) { + 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 canPlaceItem(slot, stack); + } + + @Override + public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction side) { + return slot > this.moduleSlots; // output slots only + } + + @Override + public boolean canPlaceItem(int slot, ItemStack stack) { + 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 + public int getContainerSize() { + return this.containerSize; + } + + @Override + public boolean isEmpty() { + for (ItemStack stack : this.items) { + if (!stack.isEmpty()) { + return false; + } + } + for (ItemStack stack : this.modules.items()) { + if (!stack.isEmpty()) { + return false; + } + } + return true; + } + + @Override + public ItemStack getItem(int slot) { + return routeList(slot).get(routeIndex(slot)); + } + + @Override + public ItemStack removeItem(int slot, int amount) { + ItemStack r = ContainerHelper.removeItem(routeList(slot), routeIndex(slot), amount); + if (!r.isEmpty()) { + this.setChanged(); + } + return r; + } + + @Override + public ItemStack removeItemNoUpdate(int slot) { + return ContainerHelper.takeItem(routeList(slot), routeIndex(slot)); + } + + @Override + public void setItem(int slot, ItemStack stack) { + routeList(slot).set(routeIndex(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(); + this.modules.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..60eaf65 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/machine/quarry/QuarryMenu.java @@ -0,0 +1,176 @@ +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.module.UpgradeModuleItem; +import za.co.neroland.nerospace.registry.ModItems; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * 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 TIER1_MODULE_SLOTS = 1; + + private final Container container; + private final ContainerData data; + private final int machineSlots; + private final int moduleSlots; + + /** Client constructor (Tier-1 layout). */ + public QuarryMenu(int containerId, Inventory playerInventory) { + this(containerId, playerInventory, + 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, int moduleSlots) { + super(ModMenuTypes.QUARRY_CONTROLLER.get(), containerId); + 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 + moduleSlots; + 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 = this.machineSlots; + int playerEnd = this.machineSlots + 36; + + if (index < this.machineSlots) { + 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 if (UpgradeModuleItem.isModule(raw)) { + if (!this.moveItemStackTo(raw, 1, 1 + this.moduleSlots, 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 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); + } + + @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/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/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/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/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/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/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/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/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/meteor/FallingMeteorEntity.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java new file mode 100644 index 0000000..0c01cd0 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/FallingMeteorEntity.java @@ -0,0 +1,239 @@ +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); + + // Flip the matching scheduled site to LANDED (or add a transient one for a creative-called meteor). + MeteorEventManager.get(level).onImpact(center); + + 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/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..d75c233 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/meteor/MeteorEvents.java @@ -0,0 +1,74 @@ +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.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 + * 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); + + /** 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, and syncs the tracker readout. */ + public static void tick(MinecraftServer server) { + for (ServerLevel level : server.getAllLevels()) { + 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/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/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/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/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 new file mode 100644 index 0000000..813f3ee --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/network/ModNetwork.java @@ -0,0 +1,91 @@ +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() { + // 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); + // 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/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/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/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 new file mode 100644 index 0000000..abe97a4 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlock.java @@ -0,0 +1,64 @@ +package za.co.neroland.nerospace.pipe; + +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.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} relay; sneak-empty-hand pops upgrades. */ +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; + } + + @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) { + 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..b3dd1d0 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/pipe/UniversalPipeBlockEntity.java @@ -0,0 +1,596 @@ +package za.co.neroland.nerospace.pipe; + +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.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; +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; + +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.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.menu.PipeConfigMenu; +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; + +/** + * 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 implements WorldlyContainer, MenuProvider { + + 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 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}; + + /** 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 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. */ + 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() { + return this.energy; + } + + 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) { + 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(); + } + + // --- 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) { + 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; + } + relayEnergy(level, pos); + relayGas(level, pos); + relayFluid(level, pos); + relayItems(level, pos); + } + + 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, io), false); + if (moved > 0) { + this.energy.insert(moved, false); + } + } + } + for (Direction dir : Direction.values()) { + 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(), io), true); + long accepted = neighbour.insert(offered, false); + if (accepted > 0) { + this.energy.extract(accepted, false); + } + } + } + + 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; + } + GasResource ngas = neighbour.getGas(); + if (ngas.isEmpty() || (!this.gas.getGas().isEmpty() && this.gas.getGas() != ngas)) { + continue; + } + long available = neighbour.drain(Math.min(room, io), true); + long moved = this.gas.fill(ngas, available, false); + if (moved > 0) { + neighbour.drain(moved, false); + } + } + for (Direction dir : Direction.values()) { + 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(), io), true); + long accepted = neighbour.fill(g, offered, false); + if (accepted > 0) { + this.gas.drain(accepted, false); + } + } + } + + 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 + // (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)) { + for (int i = 0; i < perTick; i++) { + if (!moveOneFiltered(src, dir.getOpposite(), this, dir, dir)) { + break; + } + } + } + } + // 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) { + for (int i = 0; i < perTick; i++) { + if (!moveOneFiltered(this, dir, dst, dir.getOpposite(), dir)) { + break; + } + } + } + } + } + + 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}, + * 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; + } + 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()); + 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)); + } + } + // 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 + 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)); + } + 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) ------------------------------------- + + @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/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/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/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/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/platform/IPlatformHelper.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java new file mode 100644 index 0000000..645ae12 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/IPlatformHelper.java @@ -0,0 +1,71 @@ +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(); + + /** 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. + + /** 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); + + // --- 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); + + // --- 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/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 new file mode 100644 index 0000000..40b885d --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/platform/Services.java @@ -0,0 +1,33 @@ +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); + + public static final NetworkPlatform NETWORK = load(NetworkPlatform.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/progression/StarGuide.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java new file mode 100644 index 0000000..8cafaf5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuide.java @@ -0,0 +1,123 @@ +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"), + 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"), + 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/StarGuideGrants.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java new file mode 100644 index 0000000..a6dfef7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideGrants.java @@ -0,0 +1,60 @@ +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"); + } + } + + /** 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) { + 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/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..57be91c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/progression/StarGuideMenu.java @@ -0,0 +1,126 @@ +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; +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.platform.Services; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** + * 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: 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 * 2; + + 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 + seen attachment. */ + @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 + } + + /** 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; + } + + 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; + + ProgressData(ServerPlayer player) { + this.player = player; + } + + @Override + public int get(int index) { + 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 + 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 new file mode 100644 index 0000000..35c8af7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlockEntities.java @@ -0,0 +1,155 @@ +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.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.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.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; +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; +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; +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 + * {@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()))); + + public static final RegistryEntry> BATTERY = + 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()))); + + public static final RegistryEntry> COMBUSTION_GENERATOR = + 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()))); + + public static final RegistryEntry> PASSIVE_GENERATOR = + 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()))); + + 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()))); + + 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(), ModBlocks.SOLAR_PANEL_T2.get(), ModBlocks.SOLAR_PANEL_T3.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()))); + + 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()))); + + public static final RegistryEntry> QUARRY_LANDMARK = + 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()))); + + public static final RegistryEntry> TERRAFORMER = + 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()))); + + public static final RegistryEntry> TERRAFORM_MONITOR = + 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()))); + + public static final RegistryEntry> STATION_CORE = + 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() { + } + + 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 new file mode 100644 index 0000000..2d0d2de --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModBlocks.java @@ -0,0 +1,302 @@ +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.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.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; +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; +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.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; +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; +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; + +/** + * 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); + + // --- 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)); + + // --- 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 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)); + /** 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))); + + /** 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() + .setId(key) + .mapColor(MapColor.METAL) + .strength(3.0F, 6.0F) + .requiresCorrectToolForDrops() + .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())); + + + 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())); + + + 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))); + + + 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))); + + + 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))); + + + 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())); + + + 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())); + + 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) + .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 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 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 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 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(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() + .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))); + + // --- 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() + .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", + 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)))); + } + + private ModBlocks() { + } + + public static void init() { + } +} 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/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/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/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..59a00ac --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntities.java @@ -0,0 +1,99 @@ +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.AlienVillager; +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.meteor.FallingMeteorEntity; +import za.co.neroland.nerospace.rocket.RocketEntity; +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}; natural-spawn placement rules from + * {@link ModSpawnPlacements}; renderers from {@code client/ClientEntityRenderers}. + */ +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)); + + 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)); + + 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)); + + 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)); + + 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() { + } + + 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..fae21eb --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModEntityAttributes.java @@ -0,0 +1,46 @@ +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.AlienVillager; +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; + +/** + * 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()); + 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()); + sink.accept(ModEntities.ALIEN_VILLAGER.get(), AlienVillager.createAttributes()); + } + + private ModEntityAttributes() { + } +} 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 new file mode 100644 index 0000000..a454bab --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModItems.java @@ -0,0 +1,343 @@ +package za.co.neroland.nerospace.registry; + +import java.util.ArrayList; +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.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.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; +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; + +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; +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.item.StationCharterItem; +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; +import za.co.neroland.nerospace.rocket.RocketTier; +import za.co.neroland.nerospace.registry.RegistrationProvider.RegistryEntry; + +/** + * Item registrations shared by both loaders, plus the creative-tab grouping the + * 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); + + // --- 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); + 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 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); + 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); + 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 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 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); + 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"); + 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"); + 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"); + /** 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"); + /** 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", + 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)); + + // --- 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); + 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)); + 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)); + + // --- 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)); + + /** 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))); + + /** 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); + 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")); + + 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 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 RegistryEntry spawnEgg(String name, RegistryEntry> type) { + return ITEMS.register(name, key -> 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)); + } + + 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(), + 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(), + 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(), + VILLAGE_CORE_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(), + 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(), + 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(), + 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(), + 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(), + 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()), + 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(), 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())); + } + + /** + * 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() { + } + + public static void init() { + } +} 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..1e55016 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModMenuTypes.java @@ -0,0 +1,81 @@ +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.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.PipeConfigMenu; +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; + +/** 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)); + + public static final RegistryEntry> NEROSIUM_GRINDER = + 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)); + + public static final RegistryEntry> ROCKET = + 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)); + + public static final RegistryEntry> QUARRY_CONTROLLER = + 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)); + + public static final RegistryEntry> HYDRATION_MODULE = + 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)); + + public static final RegistryEntry> STAR_GUIDE = + MENUS.register("star_guide", + key -> new MenuType<>(StarGuideMenu::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 new file mode 100644 index 0000000..fc0799a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModRegistries.java @@ -0,0 +1,27 @@ +package za.co.neroland.nerospace.registry; + +/** + * Aggregates the cross-loader content registries. Called once from + * {@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 { + + private ModRegistries() { + } + + public static void init() { + ModSounds.init(); + ModDataComponents.init(); + za.co.neroland.nerospace.fluid.ModFluids.init(); + ModBlocks.init(); + ModItems.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/registry/ModSounds.java b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java new file mode 100644 index 0000000..d7f1fc5 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModSounds.java @@ -0,0 +1,62 @@ +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"); + + 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)); + } + + private ModSounds() { + } + + public static void init() { + } +} 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/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..ff0b21c --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/registry/ModTags.java @@ -0,0 +1,97 @@ +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() { + } + + /** 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"); + + 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/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/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..82e118a --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketEntity.java @@ -0,0 +1,660 @@ +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); + /** 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; + /** 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, + * [5]=stationSlot (−1 = origin; only meaningful for the Orbital Station destination). + */ + 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(); + case 5 -> getStationSlot(); + default -> 0; + }; + } + + @Override + public void set(int index, int value) { + // Read-only from the client. + } + + @Override + public int getCount() { + return 6; + } + }; + + /** 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); + builder.define(DATA_STATION, -1); + } + + 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)); + } + } + + // --- 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); + } + + 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)) { + // 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())) { + 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) { + // 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); + } + } + 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())); + 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)); + 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.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)); + 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..1fbdcb8 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketMenu.java @@ -0,0 +1,214 @@ +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.ModDimensions; +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; + /** 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 = 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 + + 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 == BUTTON_CYCLE_STATION) { + current.cycleStation(); + 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; + } + + // --- 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) { + 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..e208020 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/rocket/RocketTier.java @@ -0,0 +1,94 @@ +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.config.NerospaceConfig; +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 (config-scaled, clamped to the tank so a launch is always possible). */ + public int fuelPerLaunch() { + return Math.min(NerospaceConfig.scale(this.fuelPerLaunch, NerospaceConfig.fuelCostMultiplier()), + 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/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/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/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/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/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/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/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/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/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/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/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/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 new file mode 100644 index 0000000..ec3efe7 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/village/VillageCoreBlock.java @@ -0,0 +1,123 @@ +package za.co.neroland.nerospace.village; + +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 (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: 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 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/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/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/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..b9491f3 --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenFieldEvents.java @@ -0,0 +1,62 @@ +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.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; + +/** + * 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; + /** 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, 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; + } + 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/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 new file mode 100644 index 0000000..982300f --- /dev/null +++ b/multiloader/common/src/main/java/za/co/neroland/nerospace/world/OxygenManager.java @@ -0,0 +1,249 @@ +package za.co.neroland.nerospace.world; + +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; +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 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; +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 + * 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 { + + /** 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; + /** 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( + 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 = NerospaceConfig.scale(suited ? OXYGEN_SUIT_MAX : OXYGEN_MAX, + NerospaceConfig.oxygenCapacityMultiplier()); + + 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 { + // An uncountered dimension hazard (Cindara heat / Glacira cold) multiplies the drain. + 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); + } + 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: 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) { + // 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; + } + for (BlockPos pos : BlockPos.betweenClosed( + center.offset(-SAFE_RADIUS, -SAFE_RADIUS, -SAFE_RADIUS), + center.offset(SAFE_RADIUS, SAFE_RADIUS, SAFE_RADIUS))) { + if (level.getBlockState(pos).is(ModBlocks.ROCKET_LAUNCH_PAD.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; + } + + // --- 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/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/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/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); + } + } +} 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/blockstates/solar_panel.json b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json new file mode 100644 index 0000000..cbc6443 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel.json @@ -0,0 +1,10 @@ +{ + "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 new file mode 100644 index 0000000..8be8067 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t2.json @@ -0,0 +1,10 @@ +{ + "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 new file mode 100644 index 0000000..97742a7 --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/blockstates/solar_panel_t3.json @@ -0,0 +1,10 @@ +{ + "variants": { + "anchor=true": { + "model": "nerospace:block/solar_panel_t3" + }, + "anchor=false": { + "model": "nerospace:block/solar_panel_t3" + } + } +} 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/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/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/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/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/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/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/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/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/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/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_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_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_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_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_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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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_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/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/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 new file mode 100644 index 0000000..7ca1f7a --- /dev/null +++ b/multiloader/common/src/main/resources/assets/nerospace/lang/en_us.json @@ -0,0 +1,319 @@ +{ + "block.nerospace.alien_bricks": "Alien Bricks", + "block.nerospace.alien_crystal_block": "Alien Crystal Block", + "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.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", + "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", + "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", + "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", + "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.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", + "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", + "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", + "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", + "block.nerospace.terraformer": "Terraformer", + "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", + "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", + "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", + "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.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.xertz_stalker": "Xertz Stalker", + "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", + "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", + "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", + "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", + "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", + "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", + "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.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", + "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.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", + "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.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_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", + "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.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", + "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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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_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/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/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/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/block/alien_bricks.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_bricks.png new file mode 100644 index 0000000..6ebdefd Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_bricks.png differ 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 0000000..3a51668 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_crystal_block.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_lamp.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_lamp.png new file mode 100644 index 0000000..da03f65 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_lamp.png differ 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 0000000..45903db Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_pillar.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_tile.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_tile.png new file mode 100644 index 0000000..c884d33 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/alien_tile.png differ 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 0000000..70eb501 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/battery.png differ 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 0000000..ebf6e05 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/battery_top.png differ 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 0000000..a27a36c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_block.png differ 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 0000000..3d6c631 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/cindrite_ore.png differ 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 0000000..8ac0c01 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator.png differ 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 0000000..0162269 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_front.png differ 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 0000000..92f122c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/combustion_generator_top.png differ 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 0000000..8a08963 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/cracked_alien_bricks.png differ 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 0000000..8d4814c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery.png differ 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 0000000..43b02c0 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_battery_top.png differ 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 0000000..d8ba8f9 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank.png differ 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 0000000..5052b0f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_fluid_tank_core.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank.png new file mode 100644 index 0000000..676aa38 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank_core.png new file mode 100644 index 0000000..e079d30 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_gas_tank_core.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_item_store.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_item_store.png new file mode 100644 index 0000000..45f25cb Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/creative_item_store.png differ 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 0000000..f49f53a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/deepslate_nerosium_ore.png differ 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 0000000..2b00487 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank_core.png new file mode 100644 index 0000000..5052b0f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fluid_tank_core.png differ 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 0000000..92d5e6a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_refinery.png differ 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 0000000..8966baa Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank_core.png new file mode 100644 index 0000000..02894a6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/fuel_tank_core.png differ 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 0000000..0c76d17 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank_core.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank_core.png new file mode 100644 index 0000000..e079d30 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/gas_tank_core.png differ 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 0000000..8671021 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_block.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_ore.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_ore.png new file mode 100644 index 0000000..1648064 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/glacite_ore.png differ 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 0000000..013f4b3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module.png differ 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 0000000..e65079d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_front.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_top.png new file mode 100644 index 0000000..c4eaf5f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/hydration_module_top.png differ 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 0000000..ab1442b Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store.png differ 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 0000000..a6a6296 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/item_store_top.png differ 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 0000000..7b01e1a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry.png differ 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 0000000..1d764ca Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/launch_gantry_top.png differ 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 0000000..19d2727 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_core.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_rock.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_rock.png new file mode 100644 index 0000000..0df9be9 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/meteor_rock.png differ 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 0000000..6c0f037 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_block.png differ 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 0000000..6afe2f1 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_front.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_front.png new file mode 100644 index 0000000..aed7b6d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_front.png differ 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 0000000..7ff06dc Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_grinder_top.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_ore.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_ore.png new file mode 100644 index 0000000..a501346 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosium_ore.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_block.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_block.png new file mode 100644 index 0000000..3370068 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_block.png differ 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 0000000..4a7e31c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/nerosteel_ore.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator.png new file mode 100644 index 0000000..63cccbc Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator.png differ 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 0000000..c64dbf3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/oxygen_generator_top.png differ 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 0000000..e406e33 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator.png differ 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 0000000..524d5f3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/passive_generator_top.png differ 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 0000000..d7c05b1 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_controller.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_drill.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_drill.png new file mode 100644 index 0000000..ee9768d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_drill.png differ 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 0000000..87be25c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_frame.png differ 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 0000000..f349f94 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_gantry.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_landmark.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_landmark.png new file mode 100644 index 0000000..617692e Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/quarry_landmark.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/raw_nerosium_block.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/raw_nerosium_block.png new file mode 100644 index 0000000..48f740a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/raw_nerosium_block.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel.png new file mode 100644 index 0000000..e7bb5c6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_flow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_flow.png new file mode 100644 index 0000000..91d7c1d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_flow.png differ 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 0000000..f1a6d5b Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_fuel_still.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_launch_pad.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_launch_pad.png new file mode 100644 index 0000000..422e9e7 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/rocket_launch_pad.png differ 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 0000000..33fc25c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_base.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_base.png new file mode 100644 index 0000000..1416158 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_base.png differ 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 0000000..80f6b25 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2.png differ 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 0000000..f82e4ca Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t2_base.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png new file mode 100644 index 0000000..2c2bd35 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3.png differ 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 0000000..81d869a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/solar_panel_t3_base.png differ 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 0000000..444639e Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/star_guide.png differ 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 0000000..e2d3ba0 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_core.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_floor.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_floor.png new file mode 100644 index 0000000..c3126bb Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_floor.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_wall.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_wall.png new file mode 100644 index 0000000..16a29b6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/station_wall.png differ 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 0000000..98ff520 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor.png differ 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 0000000..4d4c15f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraform_monitor_front.png differ 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 0000000..9d9a847 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer.png differ 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 0000000..d89e1dc Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_front.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_top.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_top.png new file mode 100644 index 0000000..feee181 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/terraformer_top.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/block/trash_can.png b/multiloader/common/src/main/resources/assets/nerospace/textures/block/trash_can.png new file mode 100644 index 0000000..d29be5f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/trash_can.png differ 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 0000000..d2d2283 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/universal_pipe.png differ 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 0000000..ced3aa0 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/village_core.png differ 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 0000000..8f7a9dc Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/block/xertz_quartz_ore.png differ 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 0000000..67160fd Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager.png differ 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 0000000..27a03fb Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_cindara.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glacira.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glacira.png new file mode 100644 index 0000000..1d2a9e2 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glacira.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glow.png new file mode 100644 index 0000000..463cdca Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_glow.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_meadow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_meadow.png new file mode 100644 index 0000000..d53e23c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/alien_villager_meadow.png differ 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 0000000..8a5f794 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker.png differ 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 0000000..2f0ae50 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/cinder_stalker_glow.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter.png new file mode 100644 index 0000000..9c0df97 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter_glow.png new file mode 100644 index 0000000..d05c574 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ember_strutter_glow.png differ 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 0000000..f8944bb Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit.png differ 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 0000000..4cf23b1 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_cold.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_heat.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_heat.png new file mode 100644 index 0000000..0ad994e Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_heat.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_t2.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_t2.png new file mode 100644 index 0000000..cdffd6f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid/oxygen_suit_t2.png differ 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 0000000..63694c3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit.png differ 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 0000000..f7eab40 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_cold.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_heat.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_heat.png new file mode 100644 index 0000000..e1d54d4 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_heat.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_t2.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_t2.png new file mode 100644 index 0000000..25e0265 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/equipment/humanoid_leggings/oxygen_suit_t2.png differ 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 0000000..52a76a0 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/falling_meteor.png differ 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 0000000..7cf554c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider_glow.png new file mode 100644 index 0000000..2cf6c63 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/frost_strider_glow.png differ 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 0000000..4e26b2d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling.png differ 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 0000000..2351765 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/greenling_glow.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper.png new file mode 100644 index 0000000..3ebe889 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper_glow.png new file mode 100644 index 0000000..92b9d84 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/meadow_loper_glow.png differ 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 0000000..233d187 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler.png differ 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 0000000..2901aa4 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/quartz_crawler_glow.png differ 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 0000000..fbe19c6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t1.png differ 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 0000000..b93f301 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t2.png differ 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 0000000..dc9aba5 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t3.png differ 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 0000000..e5e9783 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/rocket_t4.png differ 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 0000000..81bbf48 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden_glow.png new file mode 100644 index 0000000..79039b0 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/ruin_warden_glow.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift.png new file mode 100644 index 0000000..09df6ea Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift_glow.png new file mode 100644 index 0000000..144d7f5 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/woolly_drift_glow.png differ 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 0000000..3585039 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker_glow.png b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker_glow.png new file mode 100644 index 0000000..74a3f6e Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/entity/xertz_stalker_glow.png differ 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 0000000..69be3c3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_refinery.png differ 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 0000000..c5df507 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/fuel_tank.png differ 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 0000000..a823412 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/hydration_module.png differ 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 0000000..28b22b3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/quarry.png differ 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 0000000..576d988 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/rocket.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/star_guide.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/star_guide.png new file mode 100644 index 0000000..dad3d9e Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/star_guide.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraform_monitor.png b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraform_monitor.png new file mode 100644 index 0000000..646891d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraform_monitor.png differ 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 0000000..35e56fc Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/gui/terraformer.png differ 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 0000000..0c0c061 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_core.png differ 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 0000000..36c19d1 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_fragment.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png new file mode 100644 index 0000000..2b422fc Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_tech_scrap.png differ 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 0000000..c3de1db Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/alien_villager_spawn_egg.png differ 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 0000000..d311223 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/capacity_upgrade.png differ 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 0000000..b7c1cb6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/cindara_compass.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/cinder_stalker_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/cinder_stalker_spawn_egg.png new file mode 100644 index 0000000..5a884a6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/cinder_stalker_spawn_egg.png differ 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 0000000..30a0dc5 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/cindrite.png differ 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 0000000..7212122 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/configurator.png differ 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 0000000..3eebfd4 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/drift_fleece.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/efficiency_module.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/efficiency_module.png new file mode 100644 index 0000000..3ae35f6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/efficiency_module.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/ember_strutter_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/ember_strutter_spawn_egg.png new file mode 100644 index 0000000..7ca21a1 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/ember_strutter_spawn_egg.png differ 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 0000000..21bb95f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/fortune_module.png differ 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 0000000..17dba83 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/frame_casing.png differ 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 0000000..837ce0f Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/frost_strider_spawn_egg.png differ 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 0000000..3d67681 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/glacira_compass.png differ 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 0000000..cfd9422 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/glacite.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/grav_striders.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/grav_striders.png new file mode 100644 index 0000000..f7d82b6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/grav_striders.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenling_spawn_egg.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenling_spawn_egg.png new file mode 100644 index 0000000..92521bb Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenling_spawn_egg.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_compass.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_compass.png new file mode 100644 index 0000000..66dd881 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_compass.png differ 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 0000000..1ca2720 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/greenxertz_navigator.png differ 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 0000000..4ec0299 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/meadow_loper_spawn_egg.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_caller.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_caller.png new file mode 100644 index 0000000..5326867 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_caller.png differ 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 0000000..59371e1 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/meteor_tracker.png differ 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 0000000..80d4a20 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_dust.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_ingot.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_ingot.png new file mode 100644 index 0000000..98f95ec Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_ingot.png differ 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 0000000..9dc0cd3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosium_pickaxe.png differ 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 0000000..71a3ba3 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/nerosteel_ingot.png differ 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 0000000..6d769ff Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_boots.png differ 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 0000000..8286768 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_chestplate.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_boots.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_boots.png new file mode 100644 index 0000000..f6c6ff6 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_boots.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_chestplate.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_chestplate.png new file mode 100644 index 0000000..375c7fa Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_chestplate.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_helmet.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_helmet.png new file mode 100644 index 0000000..1d6959c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_helmet.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_leggings.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_leggings.png new file mode 100644 index 0000000..92ccb3d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_cold_leggings.png differ 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 0000000..abed14e Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_boots.png differ 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 0000000..2cce18a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_chestplate.png differ 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 0000000..943a613 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_helmet.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_leggings.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_leggings.png new file mode 100644 index 0000000..f6239a4 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_heat_leggings.png differ 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 0000000..0d63250 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_helmet.png differ 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 0000000..4e0cbed Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_leggings.png differ 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 0000000..bb85a5d Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_boots.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_chestplate.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_chestplate.png new file mode 100644 index 0000000..f6e04a8 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_chestplate.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_helmet.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_helmet.png new file mode 100644 index 0000000..d69fc79 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_helmet.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_leggings.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_leggings.png new file mode 100644 index 0000000..3d23103 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/oxygen_suit_t2_leggings.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/pipe_filter.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/pipe_filter.png new file mode 100644 index 0000000..0bb1291 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/pipe_filter.png differ 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 0000000..ad0f6ca Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/quartz_crawler_spawn_egg.png differ 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 0000000..5b9541b Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosium.png differ 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 0000000..77eba65 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/raw_nerosteel.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_bucket.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_bucket.png new file mode 100644 index 0000000..2b2ad37 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_bucket.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_canister.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_canister.png new file mode 100644 index 0000000..5c1f575 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_fuel_canister.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_1.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_1.png new file mode 100644 index 0000000..c374195 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_1.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_2.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_2.png new file mode 100644 index 0000000..c0d6280 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_2.png differ 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 0000000..b5138b5 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_3.png differ 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 0000000..99d02e4 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/rocket_tier_4.png differ 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 0000000..d5cdac8 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/silk_touch_module.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_module.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_module.png new file mode 100644 index 0000000..a88b430 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_module.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_upgrade.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_upgrade.png new file mode 100644 index 0000000..d78a81a Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/speed_upgrade.png differ 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 0000000..5995cd5 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/star_guide_book.png differ 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 0000000..93ef9ab Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/station_charter.png differ 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 0000000..eccae74 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/station_compass.png differ 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 0000000..aab4319 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/woolly_drift_spawn_egg.png differ 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 0000000..740802b Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_quartz.png differ diff --git a/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_resonator.png b/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_resonator.png new file mode 100644 index 0000000..5ffe33c Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_resonator.png differ 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 0000000..938f511 Binary files /dev/null and b/multiloader/common/src/main/resources/assets/nerospace/textures/item/xertz_stalker_spawn_egg.png differ 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/nerosium.json b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosium.json new file mode 100644 index 0000000..62bd729 --- /dev/null +++ b/multiloader/common/src/main/resources/data/c/tags/item/storage_blocks/nerosium.json @@ -0,0 +1,5 @@ +{ + "values": [ + "nerospace:nerosium_block" + ] +} 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..5fd2c2b --- /dev/null +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -0,0 +1,32 @@ +{ + "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", + "nerospace:station_floor", + "nerospace:station_wall", + "nerospace:alien_bricks", + "nerospace:cracked_alien_bricks", + "nerospace:alien_tile", + "nerospace:alien_pillar", + "nerospace:alien_lamp", + "nerospace:alien_crystal_block", + "nerospace:meteor_rock", + "nerospace:item_store", + "nerospace:battery", + "nerospace:fluid_tank", + "nerospace:combustion_generator", + "nerospace:nerosium_grinder", + "nerospace:passive_generator", + "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 new file mode 100644 index 0000000..7bccbc4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/minecraft/tags/block/needs_iron_tool.json @@ -0,0 +1,24 @@ +{ + "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", + "nerospace:station_floor", + "nerospace:station_wall", + "nerospace:item_store", + "nerospace:battery", + "nerospace:fluid_tank", + "nerospace:combustion_generator", + "nerospace:nerosium_grinder", + "nerospace:passive_generator", + "nerospace:universal_pipe", + "nerospace:trash_can" + ] +} \ No newline at end of file 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..29c0f36 --- /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_charter" }, + "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 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/loot_table/blocks/alien_bricks.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_bricks.json new file mode 100644 index 0000000..bacbc6c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_bricks.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:alien_bricks" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/alien_bricks" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_crystal_block.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_crystal_block.json new file mode 100644 index 0000000..065f7c2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_crystal_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:alien_crystal_block" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/alien_crystal_block" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_lamp.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_lamp.json new file mode 100644 index 0000000..2a5f660 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_lamp.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:alien_lamp" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/alien_lamp" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_pillar.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_pillar.json new file mode 100644 index 0000000..011dca5 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_pillar.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:alien_pillar" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/alien_pillar" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_tile.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_tile.json new file mode 100644 index 0000000..05d2c71 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/alien_tile.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:alien_tile" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/alien_tile" +} \ 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/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/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/loot_table/blocks/cracked_alien_bricks.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cracked_alien_bricks.json new file mode 100644 index 0000000..72a7a0b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/cracked_alien_bricks.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:cracked_alien_bricks" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/cracked_alien_bricks" +} \ 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/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/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/fluid_tank.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/fluid_tank.json new file mode 100644 index 0000000..f2371f5 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/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:fluid_tank" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/fluid_tank" +} \ No newline at end of file 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/loot_table/blocks/gas_tank.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/gas_tank.json new file mode 100644 index 0000000..fa03161 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/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:gas_tank" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/gas_tank" +} \ 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/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/item_store.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/item_store.json new file mode 100644 index 0000000..b97d183 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/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:item_store" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/item_store" +} \ No newline at end of file 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/meteor_rock.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/meteor_rock.json new file mode 100644 index 0000000..c637909 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/meteor_rock.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:meteor_rock" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/meteor_rock" +} \ 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_grinder.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_grinder.json new file mode 100644 index 0000000..f2ac4be --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/nerosium_grinder.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_grinder" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/nerosium_grinder" +} \ 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/oxygen_generator.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/oxygen_generator.json new file mode 100644 index 0000000..fd95bcf --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/oxygen_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:oxygen_generator" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/oxygen_generator" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/passive_generator.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/passive_generator.json new file mode 100644 index 0000000..1e32d5b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/passive_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:passive_generator" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/passive_generator" +} \ No newline at end of file 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/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/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/loot_table/blocks/solar_panel.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel.json new file mode 100644 index 0000000..a3e48ef --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ { "condition": "minecraft:survives_explosion" } ], + "entries": [ { "type": "minecraft:item", "name": "nerospace:solar_panel" } ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/solar_panel" +} diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json new file mode 100644 index 0000000..e1ab588 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t2.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ { "condition": "minecraft:survives_explosion" } ], + "entries": [ { "type": "minecraft:item", "name": "nerospace:solar_panel_t2" } ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/solar_panel_t2" +} diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json new file mode 100644 index 0000000..b99d098 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/solar_panel_t3.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ { "condition": "minecraft:survives_explosion" } ], + "entries": [ { "type": "minecraft:item", "name": "nerospace:solar_panel_t3" } ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/solar_panel_t3" +} 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/common/src/main/resources/data/nerospace/loot_table/blocks/station_floor.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_floor.json new file mode 100644 index 0000000..ebe895c --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_floor.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:station_floor" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/station_floor" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_wall.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_wall.json new file mode 100644 index 0000000..48490b3 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/station_wall.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:station_wall" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/station_wall" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraform_monitor.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraform_monitor.json new file mode 100644 index 0000000..ab00aa4 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/terraform_monitor.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:terraform_monitor" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/terraform_monitor" +} \ 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/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/loot_table/blocks/universal_pipe.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/universal_pipe.json new file mode 100644 index 0000000..a123d09 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/universal_pipe.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:universal_pipe" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/universal_pipe" +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/village_core.json b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/village_core.json new file mode 100644 index 0000000..26e0430 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/loot_table/blocks/village_core.json @@ -0,0 +1,21 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "bonus_rolls": 0.0, + "conditions": [ + { + "condition": "minecraft:survives_explosion" + } + ], + "entries": [ + { + "type": "minecraft:item", + "name": "nerospace:village_core" + } + ], + "rolls": 1.0 + } + ], + "random_sequence": "nerospace:blocks/village_core" +} \ 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/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/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/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/common/src/main/resources/data/nerospace/recipe/fluid_tank.json b/multiloader/common/src/main/resources/data/nerospace/recipe/fluid_tank.json new file mode 100644 index 0000000..0ac2ce2 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/fluid_tank.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "G": "#c:glass_blocks/colorless", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NGN", + "G G", + "NGN" + ], + "result": { + "id": "nerospace:fluid_tank" + } +} \ No newline at end of file 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/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/common/src/main/resources/data/nerospace/recipe/gas_tank.json b/multiloader/common/src/main/resources/data/nerospace/recipe/gas_tank.json new file mode 100644 index 0000000..b182c30 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/gas_tank.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "N": "#c:ingots/nerosteel", + "T": "nerospace:fluid_tank" + }, + "pattern": [ + "NNN", + "NTN", + "NNN" + ], + "result": { + "id": "nerospace:gas_tank" + } +} \ 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/item_store.json b/multiloader/common/src/main/resources/data/nerospace/recipe/item_store.json new file mode 100644 index 0000000..5dc6a8e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/item_store.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "#c:chests/wooden", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NNN", + "NCN", + "NNN" + ], + "result": { + "id": "nerospace:item_store" + } +} \ 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/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_grinder.json b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_grinder.json new file mode 100644 index 0000000..090410f --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/nerosium_grinder.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "#c:cobblestones", + "F": "minecraft:furnace", + "I": "#c:ingots/nerosium" + }, + "pattern": [ + "III", + "IFI", + "CCC" + ], + "result": { + "id": "nerospace:nerosium_grinder" + } +} \ 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/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/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/oxygen_generator.json b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_generator.json new file mode 100644 index 0000000..d3cc4ac --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/oxygen_generator.json @@ -0,0 +1,18 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "C": "nerospace:rocket_fuel_canister", + "G": "#c:glass_blocks/colorless", + "N": "#c:ingots/nerosteel", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "NGN", + "RCR", + "NNN" + ], + "result": { + "id": "nerospace:oxygen_generator" + } +} \ 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_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/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 diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/passive_generator.json b/multiloader/common/src/main/resources/data/nerospace/recipe/passive_generator.json new file mode 100644 index 0000000..7251376 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/passive_generator.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "B": "#c:storage_blocks/nerosium", + "N": "#c:ingots/nerosteel", + "R": "#c:dusts/redstone" + }, + "pattern": [ + "NNN", + "NBN", + "NRN" + ], + "result": { + "id": "nerospace:passive_generator" + } +} \ 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/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/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 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/common/src/main/resources/data/nerospace/recipe/station_floor.json b/multiloader/common/src/main/resources/data/nerospace/recipe/station_floor.json new file mode 100644 index 0000000..c267aa1 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/station_floor.json @@ -0,0 +1,16 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:ingots/nerosteel" + }, + "pattern": [ + "###", + "# #", + "###" + ], + "result": { + "count": 8, + "id": "nerospace:station_floor" + } +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/recipe/station_wall.json b/multiloader/common/src/main/resources/data/nerospace/recipe/station_wall.json new file mode 100644 index 0000000..0aaea2b --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/station_wall.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "building", + "key": { + "#": "#c:ingots/nerosteel", + "I": "#c:ingots/iron" + }, + "pattern": [ + "###", + "#I#", + "###" + ], + "result": { + "count": 8, + "id": "nerospace:station_wall" + } +} \ 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/common/src/main/resources/data/nerospace/recipe/universal_pipe.json b/multiloader/common/src/main/resources/data/nerospace/recipe/universal_pipe.json new file mode 100644 index 0000000..d7300f3 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/recipe/universal_pipe.json @@ -0,0 +1,17 @@ +{ + "type": "minecraft:crafting_shaped", + "category": "misc", + "key": { + "G": "#c:glass_blocks/colorless", + "N": "#c:ingots/nerosteel" + }, + "pattern": [ + "NNN", + "NGN", + "NNN" + ], + "result": { + "count": 8, + "id": "nerospace:universal_pipe" + } +} \ No newline at end of file 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/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/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..f77b80e --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/biome/greenxertz.json @@ -0,0 +1,68 @@ +{ + "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", + "nerospace:hamlet_placed", + "nerospace:ruin_placed", + "nerospace:mega_city_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/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/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/hamlet.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/hamlet.json new file mode 100644 index 0000000..02efc1f --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/hamlet.json @@ -0,0 +1,4 @@ +{ + "type": "nerospace:hamlet", + "config": {} +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/mega_city.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/mega_city.json new file mode 100644 index 0000000..574c431 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/mega_city.json @@ -0,0 +1,4 @@ +{ + "type": "nerospace:mega_city", + "config": {} +} \ No newline at end of file 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/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/ruin.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/ruin.json new file mode 100644 index 0000000..6ce9727 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/configured_feature/ruin.json @@ -0,0 +1,4 @@ +{ + "type": "nerospace:ruin", + "config": {} +} \ 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/hamlet_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/hamlet_placed.json new file mode 100644 index 0000000..2c74061 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/hamlet_placed.json @@ -0,0 +1,19 @@ +{ + "feature": "nerospace:hamlet", + "placement": [ + { + "type": "minecraft:count", + "count": 1 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:heightmap", + "heightmap": "WORLD_SURFACE_WG" + }, + { + "type": "minecraft:biome" + } + ] +} \ No newline at end of file diff --git a/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/mega_city_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/mega_city_placed.json new file mode 100644 index 0000000..bc8a2d1 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/mega_city_placed.json @@ -0,0 +1,19 @@ +{ + "feature": "nerospace:mega_city", + "placement": [ + { + "type": "minecraft:count", + "count": 1 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:heightmap", + "heightmap": "WORLD_SURFACE_WG" + }, + { + "type": "minecraft:biome" + } + ] +} \ 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/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/ruin_placed.json b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/ruin_placed.json new file mode 100644 index 0000000..fdf2a53 --- /dev/null +++ b/multiloader/common/src/main/resources/data/nerospace/worldgen/placed_feature/ruin_placed.json @@ -0,0 +1,19 @@ +{ + "feature": "nerospace:ruin", + "placement": [ + { + "type": "minecraft:count", + "count": 1 + }, + { + "type": "minecraft:in_square" + }, + { + "type": "minecraft:heightmap", + "heightmap": "WORLD_SURFACE_WG" + }, + { + "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 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..82c9de2 --- /dev/null +++ b/multiloader/fabric/build.gradle @@ -0,0 +1,83 @@ +// 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 +def fabricApi = project.findProperty("fabric_api_version_${mc}") + +// Pull in the shared common source + resources. +sourceSets.main.java.srcDir rootProject.ext.commonJava +sourceSets.main.resources.srcDir rootProject.ext.commonResources + +dependencies { + 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) { + 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. +// 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 { + accessWidenerPath = file("src/main/resources/nerospace.accesswidener") + + runs { + client { + client() + ideConfigGenerated true + runDir 'runs/client' + } + server { + server() + ideConfigGenerated true + runDir 'runs/server' + } + } +} + +// 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/fabric_client.launch b/multiloader/fabric/fabric_client.launch new file mode 100644 index 0000000..6ca5b71 --- /dev/null +++ b/multiloader/fabric/fabric_client.launch @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/multiloader/fabric/fabric_server.launch b/multiloader/fabric/fabric_server.launch new file mode 100644 index 0000000..87e524b --- /dev/null +++ b/multiloader/fabric/fabric_server.launch @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + 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..adbdaa5 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/FabricAttachments.java @@ -0,0 +1,52 @@ +package za.co.neroland.nerospace.fabric; + +import java.util.List; + +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")); + + /** 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")); + + /** 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() { + } + + /** 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/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 new file mode 100644 index 0000000..252197c --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabric.java @@ -0,0 +1,256 @@ +package za.co.neroland.nerospace.fabric; + +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; +import net.fabricmc.fabric.api.biome.v1.BiomeSelectors; +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; +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.world.damagesource.DamageTypes; +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; +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; +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; +import za.co.neroland.nerospace.telemetry.NerospaceTelemetry; +import za.co.neroland.nerospace.progression.StarGuideGrants; +import za.co.neroland.nerospace.world.OxygenManager; + +/** + * 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 { + + /** 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); + + /** Mod-owned fluid lookup; mirrors the NeoForge fluid BlockCapability of the same id. */ + public static final BlockApiLookup FLUID = + BlockApiLookup.get( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "fluid"), + NerospaceFluidStorage.class, Direction.class); + + /** Mod-owned gas lookup; mirrors the NeoForge gas BlockCapability of the same id. */ + public static final BlockApiLookup GAS = + BlockApiLookup.get( + Identifier.fromNamespaceAndPath(NerospaceCommon.MOD_ID, "gas"), + NerospaceGasStorage.class, Direction.class); + + @Override + 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. + + addOverworldOre("nerosium_ore_placed"); + + // 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(); + ServerTickEvents.END_SERVER_TICK.register(server -> { + server.getPlayerList().getPlayers().forEach(player -> { + OxygenManager.tick(player); + StarGuideGrants.tick(player); + }); + MeteorEvents.tick(server); + 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) -> + 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. + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.ITEM_STORE.get()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.BATTERY.get()); + + 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()); + + ItemStorage.SIDED.registerForBlockEntity( + (be, direction) -> ContainerStorage.of(be, direction), + ModBlockEntities.NEROSIUM_GRINDER.get()); + ENERGY.registerForBlockEntity( + (be, direction) -> 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()); + + ENERGY.registerForBlockEntity( + (be, direction) -> be.getEnergy(), + ModBlockEntities.UNIVERSAL_PIPE.get()); + 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()); + + 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()); + + // 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()); + + // 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()); + FLUID.registerForBlockEntity( + (be, direction) -> be.getFluid(), + ModBlockEntities.TRASH_CAN.get()); + + ENERGY.registerForBlockEntity( + (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(), + 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()); + + // 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) { + 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/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..e6bdfce --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/fabric/NerospaceFabricClient.java @@ -0,0 +1,75 @@ +package za.co.neroland.nerospace.fabric; + +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; +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.PipeConfigScreen; +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; + +/** Fabric client entry point — screen + entity-renderer registration. */ +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); + 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); + MenuScreens.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); + 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 + public void register(EntityType type, EntityRendererProvider provider) { + 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 -> { + MeteorTrackerHud.tick(); + ClientOxygenVisuals.tick(); + }); + } +} 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/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/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/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/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/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..be86970 --- /dev/null +++ b/multiloader/fabric/src/main/java/za/co/neroland/nerospace/platform/FabricPlatformHelper.java @@ -0,0 +1,88 @@ +package za.co.neroland.nerospace.platform; + +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.NerospaceCommon; +import za.co.neroland.nerospace.fabric.FabricAttachments; + +/** + * 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; + } + + @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); + } + + @Override + 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); + } + + @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/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.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/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.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/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/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/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/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/fabric/src/main/resources/nerospace.accesswidener b/multiloader/fabric/src/main/resources/nerospace.accesswidener new file mode 100644 index 0000000..dd05ff9 --- /dev/null +++ b/multiloader/fabric/src/main/resources/nerospace.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 official +accessible class net/minecraft/world/level/block/entity/BlockEntityType$BlockEntitySupplier +accessible method net/minecraft/world/level/block/entity/BlockEntityType (Lnet/minecraft/world/level/block/entity/BlockEntityType$BlockEntitySupplier;Ljava/util/Set;)V 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/fabric/src/main/templates/fabric.mod.json b/multiloader/fabric/src/main/templates/fabric.mod.json new file mode 100644 index 0000000..abae495 --- /dev/null +++ b/multiloader/fabric/src/main/templates/fabric.mod.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${mod_version}", + "name": "${mod_name}", + "description": "Nerospace - multiloader build (Fabric).", + "authors": ["${mod_authors}"], + "license": "${mod_license}", + "accessWidener": "nerospace.accesswidener", + "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", + "fabric-api": "*" + } +} diff --git a/multiloader/gradle.properties b/multiloader/gradle.properties new file mode 100644 index 0000000..376d7d3 --- /dev/null +++ b/multiloader/gradle.properties @@ -0,0 +1,54 @@ +# ---------------------------------------------------------------------------- +# Nerospace multiloader scaffold — MultiLoader-Template (ModDevGradle + Fabric Loom) +# +# 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. +# +# 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). +# +# 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 +# ModDevGradle/Loom do not always agree with the configuration cache; 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 (override with -Pminecraft_version=26.1.2) -------- +minecraft_version=26.2 + +## Version matrix (CI iterates these) --------------------------------------- +mc_versions=26.1.2,26.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 +# 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.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.151.0+26.1.2 +fabric_api_version_26.2=0.152.1+26.2 diff --git a/multiloader/gradle/wrapper/gradle-wrapper.jar b/multiloader/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/multiloader/gradle/wrapper/gradle-wrapper.jar differ diff --git a/multiloader/gradle/wrapper/gradle-wrapper.properties b/multiloader/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3eadb10 --- /dev/null +++ b/multiloader/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,11 @@ +# The multiloader build runs on its OWN Gradle, independent of the repo root +# (which stays on 9.2.1 for NeoForge/ModDevGradle). Architectury Loom 1.17.x +# calls Configuration.extendsFrom(Provider...), added in Gradle 9.4.0, so this +# build requires Gradle >= 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 diff --git a/multiloader/neoforge/build.gradle b/multiloader/neoforge/build.gradle new file mode 100644 index 0000000..1fba922 --- /dev/null +++ b/multiloader/neoforge/build.gradle @@ -0,0 +1,119 @@ +// 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. + +plugins { + id 'net.neoforged.moddev' +} + +def mc = rootProject.minecraft_version +def neoVersion = project.findProperty("neo_version_${mc}") + +repositories { + mavenLocal() // resolves a self-built NeoForge 26.2 until the official jar drops +} + +// Pull in the shared common source + resources. +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; +// 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()) { + accessTransformers.from(at.absolutePath) + } + + runs { + client { + client() + gameDirectory = file('runs/client') + } + server { + 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 { + "${rootProject.mod_id}" { + sourceSet sourceSets.main + } + } +} + +// 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 + } +} 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..f78a11f --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeAttachments.java @@ -0,0 +1,61 @@ +package za.co.neroland.nerospace.neoforge; + +import java.util.List; +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()); + + /** 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()); + + /** 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() { + } + + public static void register(IEventBus modEventBus) { + ATTACHMENT_TYPES.register(modEventBus); + } +} 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..6e50509 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeCapabilities.java @@ -0,0 +1,237 @@ +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.fluid.NerospaceFluidStorage; +import za.co.neroland.nerospace.gas.NerospaceGasStorage; +import za.co.neroland.nerospace.registry.ModBlockEntities; + +/** + * 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); + + /** 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); + + /** 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() { + } + + 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)); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.BATTERY.get(), + (be, side) -> be.getEnergy()); + + event.registerBlockEntity( + 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()); + + 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()); + + 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()); + + event.registerBlockEntity( + ENERGY, + ModBlockEntities.UNIVERSAL_PIPE.get(), + (be, side) -> be.getEnergy()); + event.registerBlockEntity( + 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(), + (be, side) -> side != null + ? new WorldlyContainerWrapper(be, side) + : VanillaContainerWrapper.of(be)); + + 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()); + + // 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)); + + // 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(), + (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()); + + // 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, + 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)); + + // 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 new file mode 100644 index 0000000..d00c2de --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NeoForgeClientSetup.java @@ -0,0 +1,97 @@ +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; +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.ClientBlockEntityRenderers; +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; +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; +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; +import za.co.neroland.nerospace.registry.ModMenuTypes; + +/** NeoForge client-only wiring (screen + fluid-model registration). Loaded only behind Dist.CLIENT. */ +public final class NeoForgeClientSetup { + + private NeoForgeClientSetup() { + } + + public static void init(IEventBus modEventBus) { + modEventBus.addListener(NeoForgeClientSetup::onRegisterScreens); + modEventBus.addListener(NeoForgeClientSetup::onRegisterFluidModels); + modEventBus.addListener(NeoForgeClientSetup::onRegisterEntityRenderers); + // 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) { + ClientEntityRenderers.registerAll(new ClientEntityRenderers.Sink() { + @Override + public void register(EntityType type, EntityRendererProvider provider) { + 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) { + 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); + event.register(ModMenuTypes.QUARRY_CONTROLLER.get(), QuarryScreen::new); + 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. */ + 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/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 new file mode 100644 index 0000000..143e6f9 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/neoforge/NerospaceNeoForge.java @@ -0,0 +1,111 @@ +package za.co.neroland.nerospace.neoforge; + +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.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; +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; +import net.minecraft.world.entity.SpawnPlacements; +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.gear.AlienGearAbilities; +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; +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; + +/** + * 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 { + + 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); + NeoForgeAttachments.register(modEventBus); + NeoForgeNetwork.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); + StarGuideGrants.tick(serverPlayer); + } + }); + // 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()); + }); + // 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())); + // 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. + 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); + } + }); + } +} 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/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/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/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/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..88dc0e5 --- /dev/null +++ b/multiloader/neoforge/src/main/java/za/co/neroland/nerospace/platform/NeoForgePlatformHelper.java @@ -0,0 +1,92 @@ +package za.co.neroland.nerospace.platform; + +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; + +/** + * 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() { + // 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 + public boolean isModLoaded(String modId) { + return ModList.get().isLoaded(modId); + } + + @Override + 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()); + } + + @Override + 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); + } + + @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); + } +} 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.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 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.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 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 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/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 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/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 diff --git a/multiloader/neoforge/src/main/templates/META-INF/neoforge.mods.toml b/multiloader/neoforge/src/main/templates/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..5cec1e2 --- /dev/null +++ b/multiloader/neoforge/src/main/templates/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/settings.gradle b/multiloader/settings.gradle new file mode 100644 index 0000000..8029ace --- /dev/null +++ b/multiloader/settings.gradle @@ -0,0 +1,28 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + maven { url 'https://maven.fabricmc.net/' } // Fabric Loom, Loader, API + 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' +} + +rootProject.name = 'nerospace-multiloader' + +// 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' 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). +// ===================================================================== diff --git a/nerospace-multiloader.code-workspace b/nerospace-multiloader.code-workspace new file mode 100644 index 0000000..38945ce --- /dev/null +++ b/nerospace-multiloader.code-workspace @@ -0,0 +1,20 @@ +{ + "folders": [ + { + "name": "nerospace-multiloader", + "path": "multiloader" + } + ], + "settings": { + // 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/nerospace.code-workspace b/nerospace.code-workspace new file mode 100644 index 0000000..03d2032 --- /dev/null +++ b/nerospace.code-workspace @@ -0,0 +1,19 @@ +{ + "//": "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" + } + ], + "settings": { + "java.import.gradle.enabled": true, + "java.configuration.updateBuildConfiguration": "automatic", + "java.compile.nullAnalysis.mode": "automatic", + "java.debug.settings.onBuildFailureProceed": true + } +}