diff --git a/.github/ISSUE_TEMPLATE/10-snow-bug.yml b/.github/ISSUE_TEMPLATE/10-snow-bug.yml new file mode 100644 index 0000000..ae84a0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/10-snow-bug.yml @@ -0,0 +1,127 @@ +name: Snow bug report +description: Report a crash, broken behaviour, or compatibility problem in Snow. +title: "[Snow] " +labels: + - "project:snow" +type: Bug +body: + - type: markdown + attributes: + value: | + Report a problem with Snow. Tell us what happened and how we can reproduce it. If you are unsure about something, say so. + + - type: markdown + attributes: + value: | + If you are on a game version or loader not listed here, we are unfortunately not supporting it anymore. + + - type: checkboxes + id: existing-issues + attributes: + label: Existing issues + options: + - label: I have [searched the existing issues](https://github.com/IMB11-Mods/JavaEditionMods/issues?q=is%3Aissue%20label%3A%22project%3Asnow%22) for this problem or suggestion. + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Describe the problem and what you expected to happen instead. Include the exact error message, if there is one. + placeholder: | + What happened: + What I expected: + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: How can we reproduce it? + description: List the steps that trigger the problem. If it happens unpredictably, describe what you were doing and how often it happens. + placeholder: | + 1. Start the game with... + 2. Open or do... + 3. The problem happens when... + validations: + required: true + + - type: dropdown + id: minecraft-version + attributes: + label: Minecraft version + description: Which Minecraft version were you using when the problem happened? + options: + - "26.2" + - "26.3" + validations: + required: true + + - type: dropdown + id: mod-loader + attributes: + label: Mod loader + description: Which mod loader are you using? + options: + - Fabric + - NeoForge + validations: + required: true + + - type: input + id: mod-version + attributes: + label: Snow version + description: Copy the exact version from your launcher's mod list or the mod's filename. Please avoid "latest". + validations: + required: true + + - type: dropdown + id: environment + attributes: + label: Where does it happen? + options: + - Starting the game + - Singleplayer + - Playing on a server + - Running a dedicated server + - Development/build + - Other / Not sure + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs or crash report + description: | + Open the affected game instance's folder in your launcher and find logs/latest.log. Upload it to [mclo.gs](https://mclo.gs/) and paste the link here, even if the game did not crash. + If the game crashed, include the matching report from the crash-reports folder too. For development issues, include the build output instead. + Review logs for personal information before sharing them. If logs are unavailable, explain why. + placeholder: Paste your log links or explain why logs are unavailable. + validations: + required: true + + - type: textarea + id: other-mods-and-settings + attributes: + label: Other mods, packs, or changed settings + description: Include relevant mods and versions, a modpack link and version, resource packs, or settings you changed. + validations: + required: false + + - type: textarea + id: troubleshooting + attributes: + label: What have you tried? + description: Mention anything you tested and whether it helped. If you tested with only this mod and its required dependencies, tell us the result. You do not need to do this before reporting. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Screenshots, videos, or extra details + description: Attach anything that helps demonstrate the problem. For development issues, include a small code example or example project if possible. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/11-snow-feature.yml b/.github/ISSUE_TEMPLATE/11-snow-feature.yml new file mode 100644 index 0000000..3958cab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/11-snow-feature.yml @@ -0,0 +1,86 @@ +name: Snow feature request +description: Suggest a new feature or improvement for Snow. +title: "[Snow] " +labels: + - "project:snow" +type: Feature +body: + - type: markdown + attributes: + value: | + Suggest an improvement for Snow. Explain what you want to achieve and how the change would help. + + - type: markdown + attributes: + value: | + If you are on a game version or loader not listed here, we are unfortunately not supporting it anymore. + + - type: checkboxes + id: existing-issues + attributes: + label: Existing issues + options: + - label: I have [searched the existing issues](https://github.com/IMB11-Mods/JavaEditionMods/issues?q=is%3Aissue%20label%3A%22project%3Asnow%22) for this problem or suggestion. + required: true + + - type: textarea + id: use-case + attributes: + label: What would this help you do? + description: Describe the problem, missing capability, or situation you want to improve. A concrete example helps. + validations: + required: true + + - type: textarea + id: proposed-feature + attributes: + label: What would you like to happen? + description: Describe how you would expect the feature to work. You do not need to explain how to implement it. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives or workarounds + description: Is there another way to achieve this today? What does not work well about it? + validations: + required: false + + - type: dropdown + id: minecraft-version + attributes: + label: Minecraft version + description: Select a version only if your request is specific to it. + options: + - "26.2" + - "26.3" + validations: + required: false + + - type: dropdown + id: mod-loader + attributes: + label: Mod loader + description: Select a loader only if your request is specific to it. + options: + - Fabric + - NeoForge + validations: + required: false + + - type: textarea + id: related-mods + attributes: + label: Relevant mod versions or other mods + description: Include Snow or other mod versions only if the request depends on them. + validations: + required: false + + - type: textarea + id: additional-context + attributes: + label: Examples or additional context + description: Add screenshots, mockups, links, or related requests. + validations: + required: false diff --git a/.github/scripts/changelog-comment.cjs b/.github/scripts/changelog-comment.cjs index a64c83d..698239a 100644 --- a/.github/scripts/changelog-comment.cjs +++ b/.github/scripts/changelog-comment.cjs @@ -17,7 +17,7 @@ module.exports = async ({ github, context }) => { const issue_number = pull.number; const repo = { ...context.repo, issue_number }; const comments = await github.paginate(github.rest.issues.listComments, repo); - for (const project of ['sounds', 'mru', 'shields', 'fog', 'glass']) { + for (const project of ['sounds', 'mru', 'shields', 'fog', 'glass', 'snow']) { if (!pull.labels.some(label => label.name === `project:${project}`)) continue; const marker = `[release-changelog-${project}]: #`; if (comments.some(comment => comment.user.type === 'Bot' && comment.body.split(/\r?\n/).includes(marker))) continue; diff --git a/.github/scripts/prepare-release.mjs b/.github/scripts/prepare-release.mjs index e49f976..821ae4e 100644 --- a/.github/scripts/prepare-release.mjs +++ b/.github/scripts/prepare-release.mjs @@ -11,7 +11,7 @@ const git = (...values) => run('git', values); const project = option('project'); const version = option('version'); const versionPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/; -if (!['sounds', 'mru', 'shields', 'fog', 'glass'].includes(project) || !versionPattern.test(version ?? '')) throw new Error('Provide --project sounds|mru|shields|fog|glass and --version X.Y.Z. Optional: --from REF, --write.'); +if (!['sounds', 'mru', 'shields', 'fog', 'glass', 'snow'].includes(project) || !versionPattern.test(version ?? '')) throw new Error('Provide --project sounds|mru|shields|fog|glass|snow and --version X.Y.Z. Optional: --from REF, --write.'); const suffix = version.match(/^[^-+]+(?:-([^+]+))?(?:\+(.+))?$/); if ([suffix?.[1], suffix?.[2]].filter(Boolean).some(value => value.split('.').some(part => !part)) || suffix?.[1]?.split('.').some(part => /^0\d+$/.test(part))) throw new Error('Invalid semantic version suffix'); const head = git('rev-parse', 'HEAD'); diff --git a/.github/workflows/gametest-snow.yml b/.github/workflows/gametest-snow.yml new file mode 100644 index 0000000..b3946ae --- /dev/null +++ b/.github/workflows/gametest-snow.yml @@ -0,0 +1,27 @@ +name: Snow game tests + +on: + push: + branches: ['**'] + paths: ['snow/**'] + pull_request: + paths: ['snow/**'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + gametest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: '25' + - uses: gradle/actions/setup-gradle@v6 + with: + cache-read-only: ${{ github.event_name == 'pull_request' }} + - name: Run game tests + run: bash ./gradlew --no-daemon --build-cache --continue -Pgametest :snow:runGameTests diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 93606ff..d6e63cb 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -7,7 +7,7 @@ on: description: project type: choice required: true - options: [sounds, mru, shields, fog, glass] + options: [sounds, mru, shields, fog, glass, snow] version: description: version type: string diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4fae4e8..f052b2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,7 @@ on: - shields/v* - fog/v* - glass/v* + - snow/v* workflow_dispatch: inputs: tag: @@ -34,8 +35,8 @@ jobs: RELEASE_TAG: ${{ inputs.tag || github.ref_name }} run: | case "$RELEASE_TAG" in - sounds/v*|mru/v*|shields/v*|fog/v*|glass/v*) ;; - *) echo 'Expected a sounds/v, mru/v, shields/v, fog/v, or glass/v release tag'; exit 1 ;; + sounds/v*|mru/v*|shields/v*|fog/v*|glass/v*|snow/v*) ;; + *) echo 'Expected a sounds/v, mru/v, shields/v, fog/v, glass/v, or snow/v release tag'; exit 1 ;; esac test "$(git rev-parse HEAD)" = "$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}")" echo "RELEASE_MOD=${RELEASE_TAG%%/*}" >> "$GITHUB_ENV" @@ -52,7 +53,7 @@ jobs: GITHUB_TOKEN: ${{ github.token }} run: | case "$RELEASE_MOD" in - mru|sounds|shields|fog|glass) task=":$RELEASE_MOD:publishMods" ;; + mru|sounds|shields|fog|glass|snow) task=":$RELEASE_MOD:publishMods" ;; *) exit 1 ;; esac GITHUB_REF_TYPE=tag GITHUB_REF_NAME="$RELEASE_TAG" GITHUB_SHA="$(git rev-parse HEAD)" bash ./gradlew --no-daemon --build-cache "$task" diff --git a/.gitignore b/.gitignore index d9a06b2..2bf3c20 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ .DS_Store Thumbs.db **/.cache +.openidea/ +**/bin/ +**/.eclipse/ diff --git a/build-logic/src/main/kotlin/imb11.fabric-mod.gradle.kts b/build-logic/src/main/kotlin/imb11.fabric-mod.gradle.kts index 969870a..cd6b93d 100644 --- a/build-logic/src/main/kotlin/imb11.fabric-mod.gradle.kts +++ b/build-logic/src/main/kotlin/imb11.fabric-mod.gradle.kts @@ -48,6 +48,16 @@ if (metadata.optional("mod.datagen").toBoolean()) { } } +if (providers.gradleProperty("gametest").isPresent && metadata.optional("mod.gametest").toBoolean()) { + fabricApi { + configureTests { + createSourceSet = false + enableClientGameTests = false + eula = true + } + } +} + tasks.named("processResources") { exclude("META-INF/neoforge.mods.toml", "META-INF/mods.toml", "META-INF/accesstransformer.cfg", "interfaces.json") } diff --git a/build-logic/src/main/kotlin/imb11.neoforge-mod.gradle.kts b/build-logic/src/main/kotlin/imb11.neoforge-mod.gradle.kts index 00b9322..803a541 100644 --- a/build-logic/src/main/kotlin/imb11.neoforge-mod.gradle.kts +++ b/build-logic/src/main/kotlin/imb11.neoforge-mod.gradle.kts @@ -35,6 +35,13 @@ neoForge { gameDirectory.set(rootProject.layout.projectDirectory.dir("run/${metadata.id}/$minecraftVersion/neoforge/server")) } } + if (providers.gradleProperty("gametest").isPresent && metadata.optional("mod.gametest").toBoolean()) { + register("gameTestServer") { + type = "gameTestServer" + gameDirectory.set(rootProject.layout.projectDirectory.dir("run/${metadata.id}/$minecraftVersion/neoforge/gametest")) + systemProperty("neoforge.enabledGameTestNamespaces", metadata.id) + } + } } } diff --git a/build-logic/src/main/kotlin/imb11.publish-mod.gradle.kts b/build-logic/src/main/kotlin/imb11.publish-mod.gradle.kts index dba9a47..5d289a8 100644 --- a/build-logic/src/main/kotlin/imb11.publish-mod.gradle.kts +++ b/build-logic/src/main/kotlin/imb11.publish-mod.gradle.kts @@ -8,6 +8,7 @@ plugins { val modData = readModMetadata(layout.projectDirectory) val clientOnly = modData.optional("mod.environment") == "client" || modData.id == "sounds" val releaseTag = "${modData.id}/v${modData.version}" +val curseforgeProject = modData.optional("publish.curseforge") val publishDryRun = providers.gradleProperty("publishDryRun").map(String::toBoolean).orElse(false) val releaseChangelog = providers.fileContents(layout.projectDirectory.file("CHANGELOG.md")).asText.map { contents -> val lines = contents.lines() @@ -36,7 +37,7 @@ val validateRelease = tasks.register("validateRelease") { } } if (!publishDryRun.get()) { - listOf("MODRINTH_TOKEN", "CURSEFORGE_TOKEN", "GITHUB_TOKEN", "GITHUB_REPOSITORY", "GITHUB_SHA").forEach { + listOfNotNull("MODRINTH_TOKEN", "CURSEFORGE_TOKEN".takeIf { curseforgeProject.isNotBlank() }, "GITHUB_TOKEN", "GITHUB_REPOSITORY", "GITHUB_SHA").forEach { require(!providers.environmentVariable(it).orNull.isNullOrBlank()) { "Missing release environment variable: $it" } } } @@ -82,15 +83,17 @@ publishMods { requiredMods.forEach { requires(it) } } - curseforge("curseforge$targetName") { - from(options.get()) - projectId = modData.required("publish.curseforge") - accessToken = providers.environmentVariable("CURSEFORGE_TOKEN") - minecraftVersions.add(minecraftVersion) - client = true - server = !clientOnly - additionalFiles.from(sources) - requiredMods.forEach { requires(it) } + if (curseforgeProject.isNotBlank()) { + curseforge("curseforge$targetName") { + from(options.get()) + projectId = curseforgeProject + accessToken = providers.environmentVariable("CURSEFORGE_TOKEN") + minecraftVersions.add(minecraftVersion) + client = true + server = !clientOnly + additionalFiles.from(sources) + requiredMods.forEach { requires(it) } + } } } diff --git a/build-logic/src/main/kotlin/imb11.root.gradle.kts b/build-logic/src/main/kotlin/imb11.root.gradle.kts index 831d5f4..3fcf773 100644 --- a/build-logic/src/main/kotlin/imb11.root.gradle.kts +++ b/build-logic/src/main/kotlin/imb11.root.gradle.kts @@ -64,13 +64,31 @@ tasks.register("runDatagenAll") { dependsOn(datagenTasks) } +if (providers.gradleProperty("gametest").isPresent) { + val gameTestTasks = modProjects + .filter { readModMetadata(it.layout.projectDirectory).optional("mod.gametest").toBoolean() } + .map { mod -> + mod.tasks.register("runGameTests") { + group = "verification" + dependsOn(mod.childProjects.values.map { target -> + "${target.path}:${if (target.name.endsWith("-fabric")) "runGameTest" else "runGameTestServer"}" + }) + } + } + + tasks.register("runGameTestsAll") { + group = "verification" + dependsOn(gameTestTasks) + } +} + tasks.register("publishAllMods") { group = "publishing" dependsOn(buildAll) dependsOn(modProjects.map { "${it.path}:publishMods" }) } -listOf(":sounds", ":fog").forEach { mod -> +listOf(":sounds", ":fog", ":snow").forEach { mod -> project(mod).pluginManager.withPlugin("imb11.publish-mod") { project(mod).tasks.withType().configureEach { mustRunAfter(":mru:publishMods") diff --git a/gradle/gradle-daemon-jvm.properties b/gradle/gradle-daemon-jvm.properties new file mode 100644 index 0000000..d35f707 --- /dev/null +++ b/gradle/gradle-daemon-jvm.properties @@ -0,0 +1 @@ +toolchainVersion=25 diff --git a/gradle/minecraft/26.3.versions.toml b/gradle/minecraft/26.3.versions.toml index d805f61..e4a32d8 100644 --- a/gradle/minecraft/26.3.versions.toml +++ b/gradle/minecraft/26.3.versions.toml @@ -29,10 +29,14 @@ travelers-backpack-neoforge = "26.1.2-11.2.4-neoforge" trinkets = "4.2.1+26.3" yacl-fabric = "3.9.6+26.3-fabric" yacl-neoforge = "3.9.7+26.3-neoforge" +sodium-fabric = "mc26.3-0.9.2-fabric" +sodium-neoforge = "mc26.3-0.9.2-neoforge" [libraries] iris-fabric = { module = "maven.modrinth:iris", version.ref = "iris-fabric" } iris-neoforge = { module = "maven.modrinth:iris", version.ref = "iris-neoforge" } +sodium-fabric = { module = "maven.modrinth:sodium", version.ref = "sodium-fabric" } +sodium-neoforge = { module = "maven.modrinth:sodium", version.ref = "sodium-neoforge" } accessories-fabric = { module = "io.wispforest:accessories-neoforge", version.ref = "accessories-fabric" } accessories-neoforge = { module = "io.wispforest:accessories-neoforge", version.ref = "accessories-neoforge" } cca-base = { module = "org.ladysnake.cardinal-components-api:cardinal-components-base", version.ref = "cca" } diff --git a/mru/src/main/java/cc/cassian/mru/client/fabric/FabricClientEvents.java b/mru/src/main/java/cc/cassian/mru/client/fabric/FabricClientEvents.java index 035ef12..a183b2e 100644 --- a/mru/src/main/java/cc/cassian/mru/client/fabric/FabricClientEvents.java +++ b/mru/src/main/java/cc/cassian/mru/client/fabric/FabricClientEvents.java @@ -2,12 +2,14 @@ package cc.cassian.mru.client.fabric; import cc.cassian.mru.client.events.ClientRegisterEvent; +import dev.imb11.mru.sync.client.SyncClientNetworking; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.loader.api.FabricLoader; public class FabricClientEvents implements ClientModInitializer { @Override public void onInitializeClient() { + SyncClientNetworking.init(); FabricLoader.getInstance().invokeEntrypoints("mru_client", ClientRegisterEvent.class, ClientRegisterEvent::onInitializeClient); } } diff --git a/mru/src/main/java/dev/imb11/mru/sync/SyncNetworking.java b/mru/src/main/java/dev/imb11/mru/sync/SyncNetworking.java new file mode 100644 index 0000000..c6a2d99 --- /dev/null +++ b/mru/src/main/java/dev/imb11/mru/sync/SyncNetworking.java @@ -0,0 +1,100 @@ +//? fabric { +package dev.imb11.mru.sync; + +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; + +public final class SyncNetworking { + private static boolean initialized; + + private SyncNetworking() { + } + + static void register(SyncedConfig config) { + init(); + PayloadTypeRegistry.clientboundPlay().register(config.syncType(), config.syncCodec()); + PayloadTypeRegistry.serverboundPlay().register(config.updateType(), config.updateCodec()); + ServerPlayNetworking.registerGlobalReceiver(config.updateType(), (payload, context) -> config.receiveUpdate(context.player(), payload.json())); + } + + private static void init() { + if (initialized) { + return; + } + + initialized = true; + ServerLifecycleEvents.SERVER_STARTED.register(SyncedConfig::setServer); + ServerLifecycleEvents.SERVER_STOPPED.register(server -> SyncedConfig.setServer(null)); + ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> SyncedConfig.onServerJoin(handler.getPlayer())); + } + + public static boolean canSend(ServerPlayer player, CustomPacketPayload.Type type) { + return ServerPlayNetworking.canSend(player, type); + } + + public static void send(ServerPlayer player, CustomPacketPayload payload) { + ServerPlayNetworking.send(player, payload); + } +} +//?} else { +/*package dev.imb11.mru.sync; + +import dev.imb11.mru.sync.client.SyncClientNetworking; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.event.entity.player.PlayerEvent; +import net.neoforged.neoforge.event.server.ServerStartedEvent; +import net.neoforged.neoforge.event.server.ServerStoppedEvent; +import net.neoforged.neoforge.network.PacketDistributor; +import net.neoforged.neoforge.network.event.RegisterPayloadHandlersEvent; +import net.neoforged.neoforge.network.registration.PayloadRegistrar; + +@EventBusSubscriber(modid = "mru") +public final class SyncNetworking { + private SyncNetworking() { + } + + static void register(SyncedConfig config) { + } + + @SubscribeEvent + public static void registerPayloads(RegisterPayloadHandlersEvent event) { + PayloadRegistrar registrar = event.registrar("1").optional(); + for (SyncedConfig config : SyncedConfig.all()) { + registrar.playToClient(config.syncType(), config.syncCodec(), (payload, context) -> SyncClientNetworking.receive(config, payload.json())); + registrar.playToServer(config.updateType(), config.updateCodec(), (payload, context) -> config.receiveUpdate((ServerPlayer) context.player(), payload.json())); + } + } + + @SubscribeEvent + public static void onServerStarted(ServerStartedEvent event) { + SyncedConfig.setServer(event.getServer()); + } + + @SubscribeEvent + public static void onServerStopped(ServerStoppedEvent event) { + SyncedConfig.setServer(null); + } + + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + SyncedConfig.onServerJoin(player); + } + } + + public static boolean canSend(ServerPlayer player, CustomPacketPayload.Type type) { + return player.connection.hasChannel(type.id()); + } + + public static void send(ServerPlayer player, CustomPacketPayload payload) { + PacketDistributor.sendToPlayer(player, payload); + } +} +*///?} diff --git a/mru/src/main/java/dev/imb11/mru/sync/SyncedConfig.java b/mru/src/main/java/dev/imb11/mru/sync/SyncedConfig.java new file mode 100644 index 0000000..702f0f9 --- /dev/null +++ b/mru/src/main/java/dev/imb11/mru/sync/SyncedConfig.java @@ -0,0 +1,182 @@ +package dev.imb11.mru.sync; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +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 net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +public final class SyncedConfig { + private static final Logger LOGGER = LoggerFactory.getLogger("MRU"); + private static final Gson GSON = new Gson(); + private static final int MAX_LENGTH = 32000; + private static final Map> CONFIGS = new LinkedHashMap<>(); + private static @Nullable MinecraftServer server; + + private final Identifier id; + private final Class type; + private final Supplier local; + private final Consumer apply; + private final CustomPacketPayload.Type syncType; + private final CustomPacketPayload.Type updateType; + private final StreamCodec syncCodec; + private final StreamCodec updateCodec; + private BooleanSupplier clientRequired = () -> false; + private volatile @Nullable T remote; + private volatile boolean serverPresent; + + private SyncedConfig(Identifier id, Class type, Supplier local, Consumer apply) { + this.id = id; + this.type = type; + this.local = local; + this.apply = apply; + this.syncType = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(id.getNamespace(), id.getPath() + "/sync")); + this.updateType = new CustomPacketPayload.Type<>(Identifier.fromNamespaceAndPath(id.getNamespace(), id.getPath() + "/update")); + this.syncCodec = codec(this.syncType); + this.updateCodec = codec(this.updateType); + } + + public static SyncedConfig register(Identifier id, Class type, Supplier local, Consumer apply) { + if (CONFIGS.containsKey(id)) { + throw new IllegalStateException("Synced config " + id + " is already registered"); + } + + SyncedConfig config = new SyncedConfig<>(id, type, local, apply); + CONFIGS.put(id, config); + SyncNetworking.register(config); + return config; + } + + public SyncedConfig requireClient(BooleanSupplier required) { + this.clientRequired = required; + return this; + } + + public T get() { + T value = this.remote; + return value != null ? value : this.local.get(); + } + + public boolean isRemote() { + return this.remote != null; + } + + public boolean serverPresent() { + return this.serverPresent; + } + + public void broadcast() { + MinecraftServer current = server; + if (current != null) { + current.execute(() -> current.getPlayerList().getPlayers().forEach(this::send)); + } + } + + public Identifier id() { + return this.id; + } + + public CustomPacketPayload.Type syncType() { + return this.syncType; + } + + public CustomPacketPayload.Type updateType() { + return this.updateType; + } + + public StreamCodec syncCodec() { + return this.syncCodec; + } + + public StreamCodec updateCodec() { + return this.updateCodec; + } + + public static Collection> all() { + return Collections.unmodifiableCollection(CONFIGS.values()); + } + + public static void setServer(@Nullable MinecraftServer current) { + server = current; + } + + public static void onServerJoin(ServerPlayer player) { + for (SyncedConfig config : CONFIGS.values()) { + if (SyncNetworking.canSend(player, config.syncType)) { + config.send(player); + } else if (config.clientRequired.getAsBoolean()) { + player.connection.disconnect(Component.literal("This server requires the " + config.id.getNamespace() + " mod to be installed.")); + return; + } + } + } + + public void receiveUpdate(ServerPlayer player, String json) { + if (!player.canUseGameMasterBlocks()) { + LOGGER.warn("{} tried to change synced config {} without permission", player.getName().getString(), this.id); + return; + } + + T value = this.parse(json); + if (value != null) { + this.apply.accept(value); + LOGGER.info("{} changed synced config {}", player.getName().getString(), this.id); + this.broadcast(); + } + } + + public void receiveSync(String json) { + this.remote = this.parse(json); + } + + public void setServerPresent(boolean present) { + this.serverPresent = present; + } + + public void clearRemote() { + this.remote = null; + this.serverPresent = false; + } + + public String serialize(T value) { + return GSON.toJson(value); + } + + private void send(ServerPlayer player) { + if (SyncNetworking.canSend(player, this.syncType)) { + SyncNetworking.send(player, new Payload(this.syncType, this.serialize(this.local.get()))); + } + } + + private @Nullable T parse(String json) { + try { + return GSON.fromJson(json, this.type); + } catch (JsonParseException e) { + LOGGER.error("Failed to read synced config {}", this.id, e); + return null; + } + } + + private static StreamCodec codec(CustomPacketPayload.Type type) { + return ByteBufCodecs.stringUtf8(MAX_LENGTH).cast().map(json -> new Payload(type, json), Payload::json); + } + + public record Payload(CustomPacketPayload.Type type, String json) implements CustomPacketPayload { + } +} diff --git a/mru/src/main/java/dev/imb11/mru/sync/client/SyncClientNetworking.java b/mru/src/main/java/dev/imb11/mru/sync/client/SyncClientNetworking.java new file mode 100644 index 0000000..4015a90 --- /dev/null +++ b/mru/src/main/java/dev/imb11/mru/sync/client/SyncClientNetworking.java @@ -0,0 +1,96 @@ +//? fabric { +package dev.imb11.mru.sync.client; + +import dev.imb11.mru.sync.SyncedConfig; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.client.Minecraft; + +public final class SyncClientNetworking { + private SyncClientNetworking() { + } + + public static void init() { + for (SyncedConfig config : SyncedConfig.all()) { + ClientPlayNetworking.registerGlobalReceiver(config.syncType(), (payload, context) -> receive(config, payload.json())); + } + + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> SyncedConfig.all().forEach(config -> config.setServerPresent(ClientPlayNetworking.canSend(config.updateType())))); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> SyncedConfig.all().forEach(SyncedConfig::clearRemote)); + } + + public static void receive(SyncedConfig config, String json) { + if (!Minecraft.getInstance().isLocalServer()) { + config.receiveSync(json); + } + } + + public static void requestUpdate(SyncedConfig config, T value) { + if (ClientPlayNetworking.canSend(config.updateType())) { + ClientPlayNetworking.send(new SyncedConfig.Payload(config.updateType(), config.serialize(value))); + } + } + + public static boolean canEdit(SyncedConfig config) { + if (!config.isRemote()) { + return true; + } + + Minecraft minecraft = Minecraft.getInstance(); + return minecraft.player != null && minecraft.player.canUseGameMasterBlocks(); + } +} +//?} else { +/*package dev.imb11.mru.sync.client; + +import dev.imb11.mru.sync.SyncedConfig; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent; +import net.neoforged.neoforge.client.network.ClientPacketDistributor; + +@EventBusSubscriber(modid = "mru", value = Dist.CLIENT) +public final class SyncClientNetworking { + private SyncClientNetworking() { + } + + public static void init() { + } + + @SubscribeEvent + public static void onLoggingIn(ClientPlayerNetworkEvent.LoggingIn event) { + ClientPacketListener connection = Minecraft.getInstance().getConnection(); + SyncedConfig.all().forEach(config -> config.setServerPresent(connection != null && connection.hasChannel(config.updateType().id()))); + } + + @SubscribeEvent + public static void onLoggingOut(ClientPlayerNetworkEvent.LoggingOut event) { + SyncedConfig.all().forEach(SyncedConfig::clearRemote); + } + + public static void receive(SyncedConfig config, String json) { + if (!Minecraft.getInstance().isLocalServer()) { + config.receiveSync(json); + } + } + + public static void requestUpdate(SyncedConfig config, T value) { + ClientPacketListener connection = Minecraft.getInstance().getConnection(); + if (connection != null && connection.hasChannel(config.updateType().id())) { + ClientPacketDistributor.sendToServer(new SyncedConfig.Payload(config.updateType(), config.serialize(value))); + } + } + + public static boolean canEdit(SyncedConfig config) { + if (!config.isRemote()) { + return true; + } + + Minecraft minecraft = Minecraft.getInstance(); + return minecraft.player != null && minecraft.player.canUseGameMasterBlocks(); + } +} +*///?} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8369efa..d212a50 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,7 +22,8 @@ val modTargets = mapOf( "mru" to listOf("26.2", "26.3"), "sounds" to listOf("26.3"), "shields" to listOf("26.2", "26.3"), - "glass" to listOf("26.2") + "glass" to listOf("26.2"), + "snow" to listOf("26.2", "26.3") ) dependencyResolutionManagement { diff --git a/shields/src/main/java/dev/imb11/shields/client/ExtraShieldSpecialRenderer.java b/shields/src/main/java/dev/imb11/shields/client/ExtraShieldSpecialRenderer.java index 57d5ed2..2ad1a73 100644 --- a/shields/src/main/java/dev/imb11/shields/client/ExtraShieldSpecialRenderer.java +++ b/shields/src/main/java/dev/imb11/shields/client/ExtraShieldSpecialRenderer.java @@ -105,4 +105,4 @@ public MapCodec type() { .bakeLayer(new ModelLayerLocation(modelLayer, "main"))), Sheets.SHIELD_MAPPER.apply(this.base), Sheets.SHIELD_MAPPER.apply(this.noPattern)); } } -} \ No newline at end of file +} diff --git a/snow/CHANGELOG.md b/snow/CHANGELOG.md new file mode 100644 index 0000000..1a43b06 --- /dev/null +++ b/snow/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +## [1.0.0] + +### Added + +- Initial release. +- Support for 26.2 and 26.3 on Fabric and NeoForge. diff --git a/snow/LICENSE b/snow/LICENSE new file mode 100644 index 0000000..e0944f6 --- /dev/null +++ b/snow/LICENSE @@ -0,0 +1,13 @@ +ALL RIGHTS RESERVED + +Copyright (c) 2024 Calum H. (IMB11) + +THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +All sound effects within the assets/sounds/sounds/blocks folder are obtained from Zapsplat under ZapSplat Standard License for Gold Members. diff --git a/snow/README.md b/snow/README.md new file mode 100644 index 0000000..13d6c79 --- /dev/null +++ b/snow/README.md @@ -0,0 +1,43 @@ +Join the discord server Report a bug + +**Complete parity with Bedrock Edition's snow mechanics while being as unobtrusive as possible.** + +*Snow* is a mod that is focused around Minecraft's snow mechanics, specifically bringing much-requested features from Bedrock Edition. Most of the mod is client-side only, some things will be needed to be installed on both client and server to use though, like Snowlogging. + +## Snowlogging + +> You need to install *Snow* on the server-side to use this to use this on multiplayer servers - but if you have *Snow* installed client-side you'll still be able to join Vanilla servers. + +Plants can be snowlogged if they're in the `snow:snowloggable` block tag. To remove the snow just break it like normal, it will leave the plant alone. + +![Example of snowlogging in a snowy biome](https://cdn.modrinth.com/data/MlSnJj7B/images/230a48668ecc14e7d29a6d8f3c565d290398278b.png) + +## Snowy leaves + +> This is completely client-side only, you don't need to install _Snow_ on the server-side to use this! + +When you're in a snowy biome, and it starts snowing, leaves will slowly turn white! This uses core shaders, and is fully compatible with Sodium and Iris. + +![Example of snowy leaves in a snowy biome when snowing](https://cdn.modrinth.com/data/MlSnJj7B/images/b0eb9a37b5fff19c882c856f733377fe2500ac8b.webp) + +## Better snow generation + +> You need to install *Snow* on the server-side to use this on multiplayer servers - but if you have *Snow* installed client-side you'll still be able to join Vanilla servers. + +*Snow* changes the way snow generates in snowy biomes, there's much more layering that follows the terrain, and when it snows it will continue to layer up, with each biome having it's own unique thickness of snow on the ground - this can be controlled via datapacks and data generation, check out the wiki (coming soon)! + +![Example of snow generation changes](https://cdn.modrinth.com/data/MlSnJj7B/images/0511b2f5184757076ccf72504457c32771ff5f88.webp) + +## Coming soon: + +- Snow placement under trees: Currently the mod does not support placing snow under trees, you can use [Snow Under Trees](https://modrinth.com/mod/snow-under-trees-remastered) for this, but future updates will move this functionality into this mod. + +--- + + + +Custom sound effects obtained from [Zapsplat](https://www.zapsplat.com) under ZapSplat Standard License for Gold Members. + +By using my mods, you agree to the [End User License Agreement](https://legal.imb11.dev/mods/eula). You are free to use this mod within your Modrinth & CurseForge modpacks as long as the EULA is respected. + + \ No newline at end of file diff --git a/snow/assets/snowlogged_plants.png b/snow/assets/snowlogged_plants.png new file mode 100644 index 0000000..2ea8a6c Binary files /dev/null and b/snow/assets/snowlogged_plants.png differ diff --git a/snow/assets/snowy-trees-example.webm b/snow/assets/snowy-trees-example.webm new file mode 100644 index 0000000..4950968 Binary files /dev/null and b/snow/assets/snowy-trees-example.webm differ diff --git a/snow/branch.gradle.kts b/snow/branch.gradle.kts new file mode 100644 index 0000000..f8f400d --- /dev/null +++ b/snow/branch.gradle.kts @@ -0,0 +1,3 @@ +plugins { + id("imb11.publish-mod") +} diff --git a/snow/build.fabric.gradle.kts b/snow/build.fabric.gradle.kts new file mode 100644 index 0000000..e75f049 --- /dev/null +++ b/snow/build.fabric.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("imb11.fabric-mod") +} + +val mruProject = project(":mru:${project.name}") +evaluationDependsOn(mruProject.path) + +dependencies { + implementation(project(mruProject.path)) + implementation(mcLibrary("yacl-fabric")) + implementation(mcLibrary("modmenu")) + compileOnly(mcLibrary("sodium-fabric")) + compileOnly(mcLibrary("iris-fabric")) + val withIris = providers.gradleProperty("with_iris").isPresent + if (withIris || providers.gradleProperty("with_sodium").isPresent) { + "localRuntime"(mcLibrary("sodium-fabric")) + } + if (withIris) { + "localRuntime"(mcLibrary("iris-fabric")) + } +} + +loom { + mods { + register("mru") { + sourceSet(mruProject.extensions.getByType().getByName("main")) + } + } +} diff --git a/snow/build.neoforge.gradle.kts b/snow/build.neoforge.gradle.kts new file mode 100644 index 0000000..c6605e1 --- /dev/null +++ b/snow/build.neoforge.gradle.kts @@ -0,0 +1,39 @@ +plugins { + id("imb11.neoforge-mod") +} + +val mruProject = project(":mru:${project.name}") +evaluationDependsOn(mruProject.path) + +val sodiumDistribution = configurations.create("sodiumDistribution") { + isCanBeConsumed = false + isTransitive = false +} + +dependencies { + implementation(project(mruProject.path)) + implementation(mcLibrary("yacl-neoforge")) + add(sodiumDistribution.name, mcLibrary("sodium-neoforge")) + compileOnly(mcLibrary("iris-neoforge")) + compileOnly(files(provider { + sodiumDistribution.files.flatMap { zipTree(it).matching { include("META-INF/jarjar/net.caffeinemc.sodium-*-mod.jar") }.files } + })) + val withIris = providers.gradleProperty("with_iris").isPresent + if (withIris || providers.gradleProperty("with_sodium").isPresent) { + runtimeOnly(mcLibrary("sodium-neoforge")) + } + if (withIris) { + runtimeOnly(mcLibrary("iris-neoforge")) + } +} + +neoForge { + interfaceInjectionData.from(mruProject.tasks.named("stonecutterGenerate").map { + mruProject.layout.buildDirectory.file("generated/stonecutter/main/resources/interfaces.json").get() + }) + mods { + register("mru") { + sourceSet(mruProject.extensions.getByType().getByName("main")) + } + } +} diff --git a/snow/mod.properties b/snow/mod.properties new file mode 100644 index 0000000..9e3e2d3 --- /dev/null +++ b/snow/mod.properties @@ -0,0 +1,12 @@ +mod.id=snow +mod.name=Snow +mod.description=Brings Bedrock Edition's snow mechanics to Java Edition. +mod.license=All-Rights-Reserved +mod.version=1.0.0 +mod.group=dev.imb11.snow +mod.required=mru +mod.datagen=true +mod.gametest=true +publish.modrinth=MlSnJj7B +publish.dependencies.fabric=fabric-api,yacl,mru +publish.dependencies.neoforge=yacl,mru diff --git a/snow/src/generated/26.2/data/snow/snow/snow_profile/cold.json b/snow/src/generated/26.2/data/snow/snow/snow_profile/cold.json new file mode 100644 index 0000000..67ae2b4 --- /dev/null +++ b/snow/src/generated/26.2/data/snow/snow/snow_profile/cold.json @@ -0,0 +1,9 @@ +{ + "biomes": [ + "minecraft:windswept_hills", + "minecraft:stony_shore", + "minecraft:taiga" + ], + "max_layers": 2, + "min_layers": 1 +} \ No newline at end of file diff --git a/snow/src/generated/26.2/data/snow/snow/snow_profile/frozen.json b/snow/src/generated/26.2/data/snow/snow/snow_profile/frozen.json new file mode 100644 index 0000000..7cceaf9 --- /dev/null +++ b/snow/src/generated/26.2/data/snow/snow/snow_profile/frozen.json @@ -0,0 +1,13 @@ +{ + "biomes": [ + "minecraft:snowy_beach", + "minecraft:grove", + "minecraft:frozen_peaks", + "minecraft:jagged_peaks", + "minecraft:frozen_river", + "minecraft:frozen_ocean", + "minecraft:deep_frozen_ocean" + ], + "max_layers": 2, + "min_layers": 1 +} \ No newline at end of file diff --git a/snow/src/generated/26.2/data/snow/snow/snow_profile/ice_spikes.json b/snow/src/generated/26.2/data/snow/snow/snow_profile/ice_spikes.json new file mode 100644 index 0000000..7a78248 --- /dev/null +++ b/snow/src/generated/26.2/data/snow/snow/snow_profile/ice_spikes.json @@ -0,0 +1,6 @@ +{ + "accumulation_rate": 3.0, + "biomes": "minecraft:ice_spikes", + "max_layers": 12, + "min_layers": 4 +} \ No newline at end of file diff --git a/snow/src/generated/26.2/data/snow/snow/snow_profile/snowy_plains.json b/snow/src/generated/26.2/data/snow/snow/snow_profile/snowy_plains.json new file mode 100644 index 0000000..48d1f4b --- /dev/null +++ b/snow/src/generated/26.2/data/snow/snow/snow_profile/snowy_plains.json @@ -0,0 +1,6 @@ +{ + "accumulation_rate": 2.0, + "biomes": "minecraft:snowy_plains", + "max_layers": 8, + "min_layers": 2 +} \ No newline at end of file diff --git a/snow/src/generated/26.2/data/snow/snow/snow_profile/snowy_taiga.json b/snow/src/generated/26.2/data/snow/snow/snow_profile/snowy_taiga.json new file mode 100644 index 0000000..8e77347 --- /dev/null +++ b/snow/src/generated/26.2/data/snow/snow/snow_profile/snowy_taiga.json @@ -0,0 +1,9 @@ +{ + "accumulation_rate": 1.5, + "biomes": [ + "minecraft:snowy_taiga", + "minecraft:snowy_slopes" + ], + "max_layers": 4, + "min_layers": 1 +} \ No newline at end of file diff --git a/snow/src/generated/26.2/data/snow/structure/gametest/chunk_platform.nbt b/snow/src/generated/26.2/data/snow/structure/gametest/chunk_platform.nbt new file mode 100644 index 0000000..1efe5fa Binary files /dev/null and b/snow/src/generated/26.2/data/snow/structure/gametest/chunk_platform.nbt differ diff --git a/snow/src/generated/26.2/data/snow/structure/gametest/platform.nbt b/snow/src/generated/26.2/data/snow/structure/gametest/platform.nbt new file mode 100644 index 0000000..6422ef7 Binary files /dev/null and b/snow/src/generated/26.2/data/snow/structure/gametest/platform.nbt differ diff --git a/snow/src/generated/26.2/data/snow/tags/block/frostable_leaves.json b/snow/src/generated/26.2/data/snow/tags/block/frostable_leaves.json new file mode 100644 index 0000000..93607c6 --- /dev/null +++ b/snow/src/generated/26.2/data/snow/tags/block/frostable_leaves.json @@ -0,0 +1,10 @@ +{ + "values": [ + "minecraft:oak_leaves", + "minecraft:spruce_leaves", + "minecraft:birch_leaves", + "minecraft:jungle_leaves", + "minecraft:acacia_leaves", + "minecraft:mangrove_leaves" + ] +} diff --git a/snow/src/generated/26.2/data/snow/tags/block/snowloggable.json b/snow/src/generated/26.2/data/snow/tags/block/snowloggable.json new file mode 100644 index 0000000..bc6b25a --- /dev/null +++ b/snow/src/generated/26.2/data/snow/tags/block/snowloggable.json @@ -0,0 +1,34 @@ +{ + "values": [ + "minecraft:short_grass", + "minecraft:fern", + "minecraft:short_dry_grass", + "minecraft:tall_dry_grass", + "minecraft:bush", + "minecraft:firefly_bush", + "minecraft:nether_sprouts", + "minecraft:crimson_roots", + "minecraft:warped_roots", + "minecraft:dandelion", + "minecraft:golden_dandelion", + "minecraft:poppy", + "minecraft:blue_orchid", + "minecraft:allium", + "minecraft:azure_bluet", + "minecraft:red_tulip", + "minecraft:orange_tulip", + "minecraft:white_tulip", + "minecraft:pink_tulip", + "minecraft:oxeye_daisy", + "minecraft:cornflower", + "minecraft:lily_of_the_valley", + "minecraft:wither_rose", + "minecraft:torchflower", + "minecraft:open_eyeblossom", + "minecraft:closed_eyeblossom", + "minecraft:brown_mushroom", + "minecraft:red_mushroom", + "minecraft:crimson_fungus", + "minecraft:warped_fungus" + ] +} diff --git a/snow/src/generated/26.3/data/snow/snow/snow_profile/cold.json b/snow/src/generated/26.3/data/snow/snow/snow_profile/cold.json new file mode 100644 index 0000000..67ae2b4 --- /dev/null +++ b/snow/src/generated/26.3/data/snow/snow/snow_profile/cold.json @@ -0,0 +1,9 @@ +{ + "biomes": [ + "minecraft:windswept_hills", + "minecraft:stony_shore", + "minecraft:taiga" + ], + "max_layers": 2, + "min_layers": 1 +} \ No newline at end of file diff --git a/snow/src/generated/26.3/data/snow/snow/snow_profile/frozen.json b/snow/src/generated/26.3/data/snow/snow/snow_profile/frozen.json new file mode 100644 index 0000000..7cceaf9 --- /dev/null +++ b/snow/src/generated/26.3/data/snow/snow/snow_profile/frozen.json @@ -0,0 +1,13 @@ +{ + "biomes": [ + "minecraft:snowy_beach", + "minecraft:grove", + "minecraft:frozen_peaks", + "minecraft:jagged_peaks", + "minecraft:frozen_river", + "minecraft:frozen_ocean", + "minecraft:deep_frozen_ocean" + ], + "max_layers": 2, + "min_layers": 1 +} \ No newline at end of file diff --git a/snow/src/generated/26.3/data/snow/snow/snow_profile/ice_spikes.json b/snow/src/generated/26.3/data/snow/snow/snow_profile/ice_spikes.json new file mode 100644 index 0000000..7a78248 --- /dev/null +++ b/snow/src/generated/26.3/data/snow/snow/snow_profile/ice_spikes.json @@ -0,0 +1,6 @@ +{ + "accumulation_rate": 3.0, + "biomes": "minecraft:ice_spikes", + "max_layers": 12, + "min_layers": 4 +} \ No newline at end of file diff --git a/snow/src/generated/26.3/data/snow/snow/snow_profile/snowy_plains.json b/snow/src/generated/26.3/data/snow/snow/snow_profile/snowy_plains.json new file mode 100644 index 0000000..48d1f4b --- /dev/null +++ b/snow/src/generated/26.3/data/snow/snow/snow_profile/snowy_plains.json @@ -0,0 +1,6 @@ +{ + "accumulation_rate": 2.0, + "biomes": "minecraft:snowy_plains", + "max_layers": 8, + "min_layers": 2 +} \ No newline at end of file diff --git a/snow/src/generated/26.3/data/snow/snow/snow_profile/snowy_taiga.json b/snow/src/generated/26.3/data/snow/snow/snow_profile/snowy_taiga.json new file mode 100644 index 0000000..8e77347 --- /dev/null +++ b/snow/src/generated/26.3/data/snow/snow/snow_profile/snowy_taiga.json @@ -0,0 +1,9 @@ +{ + "accumulation_rate": 1.5, + "biomes": [ + "minecraft:snowy_taiga", + "minecraft:snowy_slopes" + ], + "max_layers": 4, + "min_layers": 1 +} \ No newline at end of file diff --git a/snow/src/generated/26.3/data/snow/structure/gametest/chunk_platform.nbt b/snow/src/generated/26.3/data/snow/structure/gametest/chunk_platform.nbt new file mode 100644 index 0000000..f0adc40 Binary files /dev/null and b/snow/src/generated/26.3/data/snow/structure/gametest/chunk_platform.nbt differ diff --git a/snow/src/generated/26.3/data/snow/structure/gametest/platform.nbt b/snow/src/generated/26.3/data/snow/structure/gametest/platform.nbt new file mode 100644 index 0000000..dae6241 Binary files /dev/null and b/snow/src/generated/26.3/data/snow/structure/gametest/platform.nbt differ diff --git a/snow/src/generated/26.3/data/snow/tags/block/frostable_leaves.json b/snow/src/generated/26.3/data/snow/tags/block/frostable_leaves.json new file mode 100644 index 0000000..93607c6 --- /dev/null +++ b/snow/src/generated/26.3/data/snow/tags/block/frostable_leaves.json @@ -0,0 +1,10 @@ +{ + "values": [ + "minecraft:oak_leaves", + "minecraft:spruce_leaves", + "minecraft:birch_leaves", + "minecraft:jungle_leaves", + "minecraft:acacia_leaves", + "minecraft:mangrove_leaves" + ] +} diff --git a/snow/src/generated/26.3/data/snow/tags/block/snowloggable.json b/snow/src/generated/26.3/data/snow/tags/block/snowloggable.json new file mode 100644 index 0000000..b82e8ee --- /dev/null +++ b/snow/src/generated/26.3/data/snow/tags/block/snowloggable.json @@ -0,0 +1,35 @@ +{ + "values": [ + "minecraft:short_grass", + "minecraft:fern", + "minecraft:short_dry_grass", + "minecraft:tall_dry_grass", + "minecraft:bush", + "minecraft:red_shrub", + "minecraft:firefly_bush", + "minecraft:nether_sprouts", + "minecraft:crimson_roots", + "minecraft:warped_roots", + "minecraft:dandelion", + "minecraft:golden_dandelion", + "minecraft:poppy", + "minecraft:blue_orchid", + "minecraft:allium", + "minecraft:azure_bluet", + "minecraft:red_tulip", + "minecraft:orange_tulip", + "minecraft:white_tulip", + "minecraft:pink_tulip", + "minecraft:oxeye_daisy", + "minecraft:cornflower", + "minecraft:lily_of_the_valley", + "minecraft:wither_rose", + "minecraft:torchflower", + "minecraft:open_eyeblossom", + "minecraft:closed_eyeblossom", + "minecraft:brown_mushroom", + "minecraft:red_mushroom", + "minecraft:crimson_fungus", + "minecraft:warped_fungus" + ] +} diff --git a/snow/src/main/java/dev/imb11/snow/Snow.java b/snow/src/main/java/dev/imb11/snow/Snow.java new file mode 100644 index 0000000..d6184b3 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/Snow.java @@ -0,0 +1,17 @@ +package dev.imb11.snow; + +import cc.cassian.mru.util.CommonUtils; +import dev.imb11.snow.config.SnowConfig; +import net.minecraft.resources.Identifier; + +public class Snow { + public static final String MOD_ID = "snow"; + + public static Identifier id(String path) { + return CommonUtils.id(MOD_ID, path); + } + + public static void init() { + SnowConfig.load(); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/accumulation/SnowAccumulation.java b/snow/src/main/java/dev/imb11/snow/accumulation/SnowAccumulation.java new file mode 100644 index 0000000..0f6bb32 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/accumulation/SnowAccumulation.java @@ -0,0 +1,87 @@ +package dev.imb11.snow.accumulation; + +import dev.imb11.snow.generation.SnowGeneration; +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import dev.imb11.snow.snowlogging.Snowlogging; +import dev.imb11.snow.util.SnowColumn; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.Mth; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.level.levelgen.Heightmap; +import org.jspecify.annotations.Nullable; + +import java.util.Optional; + +public class SnowAccumulation { + private static final int SETTLE_RADIUS = 1; + + public static void accumulate(ServerLevel level, BlockPos base) { + float rate = SnowProfiles.get(level, level.getBiome(base)).map(SnowProfile::accumulationRate).orElse(1f); + int attempts = Mth.floor(rate) + (level.getRandom().nextFloat() < Mth.frac(rate) ? 1 : 0); + for (int i = 0; i < attempts; i++) { + Column column = settle(level, base); + if (column == null) { + return; + } + + SnowColumn.place(level, column.pos, column.current + 1, Block.UPDATE_ALL, true); + } + } + + public static int target(ServerLevel level, BlockPos pos) { + Optional profile = SnowProfiles.get(level, level.getBiome(pos)); + int gamerule = level.getGameRules().get(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT); + int cap = Math.min(gamerule, SnowColumn.MAX_LAYERS); + if (profile.isEmpty()) { + return cap; + } + + int shaped = SnowGeneration.layers(level, pos, profile.get()); + return gamerule != GameRules.MAX_SNOW_ACCUMULATION_HEIGHT.defaultValue() ? Math.min(shaped, cap) : shaped; + } + + private static @Nullable Column settle(ServerLevel level, BlockPos base) { + Column best = column(level, base); + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + for (int dx = -SETTLE_RADIUS; dx <= SETTLE_RADIUS; dx++) { + for (int dz = -SETTLE_RADIUS; dz <= SETTLE_RADIUS; dz++) { + if (dx == 0 && dz == 0) { + continue; + } + + BlockPos pos = level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, cursor.set(base.getX() + dx, base.getY(), base.getZ() + dz)); + if (Math.abs(pos.getY() - base.getY()) > 1 || !canSnow(level, pos)) { + continue; + } + + Column candidate = column(level, pos); + if (candidate != null && (best == null || candidate.fill() < best.fill())) { + best = candidate; + } + } + } + + return best; + } + + private static @Nullable Column column(ServerLevel level, BlockPos pos) { + int target = target(level, pos); + int current = SnowColumn.layers(level, pos); + return current < target ? new Column(pos.immutable(), current, target) : null; + } + + private static boolean canSnow(ServerLevel level, BlockPos pos) { + Biome biome = level.getBiome(pos).value(); + return biome.shouldSnow(level, pos) || Snowlogging.shouldSnow(biome, level, pos); + } + + private record Column(BlockPos pos, int current, int target) { + private float fill() { + return (float) this.current / this.target; + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/SnowClient.java b/snow/src/main/java/dev/imb11/snow/client/SnowClient.java new file mode 100644 index 0000000..6ce56ec --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/SnowClient.java @@ -0,0 +1,51 @@ +package dev.imb11.snow.client; + +import dev.imb11.snow.client.frost.FrostBackend; +import dev.imb11.snow.client.frost.FrostWeather; +import dev.imb11.snow.snowlogging.Snowlogging; +import dev.imb11.snow.snowlogging.SnowloggingAttachment; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import org.jspecify.annotations.Nullable; + +public final class SnowClient { + private static @Nullable ClientLevel level; + + public static void init() { + Snowlogging.setListener(SnowloggingClient.INSTANCE); + } + + public static boolean isCurrent(Level level) { + return level == SnowClient.level; + } + + public static void levelChanged(@Nullable ClientLevel level) { + SnowClient.level = level; + SnowloggingClient.INSTANCE.levelChanged(); + FrostWeather.INSTANCE.levelChanged(level); + FrostBackend.INSTANCE.levelChanged(); + } + + public static void chunkUnloaded(ClientLevel level, LevelChunk chunk) { + if (isCurrent(level)) { + SnowloggingClient.INSTANCE.plantsChanged(level, SnowloggingAttachment.get(chunk), null); + } + } + + public static void blockChanged(ClientLevel level, BlockPos pos, BlockState oldState, BlockState newState) { + if (isCurrent(level)) { + SnowloggingClient.INSTANCE.blockChanged(level, pos, oldState, newState); + } + } + + public static void tick(ClientLevel level) { + if (isCurrent(level)) { + SnowloggingClient.INSTANCE.tick(level); + FrostWeather.INSTANCE.tick(level); + FrostBackend.INSTANCE.tick(level); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/SnowloggedBlockStateModel.java b/snow/src/main/java/dev/imb11/snow/client/SnowloggedBlockStateModel.java new file mode 100644 index 0000000..cca49ab --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/SnowloggedBlockStateModel.java @@ -0,0 +1,168 @@ +//? fabric { +package dev.imb11.snow.client; + +import net.fabricmc.fabric.api.client.renderer.v1.mesh.QuadEmitter; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.client.renderer.block.dispatch.BlockStateModel; +import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart; +import net.minecraft.client.resources.model.sprite.Material; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.function.Predicate; + +public class SnowloggedBlockStateModel implements BlockStateModel { + private final BlockStateModel snow; + + public SnowloggedBlockStateModel(BlockStateModel snow) { + this.snow = snow; + } + + @Override + public void collectParts(RandomSource random, List output) { + this.snow.collectParts(random, output); + } + + @Override + public Material.Baked particleMaterial() { + return this.snow.particleMaterial(); + } + + @Override + public int materialFlags() { + return this.snow.materialFlags(); + } + + @Override + public void emitQuads(QuadEmitter emitter, BlockAndTintGetter level, BlockPos pos, BlockState state, RandomSource random, Predicate<@Nullable Direction> cullTest) { + this.snow.emitQuads(emitter, level, pos, state, random, cullTest); + BlockState plant = plantAt(level, pos); + if (plant == null) { + return; + } + + Vec3 offset = plant.getOffset(pos); + emitter.pushTransform(quad -> { + for (int i = 0; i < 4; i++) { + quad.pos(i, quad.x(i) + (float) offset.x, quad.y(i) + (float) offset.y, quad.z(i) + (float) offset.z); + } + + return true; + }); + model(plant).emitQuads(emitter, level, pos, plant, random, cullTest); + emitter.popTransform(); + } + + @Override + public @Nullable Object createGeometryKey(BlockAndTintGetter level, BlockPos pos, BlockState state, RandomSource random) { + return plantAt(level, pos) == null ? this.snow.createGeometryKey(level, pos, state, random) : null; + } + + @Override + public int materialFlags(BlockAndTintGetter level, BlockPos pos, BlockState state, RandomSource random) { + BlockState plant = plantAt(level, pos); + int flags = this.snow.materialFlags(level, pos, state, random); + return plant == null ? flags : flags | model(plant).materialFlags(level, pos, plant, random); + } + + private static @Nullable BlockState plantAt(BlockAndTintGetter level, BlockPos pos) { + return level == BlockAndTintGetter.EMPTY ? null : SnowloggingClient.INSTANCE.plantAt(pos); + } + + private static BlockStateModel model(BlockState plant) { + return Minecraft.getInstance().getModelManager().getBlockStateModelSet().get(plant); + } +} +//?} else { +/*package dev.imb11.snow.client; + +import com.mojang.math.Transformation; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.client.renderer.block.dispatch.BlockStateModel; +import net.minecraft.client.renderer.block.dispatch.BlockStateModelPart; +import net.minecraft.client.resources.model.geometry.BakedQuad; +import net.minecraft.client.resources.model.sprite.Material; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import net.minecraft.util.TriState; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.Vec3; +import net.neoforged.neoforge.client.model.DelegateBlockStateModel; +import net.neoforged.neoforge.client.model.quad.QuadTransforms; +import org.joml.Matrix4f; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class SnowloggedBlockStateModel extends DelegateBlockStateModel { + public SnowloggedBlockStateModel(BlockStateModel snow) { + super(snow); + } + + @Override + public void collectParts(BlockAndTintGetter level, BlockPos pos, BlockState state, RandomSource random, List parts) { + super.collectParts(level, pos, state, random, parts); + BlockState plant = plantAt(level, pos); + if (plant == null) { + return; + } + + Vec3 offset = plant.getOffset(pos); + Transformation transformation = new Transformation(new Matrix4f().translation((float) offset.x, (float) offset.y, (float) offset.z)); + List plantParts = new ArrayList<>(); + model(plant).collectParts(level, pos, plant, random, plantParts); + plantParts.forEach(part -> parts.add(new OffsetPart(part, transformation))); + } + + @Override + public int materialFlags(BlockAndTintGetter level, BlockPos pos, BlockState state) { + BlockState plant = plantAt(level, pos); + int flags = super.materialFlags(level, pos, state); + return plant == null ? flags : flags | model(plant).materialFlags(level, pos, plant); + } + + private static @Nullable BlockState plantAt(BlockAndTintGetter level, BlockPos pos) { + return level == BlockAndTintGetter.EMPTY ? null : SnowloggingClient.INSTANCE.plantAt(pos); + } + + private static BlockStateModel model(BlockState plant) { + return Minecraft.getInstance().getModelManager().getBlockStateModelSet().get(plant); + } + + private record OffsetPart(BlockStateModelPart part, Transformation transformation) implements BlockStateModelPart { + @Override + public List getQuads(@Nullable Direction direction) { + return this.part.getQuads(direction).stream().map(quad -> QuadTransforms.applyTransformation(quad, this.transformation)).toList(); + } + + @Override + public boolean useAmbientOcclusion() { + return this.part.useAmbientOcclusion(); + } + + @Override + public TriState ambientOcclusion() { + return this.part.ambientOcclusion(); + } + + @Override + public Material.Baked particleMaterial() { + return this.part.particleMaterial(); + } + + @Override + public int materialFlags() { + return this.part.materialFlags(); + } + } +} +*///?} diff --git a/snow/src/main/java/dev/imb11/snow/client/SnowloggedTintSource.java b/snow/src/main/java/dev/imb11/snow/client/SnowloggedTintSource.java new file mode 100644 index 0000000..7a9387b --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/SnowloggedTintSource.java @@ -0,0 +1,32 @@ +package dev.imb11.snow.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.color.block.BlockTintSource; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; + +public class SnowloggedTintSource implements BlockTintSource { + private static final int WHITE = -1; + + @Override + public int color(BlockState state) { + return WHITE; + } + + @Override + public int colorInWorld(BlockState state, BlockAndTintGetter level, BlockPos pos) { + BlockState plant = SnowloggingClient.INSTANCE.plantAt(pos); + if (plant == null) { + return WHITE; + } + + BlockTintSource source = Minecraft.getInstance().getBlockColors().getTintSource(plant, 0); + return source == null ? WHITE : source.colorInWorld(plant, level, pos); + } + + @Override + public int colorAsTerrainParticle(BlockState state, BlockAndTintGetter level, BlockPos pos) { + return WHITE; + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/SnowloggingClient.java b/snow/src/main/java/dev/imb11/snow/client/SnowloggingClient.java new file mode 100644 index 0000000..ba688a2 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/SnowloggingClient.java @@ -0,0 +1,105 @@ +package dev.imb11.snow.client; + +import dev.imb11.snow.config.SnowConfig; +import dev.imb11.snow.snowlogging.SnowloggedPlants; +import dev.imb11.snow.snowlogging.Snowlogging; +import dev.imb11.snow.snowlogging.SnowloggingListener; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class SnowloggingClient implements SnowloggingListener { + public static final SnowloggingClient INSTANCE = new SnowloggingClient(); + private final Map plants = new ConcurrentHashMap<>(); + private final Map predictions = new ConcurrentHashMap<>(); + + public @Nullable BlockState plantAt(BlockPos pos) { + Prediction prediction = this.predictions.get(pos.asLong()); + return prediction != null ? prediction.plant() : this.plants.get(pos.asLong()); + } + + public void levelChanged() { + this.plants.clear(); + this.predictions.clear(); + } + + @Override + public void plantsChanged(Level level, @Nullable SnowloggedPlants previous, @Nullable SnowloggedPlants current) { + if (!SnowClient.isCurrent(level)) { + return; + } + + ClientLevel clientLevel = (ClientLevel) level; + if (previous != null) { + previous.plants().keySet().forEach(pos -> { + if (current == null || current.get(pos) == null) { + this.plants.remove(pos.asLong()); + setDirty(clientLevel, pos); + } + }); + } + + if (current != null) { + current.plants().forEach((pos, plant) -> { + BlockState shown = this.plantAt(pos); + this.predictions.remove(pos.asLong()); + this.plants.put(pos.asLong(), plant); + if (shown != plant) { + setDirty(clientLevel, pos); + } + }); + } + } + + @Override + public void plantPredicted(Level level, BlockPos pos, BlockState plant) { + if (SnowClient.isCurrent(level)) { + this.predict(level, pos, plant); + setDirty((ClientLevel) level, pos); + } + } + + public void blockChanged(ClientLevel level, BlockPos pos, BlockState oldState, BlockState newState) { + if (newState.is(Blocks.SNOW) && !oldState.is(Blocks.SNOW) && SnowConfig.SYNC.serverPresent() && Snowlogging.canSnowlog(oldState)) { + if (!this.plants.containsKey(pos.asLong())) { + this.predict(level, pos, oldState); + } + } else if (oldState.is(Blocks.SNOW) && !newState.is(Blocks.SNOW)) { + this.predictions.remove(pos.asLong()); + } + } + + public void tick(ClientLevel level) { + if (this.predictions.isEmpty()) { + return; + } + + long time = level.getGameTime(); + this.predictions.entrySet().removeIf(entry -> { + if (entry.getValue().expiresAt() > time) { + return false; + } + + setDirty(level, BlockPos.of(entry.getKey())); + return true; + }); + } + + private void predict(Level level, BlockPos pos, BlockState plant) { + this.predictions.put(pos.asLong(), new Prediction(plant, level.getGameTime() + 60)); + } + + private static void setDirty(ClientLevel level, BlockPos pos) { + level.setSectionDirtyWithNeighbors(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getY()), SectionPos.blockToSectionCoord(pos.getZ())); + } + + private record Prediction(BlockState plant, long expiresAt) { + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/BiomeTemperatureResolver.java b/snow/src/main/java/dev/imb11/snow/client/frost/BiomeTemperatureResolver.java new file mode 100644 index 0000000..bc2ee2e --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/BiomeTemperatureResolver.java @@ -0,0 +1,32 @@ +package dev.imb11.snow.client.frost; + +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.core.BlockPos; +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; +import net.minecraft.world.level.ColorResolver; + +public final class BiomeTemperatureResolver { + private static final float MIN_TEMPERATURE = -1.0F; + private static final float MAX_TEMPERATURE = 1.0F; + public static final ColorResolver RESOLVER = (biome, x, z) -> encode(biome.hasPrecipitation() ? biome.getBaseTemperature() : MAX_TEMPERATURE); + + public static float temperatureAt(BlockAndTintGetter level, BlockPos pos, int seaLevel) { + float temperature = decode(level.getBlockTint(pos, RESOLVER)); + int snowLine = seaLevel + 17; + if (pos.getY() > snowLine) { + temperature -= (pos.getY() - snowLine) * 0.05F / 40.0F; + } + + return temperature; + } + + private static int encode(float temperature) { + int value = Math.round(Mth.clamp((temperature - MIN_TEMPERATURE) / (MAX_TEMPERATURE - MIN_TEMPERATURE), 0.0F, 1.0F) * 255.0F); + return ARGB.color(255, value, value, value); + } + + private static float decode(int color) { + return MIN_TEMPERATURE + ARGB.red(color) / 255.0F * (MAX_TEMPERATURE - MIN_TEMPERATURE); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/FrostBackend.java b/snow/src/main/java/dev/imb11/snow/client/frost/FrostBackend.java new file mode 100644 index 0000000..8b003a1 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/FrostBackend.java @@ -0,0 +1,73 @@ +package dev.imb11.snow.client.frost; + +import dev.imb11.snow.client.frost.compat.IrisCompat; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class FrostBackend { + public static final FrostBackend INSTANCE = new FrostBackend(); + private static final int BAKED_STEPS = 8; + private final Set sections = ConcurrentHashMap.newKeySet(); + private volatile float bakedProgress; + private volatile boolean shaderMode; + + public void levelChanged() { + this.sections.clear(); + this.bakedProgress = quantize(FrostWeather.INSTANCE.progress()); + this.shaderMode = shaderAvailable(); + } + + public void tick(ClientLevel level) { + boolean shader = shaderAvailable(); + float baked = quantize(FrostWeather.INSTANCE.progress()); + if (shader != this.shaderMode || (!shader && baked != this.bakedProgress)) { + this.shaderMode = shader; + this.bakedProgress = baked; + this.rebuild(level); + } + } + + public boolean useShader() { + return this.shaderMode; + } + + public int meshColor(int tint, float weight, BlockPos pos) { + this.sections.add(SectionPos.asLong(pos)); + return this.shaderMode ? FrostColor.encode(tint, weight) : FrostColor.apply(tint, weight * this.bakedProgress); + } + + private static boolean shaderAvailable() { + return FrostShader.ready() && !IrisCompat.shaderPackInUse(); + } + + private static float quantize(float value) { + return Math.round(value * BAKED_STEPS) / (float) BAKED_STEPS; + } + + private void rebuild(ClientLevel level) { + Minecraft minecraft = Minecraft.getInstance(); + if (minecraft.player == null || this.sections.isEmpty()) { + return; + } + + List sections = new ArrayList<>(this.sections); + this.sections.clear(); + SectionPos centre = SectionPos.of(minecraft.player.blockPosition()); + int distance = minecraft.options.getEffectiveRenderDistance() + 1; + for (long section : sections) { + int x = SectionPos.x(section); + int y = SectionPos.y(section); + int z = SectionPos.z(section); + if (Math.abs(x - centre.x()) <= distance && Math.abs(z - centre.z()) <= distance && y >= level.getMinSectionY() && y <= level.getMaxSectionY()) { + minecraft.levelExtractor.setSectionDirty(x, y, z); + } + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/FrostColor.java b/snow/src/main/java/dev/imb11/snow/client/frost/FrostColor.java new file mode 100644 index 0000000..dc8d118 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/FrostColor.java @@ -0,0 +1,25 @@ +package dev.imb11.snow.client.frost; + +import net.minecraft.util.ARGB; +import net.minecraft.util.Mth; + +public final class FrostColor { + public static int apply(int color, float amount) { + if (amount <= 0.0F) { + return color; + } + + return ARGB.color(ARGB.alpha(color), whiten(ARGB.red(color), amount), whiten(ARGB.green(color), amount), whiten(ARGB.blue(color), amount)); + } + + public static int encode(int color, float weight) { + int brightest = Math.max(ARGB.red(color), Math.max(ARGB.green(color), ARGB.blue(color))); + int weightBits = Mth.clamp(Math.round(weight * 15.0F), 1, 15); + int tintBits = Mth.clamp(Math.round(brightest / 16.0F) - 1, 0, 14); + return ARGB.color(weightBits << 4 | tintBits, ARGB.red(color), ARGB.green(color), ARGB.blue(color)); + } + + private static int whiten(int channel, float amount) { + return Mth.clamp(Math.round(channel + (255 - channel) * amount), 0, 255); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/FrostShader.java b/snow/src/main/java/dev/imb11/snow/client/frost/FrostShader.java new file mode 100644 index 0000000..f3b6a72 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/FrostShader.java @@ -0,0 +1,63 @@ +package dev.imb11.snow.client.frost; + +import cc.cassian.mru.Platform; +import net.minecraft.resources.Identifier; + +public final class FrostShader { + private static final boolean SODIUM = Platform.INSTANCE.isLoaded("sodium"); + //? if >=26.3 { + /*private static final String IMPORT = "#include "; + *///?} else { + private static final String IMPORT = "#moj_import "; + //?} + private static final Identifier VANILLA_TERRAIN = Identifier.withDefaultNamespace("core/terrain"); + private static final Identifier SODIUM_TERRAIN = Identifier.fromNamespaceAndPath("sodium", "blocks/block_layer_opaque"); + private static final String VANILLA_GLOBALS_ANCHOR = "int UseRgss;"; + private static final String SODIUM_GLOBALS_ANCHOR = "bool u_UseRGSS;"; + private static volatile boolean vanillaGlobals; + private static volatile boolean vanillaVertex; + private static volatile boolean sodiumGlobals; + private static volatile boolean sodiumVertex; + + public static boolean ready() { + return SODIUM ? sodiumGlobals && sodiumVertex : vanillaGlobals && vanillaVertex; + } + + public static String patchVertex(Identifier id, boolean vertex, String source) { + if (!vertex) { + return source; + } + + if (id.equals(VANILLA_TERRAIN)) { + String anchor = "vertexColor = Color * sample_lightmap(Sampler2, UV2);"; + vanillaVertex = source.contains(anchor); + return patch(source, "", anchor, "vertexColor = snow_applyFrost(Color, SnowFrost) * sample_lightmap(Sampler2, UV2);"); + } + + if (id.equals(SODIUM_TERRAIN)) { + String anchor = "_vert_init();"; + sodiumVertex = source.contains(anchor); + return patch(source, "", anchor, anchor + "\n _vert_color = snow_applyFrost(_vert_color, u_SnowFrost);"); + } + + return source; + } + + public static String patchGlobals(String source) { + if (source.contains(VANILLA_GLOBALS_ANCHOR)) { + vanillaGlobals = true; + return source.replace(VANILLA_GLOBALS_ANCHOR, VANILLA_GLOBALS_ANCHOR + "\n float SnowFrost;"); + } + + if (source.contains(SODIUM_GLOBALS_ANCHOR)) { + sodiumGlobals = true; + return source.replace(SODIUM_GLOBALS_ANCHOR, SODIUM_GLOBALS_ANCHOR + "\n float u_SnowFrost;"); + } + + return source; + } + + private static String patch(String source, String globals, String anchor, String replacement) { + return source.replace(IMPORT + globals, IMPORT + globals + "\n" + IMPORT + "").replace(anchor, replacement); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/FrostWeather.java b/snow/src/main/java/dev/imb11/snow/client/frost/FrostWeather.java new file mode 100644 index 0000000..18059f8 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/FrostWeather.java @@ -0,0 +1,37 @@ +package dev.imb11.snow.client.frost; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.util.Mth; +import org.jspecify.annotations.Nullable; + +public final class FrostWeather { + public static final FrostWeather INSTANCE = new FrostWeather(); + private static final int DEFAULT_SEA_LEVEL = 63; + private volatile int seaLevel = DEFAULT_SEA_LEVEL; + private volatile float previousProgress; + private volatile float progress; + + public void levelChanged(@Nullable ClientLevel level) { + this.seaLevel = level == null ? DEFAULT_SEA_LEVEL : level.getSeaLevel(); + this.progress = level == null ? 0.0F : level.getRainLevel(1.0F); + this.previousProgress = this.progress; + } + + public void tick(ClientLevel level) { + this.previousProgress = this.progress; + this.progress = Mth.approach(this.progress, level.getRainLevel(1.0F), 1.0F / 400); + } + + public int seaLevel() { + return this.seaLevel; + } + + public float progress() { + return this.progress; + } + + public float interpolatedProgress() { + return Mth.lerp(Minecraft.getInstance().getDeltaTracker().getGameTimeDeltaPartialTick(false), this.previousProgress, this.progress); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/FrostedLeafTintSource.java b/snow/src/main/java/dev/imb11/snow/client/frost/FrostedLeafTintSource.java new file mode 100644 index 0000000..6cb8183 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/FrostedLeafTintSource.java @@ -0,0 +1,42 @@ +package dev.imb11.snow.client.frost; + +import net.minecraft.client.color.block.BlockTintSource; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.Property; + +import java.util.Set; + +public record FrostedLeafTintSource(BlockTintSource delegate) implements BlockTintSource { + @Override + public int color(BlockState state) { + return this.delegate.color(state); + } + + @Override + public int colorInWorld(BlockState state, BlockAndTintGetter level, BlockPos pos) { + int color = this.delegate.colorInWorld(state, level, pos); + float weight = LeafFrost.weight(level, state, pos); + if (weight <= 0.0F) { + return color; + } + + return level instanceof ClientLevel ? immediate(color, weight) : FrostBackend.INSTANCE.meshColor(color, weight, pos); + } + + @Override + public int colorAsTerrainParticle(BlockState state, BlockAndTintGetter level, BlockPos pos) { + return immediate(this.delegate.colorAsTerrainParticle(state, level, pos), LeafFrost.weight(level, state, pos)); + } + + @Override + public Set> relevantProperties() { + return this.delegate.relevantProperties(); + } + + private static int immediate(int color, float weight) { + return FrostColor.apply(color, weight * FrostWeather.INSTANCE.interpolatedProgress()); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/LeafFrost.java b/snow/src/main/java/dev/imb11/snow/client/frost/LeafFrost.java new file mode 100644 index 0000000..728f04b --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/LeafFrost.java @@ -0,0 +1,20 @@ +package dev.imb11.snow.client.frost; + +import dev.imb11.snow.frost.FrostableLeaves; +import net.minecraft.client.renderer.block.BlockAndTintGetter; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.state.BlockState; + +public final class LeafFrost { + private static final float SNOW_TEMPERATURE = 0.15F; + + public static float weight(BlockAndTintGetter level, BlockState state, BlockPos pos) { + if (!FrostableLeaves.contains(state)) { + return 0.0F; + } + + float temperature = BiomeTemperatureResolver.temperatureAt(level, pos, FrostWeather.INSTANCE.seaLevel()); + return Mth.clamp((SNOW_TEMPERATURE - temperature) / (SNOW_TEMPERATURE - 0.05F), 0.0F, 1.0F); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/compat/IrisCompat.java b/snow/src/main/java/dev/imb11/snow/client/frost/compat/IrisCompat.java new file mode 100644 index 0000000..37a9446 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/compat/IrisCompat.java @@ -0,0 +1,17 @@ +package dev.imb11.snow.client.frost.compat; + +import cc.cassian.mru.Platform; + +public final class IrisCompat { + private static final boolean LOADED = Platform.INSTANCE.isLoaded("iris"); + + public static boolean shaderPackInUse() { + return LOADED && Access.shaderPackInUse(); + } + + private static final class Access { + private static boolean shaderPackInUse() { + return net.irisshaders.iris.api.v0.IrisApi.getInstance().isShaderPackInUse(); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/client/frost/compat/SodiumFrostColorProvider.java b/snow/src/main/java/dev/imb11/snow/client/frost/compat/SodiumFrostColorProvider.java new file mode 100644 index 0000000..723a8df --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/client/frost/compat/SodiumFrostColorProvider.java @@ -0,0 +1,24 @@ +package dev.imb11.snow.client.frost.compat; + +import dev.imb11.snow.client.frost.FrostBackend; +import dev.imb11.snow.client.frost.LeafFrost; +import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; +import net.caffeinemc.mods.sodium.client.model.quad.ModelQuadView; +import net.caffeinemc.mods.sodium.client.world.LevelSlice; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; + +public record SodiumFrostColorProvider(ColorProvider delegate) implements ColorProvider { + @Override + public void getColors(LevelSlice slice, BlockPos pos, BlockPos.MutableBlockPos scratch, BlockState state, ModelQuadView quad, int[] colors, boolean blend) { + this.delegate.getColors(slice, pos, scratch, state, quad, colors, blend); + float weight = LeafFrost.weight(slice, state, pos); + if (weight <= 0.0F) { + return; + } + + for (int i = 0; i < colors.length; i++) { + colors[i] = FrostBackend.INSTANCE.meshColor(colors[i], weight, pos); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/config/SnowConfig.java b/snow/src/main/java/dev/imb11/snow/config/SnowConfig.java new file mode 100644 index 0000000..df14c40 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/config/SnowConfig.java @@ -0,0 +1,100 @@ +package dev.imb11.snow.config; + +import cc.cassian.mru.Platform; +import com.google.gson.GsonBuilder; +import dev.imb11.mru.sync.SyncedConfig; +import dev.imb11.mru.sync.client.SyncClientNetworking; +import dev.imb11.mru.yacl.ConfigHelper; +import dev.imb11.snow.Snow; +import dev.isxander.yacl3.api.ConfigCategory; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.YetAnotherConfigLib; +import dev.isxander.yacl3.config.v2.api.ConfigClassHandler; +import dev.isxander.yacl3.config.v2.api.SerialEntry; +import dev.isxander.yacl3.config.v2.api.serializer.GsonConfigSerializerBuilder; +import net.minecraft.network.chat.Component; + +import java.util.List; + +public class SnowConfig { + public static final ConfigHelper HELPER = new ConfigHelper(Snow.MOD_ID, "config"); + private static final ConfigClassHandler HANDLER = ConfigClassHandler + .createBuilder(SnowConfig.class) + .id(Snow.id("config")) + .serializer(config -> GsonConfigSerializerBuilder + .create(config) + .setPath(Platform.INSTANCE.configPath().resolve(Snow.MOD_ID + ".json")) + .appendGsonBuilder(GsonBuilder::setPrettyPrinting) + .build()) + .build(); + public static final SyncedConfig SYNC = SyncedConfig + .register(Snow.id("config"), SnowConfig.class, HANDLER::instance, SnowConfig::applyFromOperator); + + @SerialEntry + public boolean accumulation = true; + @SerialEntry + public boolean generation = true; + @SerialEntry + public boolean gradualMelting = true; + @SerialEntry + public boolean meltInWarmBiomes = false; + @SerialEntry + public boolean shovelHarvesting = true; + @SerialEntry + public boolean snowlogging = true; + + public static SnowConfig get() { + return SYNC.get(); + } + + public static void load() { + HANDLER.load(); + } + + private static void applyFromOperator(SnowConfig received) { + SnowConfig config = HANDLER.instance(); + config.accumulation = received.accumulation; + config.generation = received.generation; + config.gradualMelting = received.gradualMelting; + config.meltInWarmBiomes = received.meltInWarmBiomes; + config.shovelHarvesting = received.shovelHarvesting; + config.snowlogging = received.snowlogging; + HANDLER.save(); + } + + public static YetAnotherConfigLib getYACL() { + SnowConfig config = SYNC.get(); + SnowConfig defaults = HANDLER.defaults(); + boolean remote = SYNC.isRemote(); + boolean editable = SyncClientNetworking.canEdit(SYNC); + List> options = List.of( + HELPER.get("accumulation.enabled", defaults.accumulation, () -> config.accumulation, value -> config.accumulation = value), + HELPER.get("generation.enabled", defaults.generation, () -> config.generation, value -> config.generation = value), + HELPER.get("melting.gradual_melting", defaults.gradualMelting, () -> config.gradualMelting, value -> config.gradualMelting = value), + HELPER.get("melting.melt_in_warm_biomes", defaults.meltInWarmBiomes, () -> config.meltInWarmBiomes, value -> config.meltInWarmBiomes = value), + HELPER.get("harvesting.shovel_harvesting", defaults.shovelHarvesting, () -> config.shovelHarvesting, value -> config.shovelHarvesting = value), + HELPER.get("snowlogging.enabled", defaults.snowlogging, () -> config.snowlogging, value -> config.snowlogging = value) + ); + if (!editable) { + options.forEach(option -> option.setAvailable(false)); + } + + return YetAnotherConfigLib.createBuilder() + .title(Component.translatable("snow.config.title")) + .category(ConfigCategory.createBuilder() + .name(Component.translatable("snow.config.title")) + .options(options) + .build()) + .save(() -> { + if (remote) { + if (editable) { + SyncClientNetworking.requestUpdate(SYNC, config); + } + } else { + HANDLER.save(); + SYNC.broadcast(); + } + }) + .build(); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/datagen/SnowBlockTagProvider.java b/snow/src/main/java/dev/imb11/snow/datagen/SnowBlockTagProvider.java new file mode 100644 index 0000000..427a5f9 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/datagen/SnowBlockTagProvider.java @@ -0,0 +1,64 @@ +//? fabric { +package dev.imb11.snow.datagen; + +import dev.imb11.snow.frost.FrostableLeaves; +import dev.imb11.snow.snowlogging.Snowlogging; +import net.fabricmc.fabric.api.datagen.v1.FabricPackOutput; +import net.fabricmc.fabric.api.datagen.v1.provider.FabricTagsProvider; +import net.minecraft.core.HolderLookup; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public class SnowBlockTagProvider extends FabricTagsProvider.BlockTagsProvider { + private static final List SNOWLOGGABLE = List.of( + Blocks.SHORT_GRASS, + Blocks.FERN, + Blocks.SHORT_DRY_GRASS, + Blocks.TALL_DRY_GRASS, + Blocks.BUSH, + //? if >=26.3 { + /*Blocks.RED_SHRUB, + *///?} + Blocks.FIREFLY_BUSH, + Blocks.NETHER_SPROUTS, + Blocks.CRIMSON_ROOTS, + Blocks.WARPED_ROOTS, + Blocks.DANDELION, + Blocks.GOLDEN_DANDELION, + Blocks.POPPY, + Blocks.BLUE_ORCHID, + Blocks.ALLIUM, + Blocks.AZURE_BLUET, + Blocks.RED_TULIP, + Blocks.ORANGE_TULIP, + Blocks.WHITE_TULIP, + Blocks.PINK_TULIP, + Blocks.OXEYE_DAISY, + Blocks.CORNFLOWER, + Blocks.LILY_OF_THE_VALLEY, + Blocks.WITHER_ROSE, + Blocks.TORCHFLOWER, + Blocks.OPEN_EYEBLOSSOM, + Blocks.CLOSED_EYEBLOSSOM, + Blocks.BROWN_MUSHROOM, + Blocks.RED_MUSHROOM, + Blocks.CRIMSON_FUNGUS, + Blocks.WARPED_FUNGUS + ); + + public SnowBlockTagProvider(FabricPackOutput output, CompletableFuture registriesFuture) { + super(output, registriesFuture); + } + + @Override + protected void addTags(HolderLookup.Provider registries) { + this.builder(Snowlogging.SNOWLOGGABLE) + .addAll(SNOWLOGGABLE.stream().map(block -> block.builtInRegistryHolder().key())); + this.builder(FrostableLeaves.TAG) + .addAll(FrostableLeaves.DEFAULTS.stream().map(block -> block.builtInRegistryHolder().key())); + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/datagen/SnowDatagen.java b/snow/src/main/java/dev/imb11/snow/datagen/SnowDatagen.java new file mode 100644 index 0000000..8c24a9e --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/datagen/SnowDatagen.java @@ -0,0 +1,30 @@ +//? fabric { +package dev.imb11.snow.datagen; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.profile.SnowProfiles; +import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; +import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator; +import net.minecraft.core.RegistrySetBuilder; +import org.jetbrains.annotations.Nullable; + +public class SnowDatagen implements DataGeneratorEntrypoint { + @Override + public void onInitializeDataGenerator(FabricDataGenerator fabricDataGenerator) { + var pack = fabricDataGenerator.createPack(); + pack.addProvider(SnowProfileProvider::new); + pack.addProvider(SnowBlockTagProvider::new); + pack.addProvider(SnowTestStructureProvider::new); + } + + @Override + public void buildRegistry(RegistrySetBuilder registryBuilder) { + registryBuilder.add(SnowProfiles.KEY, SnowProfileProvider::bootstrap); + } + + @Override + public @Nullable String getEffectiveModId() { + return Snow.MOD_ID; + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/datagen/SnowProfileProvider.java b/snow/src/main/java/dev/imb11/snow/datagen/SnowProfileProvider.java new file mode 100644 index 0000000..4e3ada1 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/datagen/SnowProfileProvider.java @@ -0,0 +1,51 @@ +//? fabric { +package dev.imb11.snow.datagen; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import net.fabricmc.fabric.api.datagen.v1.FabricPackOutput; +import net.fabricmc.fabric.api.datagen.v1.provider.FabricDynamicRegistryProvider; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.BootstrapContext; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; + +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; + +public class SnowProfileProvider extends FabricDynamicRegistryProvider { + public SnowProfileProvider(FabricPackOutput output, CompletableFuture registriesFuture) { + super(output, registriesFuture); + } + + public static void bootstrap(BootstrapContext context) { + HolderGetter biomes = context.lookup(Registries.BIOME); + register(context, biomes, "ice_spikes", 4, 12, 3f, Biomes.ICE_SPIKES); + register(context, biomes, "snowy_plains", 2, 8, 2f, Biomes.SNOWY_PLAINS); + register(context, biomes, "snowy_taiga", 1, 4, 1.5f, Biomes.SNOWY_TAIGA, Biomes.SNOWY_SLOPES); + register(context, biomes, "frozen", 1, 2, 1f, Biomes.SNOWY_BEACH, Biomes.GROVE, Biomes.FROZEN_PEAKS, Biomes.JAGGED_PEAKS, Biomes.FROZEN_RIVER, Biomes.FROZEN_OCEAN, Biomes.DEEP_FROZEN_OCEAN); + register(context, biomes, "cold", 1, 2, 1f, Biomes.WINDSWEPT_HILLS, Biomes.STONY_SHORE, Biomes.TAIGA); + } + + @SafeVarargs + private static void register(BootstrapContext context, HolderGetter biomes, String name, int minLayers, int maxLayers, float accumulationRate, ResourceKey... keys) { + HolderSet holders = HolderSet.direct(Arrays.stream(keys).map(biomes::getOrThrow).toList()); + context.register(ResourceKey.create(SnowProfiles.KEY, Snow.id(name)), new SnowProfile(holders, minLayers, maxLayers, accumulationRate, SnowProfile.DEFAULT_DRIFT_STRENGTH, 0)); + } + + @Override + protected void configure(HolderLookup.Provider registries, Entries entries) { + entries.addAll(registries.lookupOrThrow(SnowProfiles.KEY)); + } + + @Override + public String getName() { + return "Snow Profiles"; + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/datagen/SnowTestStructureProvider.java b/snow/src/main/java/dev/imb11/snow/datagen/SnowTestStructureProvider.java new file mode 100644 index 0000000..3d647b0 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/datagen/SnowTestStructureProvider.java @@ -0,0 +1,81 @@ +//? fabric { +package dev.imb11.snow.datagen; + +import com.google.common.hash.Hashing; +import dev.imb11.snow.gametest.support.SnowTestHelper; +import net.fabricmc.fabric.api.datagen.v1.FabricPackOutput; +import net.minecraft.data.CachedOutput; +import net.minecraft.data.DataProvider; +import net.minecraft.data.PackOutput; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.IntTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtIo; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.block.Blocks; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.concurrent.CompletableFuture; + +public class SnowTestStructureProvider implements DataProvider { + private final PackOutput.PathProvider paths; + + public SnowTestStructureProvider(FabricPackOutput output) { + this.paths = output.createPathProvider(PackOutput.Target.DATA_PACK, "structure"); + } + + @Override + public CompletableFuture run(CachedOutput output) { + try { + write(output, SnowTestHelper.PLATFORM, 9, 5, 9); + write(output, SnowTestHelper.CHUNK_PLATFORM, 16, 3, 16); + return CompletableFuture.completedFuture(null); + } catch (IOException e) { + return CompletableFuture.failedFuture(e); + } + } + + private void write(CachedOutput output, Identifier id, int width, int height, int depth) throws IOException { + ListTag palette = new ListTag(); + palette.add(NbtUtils.writeBlockState(Blocks.STONE.defaultBlockState())); + + ListTag blocks = new ListTag(); + for (int x = 0; x < width; x++) { + for (int z = 0; z < depth; z++) { + CompoundTag block = new CompoundTag(); + block.put("pos", ints(x, 0, z)); + block.putInt("state", 0); + blocks.add(block); + } + } + + CompoundTag structure = new CompoundTag(); + structure.put("size", ints(width, height, depth)); + structure.put("palette", palette); + structure.put("blocks", blocks); + structure.put("entities", new ListTag()); + NbtUtils.addCurrentDataVersion(structure); + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + NbtIo.writeCompressed(structure, bytes); + byte[] data = bytes.toByteArray(); + output.writeIfNeeded(this.paths.file(id, "nbt"), data, Hashing.sha1().hashBytes(data)); + } + + private static ListTag ints(int... values) { + ListTag list = new ListTag(); + for (int value : values) { + list.add(IntTag.valueOf(value)); + } + + return list; + } + + @Override + public String getName() { + return "Snow Test Structures"; + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/fabric/ModMenuCompat.java b/snow/src/main/java/dev/imb11/snow/fabric/ModMenuCompat.java new file mode 100644 index 0000000..b851e58 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/fabric/ModMenuCompat.java @@ -0,0 +1,14 @@ +//? fabric { +package dev.imb11.snow.fabric; + +import com.terraformersmc.modmenu.api.ConfigScreenFactory; +import com.terraformersmc.modmenu.api.ModMenuApi; +import dev.imb11.snow.config.SnowConfig; + +public class ModMenuCompat implements ModMenuApi { + @Override + public ConfigScreenFactory getModConfigScreenFactory() { + return parent -> SnowConfig.getYACL().generateScreen(parent); + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/fabric/SnowFabric.java b/snow/src/main/java/dev/imb11/snow/fabric/SnowFabric.java new file mode 100644 index 0000000..1465a93 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/fabric/SnowFabric.java @@ -0,0 +1,23 @@ +//? fabric { +package dev.imb11.snow.fabric; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import dev.imb11.snow.gametest.SnowGameTests; +import dev.imb11.snow.snowlogging.SnowloggingAttachment; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.event.registry.DynamicRegistries; + +public class SnowFabric implements ModInitializer { + @Override + public void onInitialize() { + DynamicRegistries.register(SnowProfiles.KEY, SnowProfile.CODEC); + Snow.init(); + SnowloggingAttachment.init(); + if (SnowGameTests.enabled()) { + SnowFabricGameTests.register(); + } + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/fabric/SnowFabricClient.java b/snow/src/main/java/dev/imb11/snow/fabric/SnowFabricClient.java new file mode 100644 index 0000000..e062757 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/fabric/SnowFabricClient.java @@ -0,0 +1,28 @@ +//? fabric { +package dev.imb11.snow.fabric; + +import dev.imb11.snow.client.SnowClient; +import dev.imb11.snow.client.SnowloggedBlockStateModel; +import dev.imb11.snow.client.SnowloggingClient; +import dev.imb11.snow.client.frost.BiomeTemperatureResolver; +import dev.imb11.snow.snowlogging.SnowloggingAttachment; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents; +import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin; +import net.fabricmc.fabric.api.client.rendering.v1.ColorResolverRegistry; +import net.minecraft.world.level.block.Blocks; + +public class SnowFabricClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + SnowClient.init(); + ColorResolverRegistry.register(BiomeTemperatureResolver.RESOLVER); + ModelLoadingPlugin.register(context -> context.modifyBlockModelAfterBake().register((model, modelContext) -> modelContext.state().is(Blocks.SNOW) ? new SnowloggedBlockStateModel(model) : model)); + + ClientChunkEvents.CHUNK_LOAD.register((level, chunk) -> { + SnowloggingClient.INSTANCE.plantsChanged(level, null, chunk.getAttached(SnowloggingAttachment.TYPE)); + chunk.onAttachedSet(SnowloggingAttachment.TYPE).register((previous, current) -> SnowloggingClient.INSTANCE.plantsChanged(level, previous, current)); + }); + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/fabric/SnowFabricGameTests.java b/snow/src/main/java/dev/imb11/snow/fabric/SnowFabricGameTests.java new file mode 100644 index 0000000..e73e2b2 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/fabric/SnowFabricGameTests.java @@ -0,0 +1,24 @@ +//? fabric { +package dev.imb11.snow.fabric; + +import dev.imb11.snow.gametest.SnowGameTests; +import dev.imb11.snow.gametest.support.SnowTestEnvironments; +import net.fabricmc.fabric.api.event.registry.DynamicRegistrySetupCallback; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTestInstance; +import net.minecraft.gametest.framework.TestEnvironmentDefinition; + +public class SnowFabricGameTests { + public static void register() { + Registry.register(BuiltInRegistries.TEST_ENVIRONMENT_DEFINITION_TYPE, SnowTestEnvironments.CONFIG_TYPE, SnowTestEnvironments.ConfigEnvironment.CODEC); + SnowGameTests.all().forEach(test -> Registry.register(BuiltInRegistries.TEST_FUNCTION, test.id(), test.function())); + + DynamicRegistrySetupCallback.EVENT.register(view -> view.getOptional(Registries.TEST_INSTANCE).ifPresent(instances -> { + Registry> environments = view.>getOptional(Registries.TEST_ENVIRONMENT).orElseThrow(); + SnowGameTests.registerInstances((key, environment) -> Registry.registerForHolder(environments, key, environment), (id, instance) -> Registry.register(instances, id, instance)); + })); + } +} +//?} diff --git a/snow/src/main/java/dev/imb11/snow/frost/FrostableLeaves.java b/snow/src/main/java/dev/imb11/snow/frost/FrostableLeaves.java new file mode 100644 index 0000000..68c0b1b --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/frost/FrostableLeaves.java @@ -0,0 +1,29 @@ +package dev.imb11.snow.frost; + +import dev.imb11.snow.Snow; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.List; +import java.util.Set; + +public final class FrostableLeaves { + public static final TagKey TAG = TagKey.create(Registries.BLOCK, Snow.id("frostable_leaves")); + public static final List DEFAULTS = List.of( + Blocks.OAK_LEAVES, + Blocks.SPRUCE_LEAVES, + Blocks.BIRCH_LEAVES, + Blocks.JUNGLE_LEAVES, + Blocks.ACACIA_LEAVES, + Blocks.MANGROVE_LEAVES + ); + private static final Set DEFAULT_SET = Set.copyOf(DEFAULTS); + + public static boolean contains(BlockState state) { + return BuiltInRegistries.BLOCK.get(TAG).isPresent() ? state.is(TAG) : DEFAULT_SET.contains(state.getBlock()); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/AccumulationTests.java b/snow/src/main/java/dev/imb11/snow/gametest/AccumulationTests.java new file mode 100644 index 0000000..8dc3b8a --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/AccumulationTests.java @@ -0,0 +1,122 @@ +package dev.imb11.snow.gametest; + +import dev.imb11.snow.accumulation.SnowAccumulation; +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import dev.imb11.snow.gametest.SnowGameTests.TestDefinition; +import dev.imb11.snow.gametest.support.SnowTestHelper; +import dev.imb11.snow.util.SnowColumn; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityTypes; +import net.minecraft.world.entity.animal.pig.Pig; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; + +import java.util.List; +import java.util.stream.Stream; + +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.SNOWFALL; +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.SNOWFALL_CAPPED; +import static dev.imb11.snow.gametest.support.SnowTestHelper.PLATFORM; +import static dev.imb11.snow.gametest.support.SnowTestHelper.SURFACE; + +public class AccumulationTests { + private static final int FILL_ATTEMPTS = 1024; + private static final double TOLERANCE = 1.0E-5; + + public static List all() { + return List.of( + new TestDefinition("accumulation/profiles", SNOWFALL, PLATFORM, 20, AccumulationTests::profiles), + new TestDefinition("accumulation/snowfall_hook", SNOWFALL, PLATFORM, 100, AccumulationTests::snowfallHook), + new TestDefinition("accumulation/vanilla_fallback", SNOWFALL, PLATFORM, 20, AccumulationTests::vanillaFallback), + new TestDefinition("accumulation/gamerule_cap", SNOWFALL_CAPPED, PLATFORM, 20, AccumulationTests::gameruleCap), + new TestDefinition("accumulation/pushes_entities", SNOWFALL, PLATFORM, 20, AccumulationTests::pushesEntities), + new TestDefinition("accumulation/priority", SNOWFALL, PLATFORM, 20, AccumulationTests::priority) + ); + } + + private static void profiles(GameTestHelper helper) { + for (Holder biome : SnowTestHelper.profiledBiomes(helper)) { + SnowProfile profile = SnowTestHelper.setBiome(helper, biome); + String name = biome.getRegisteredName(); + SnowTestHelper.clear(helper, SURFACE); + int target = target(helper); + helper.assertValueInBetween(profile.minLayers(), target, SnowColumn.MAX_LAYERS, name + " accumulation target"); + + SnowAccumulation.accumulate(helper.getLevel(), helper.absolutePos(SURFACE)); + int highest = Math.min(target, Mth.ceil(profile.accumulationRate())); + helper.assertValueInBetween(1, SnowTestHelper.layers(helper, SURFACE), highest, name + " layers after one snowfall tick"); + + fill(helper); + int expectedMax = profile.accumulationRate() > 0 ? target : 0; + helper.assertValueEqual(SnowTestHelper.layers(helper, SURFACE), expectedMax, name + " maximum layers"); + } + + helper.succeed(); + } + + private static void snowfallHook(GameTestHelper helper) { + SnowProfile profile = SnowTestHelper.setBiome(helper, SnowTestHelper.biome(helper, Biomes.ICE_SPIKES)); + helper.assertTrue(profile.minLayers() > 1, "Ice Spikes should accumulate more than one layer"); + int target = target(helper); + helper.startSequence() + .thenWaitUntil(() -> helper.assertTrue(helper.getLevel().isRaining(), "Rain should be falling")) + .thenExecute(() -> { + for (int i = 0; i < FILL_ATTEMPTS; i++) { + helper.tickPrecipitation(SURFACE); + } + + SnowTestHelper.assertLayers(helper, SURFACE, target); + }) + .thenSucceed(); + } + + private static void vanillaFallback(GameTestHelper helper) { + helper.setBiome(Biomes.PLAINS); + fill(helper); + SnowTestHelper.assertLayers(helper, SURFACE, 1); + helper.succeed(); + } + + private static void gameruleCap(GameTestHelper helper) { + SnowProfile profile = SnowTestHelper.setBiome(helper, SnowTestHelper.biome(helper, Biomes.ICE_SPIKES)); + int target = target(helper); + helper.assertValueInBetween(profile.minLayers(), target, 5, "capped accumulation target"); + fill(helper); + SnowTestHelper.assertLayers(helper, SURFACE, target); + helper.succeed(); + } + + private static void pushesEntities(GameTestHelper helper) { + SnowTestHelper.setBiome(helper, SnowTestHelper.biome(helper, Biomes.ICE_SPIKES)); + Pig pig = helper.spawnWithNoFreeWill(EntityTypes.PIG, SURFACE); + fill(helper); + double surface = SnowTestHelper.surfaceY(helper, SURFACE); + helper.assertValueInBetween(surface - TOLERANCE, pig.getY(), surface + TOLERANCE, "pig height"); + helper.succeed(); + } + + private static void priority(GameTestHelper helper) { + Holder plains = SnowTestHelper.biome(helper, Biomes.PLAINS); + SnowProfile low = new SnowProfile(HolderSet.direct(plains), 1, 2, 1f, SnowProfile.DEFAULT_DRIFT_STRENGTH, 0); + SnowProfile high = new SnowProfile(HolderSet.direct(plains), 1, 6, 1f, SnowProfile.DEFAULT_DRIFT_STRENGTH, 5); + SnowProfile resolved = SnowProfiles.resolve(Stream.of(low, high), plains).orElseThrow(); + helper.assertValueEqual(resolved, high, "resolved profile"); + helper.succeed(); + } + + private static int target(GameTestHelper helper) { + return SnowAccumulation.target(helper.getLevel(), helper.absolutePos(SURFACE)); + } + + private static void fill(GameTestHelper helper) { + BlockPos pos = helper.absolutePos(SURFACE); + for (int i = 0; i < FILL_ATTEMPTS; i++) { + SnowAccumulation.accumulate(helper.getLevel(), pos); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/GenerationTests.java b/snow/src/main/java/dev/imb11/snow/gametest/GenerationTests.java new file mode 100644 index 0000000..bd58715 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/GenerationTests.java @@ -0,0 +1,126 @@ +package dev.imb11.snow.gametest; + +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.gametest.SnowGameTests.TestDefinition; +import dev.imb11.snow.gametest.support.SnowTestHelper; +import dev.imb11.snow.generation.SnowDrift; +import dev.imb11.snow.generation.SnowGeneration; +import dev.imb11.snow.util.SnowColumn; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowLayerBlock; +//? if >=26.3 { +/*import net.minecraft.world.level.levelgen.feature.SnowAndFreezeFeature; +*///?} else { +import net.minecraft.world.level.levelgen.feature.Feature; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; +//?} + +import java.util.List; + +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.SNOWFALL; +import static dev.imb11.snow.gametest.support.SnowTestHelper.CHUNK_PLATFORM; +import static dev.imb11.snow.gametest.support.SnowTestHelper.PLATFORM; +import static dev.imb11.snow.gametest.support.SnowTestHelper.SURFACE; + +public class GenerationTests { + private static final int CENTRE_MIN = 4; + private static final int CENTRE_MAX = 11; + private static final int PLATFORM_SIZE = 9; + private static final int DRIFT_BASE_LAYERS = 2; + private static final int STEP_X = 5; + private static final BlockPos FLAT = new BlockPos(2, 1, 4); + private static final BlockPos BELOW_STEP = new BlockPos(STEP_X - 1, 1, 4); + private static final BlockPos CANOPY = new BlockPos(1, 3, 1); + + public static List all() { + return List.of( + new TestDefinition("generation/profiles", SNOWFALL, PLATFORM, 20, GenerationTests::profiles), + new TestDefinition("generation/vanilla_fallback", SNOWFALL, PLATFORM, 20, GenerationTests::vanillaFallback), + new TestDefinition("generation/feature_hook", SNOWFALL, CHUNK_PLATFORM, 20, GenerationTests::featureHook), + new TestDefinition("generation/drift", SNOWFALL, PLATFORM, 20, GenerationTests::drift), + new TestDefinition("generation/blocked_overflow", SNOWFALL, PLATFORM, 20, GenerationTests::blockedOverflow) + ); + } + + private static void profiles(GameTestHelper helper) { + ServerLevel level = helper.getLevel(); + BlockPos pos = helper.absolutePos(SURFACE); + for (Holder biome : SnowTestHelper.profiledBiomes(helper)) { + SnowProfile profile = SnowTestHelper.setBiome(helper, biome); + String name = biome.getRegisteredName(); + for (int i = 0; i < 32; i++) { + SnowTestHelper.clear(helper, SURFACE); + helper.assertTrue(SnowGeneration.place(level, pos, Block.UPDATE_ALL), name + " should generate snow from its profile"); + helper.assertValueInBetween(profile.minLayers(), SnowTestHelper.layers(helper, SURFACE), profile.maxLayers(), name + " generated layers"); + } + } + + helper.succeed(); + } + + private static void vanillaFallback(GameTestHelper helper) { + helper.setBiome(Biomes.PLAINS); + ServerLevel level = helper.getLevel(); + helper.assertFalse(SnowGeneration.place(level, helper.absolutePos(SURFACE), Block.UPDATE_ALL), "Plains has no profile, so vanilla generation should run"); + SnowTestHelper.assertLayers(helper, SURFACE, 0); + helper.succeed(); + } + + private static void featureHook(GameTestHelper helper) { + SnowProfile profile = SnowTestHelper.setBiome(helper, SnowTestHelper.biome(helper, Biomes.ICE_SPIKES)); + ServerLevel level = helper.getLevel(); + BlockPos origin = helper.absolutePos(BlockPos.ZERO); + //? if >=26.3 { + /*new SnowAndFreezeFeature().place(level, level.getChunkSource().getGenerator(), level.getRandom(), origin); + *///?} else { + Feature.FREEZE_TOP_LAYER.place(NoneFeatureConfiguration.INSTANCE, level, level.getChunkSource().getGenerator(), level.getRandom(), origin); + //?} + + for (int x = CENTRE_MIN; x <= CENTRE_MAX; x++) { + for (int z = CENTRE_MIN; z <= CENTRE_MAX; z++) { + helper.assertValueInBetween(profile.minLayers(), SnowTestHelper.layers(helper, new BlockPos(x, 1, z)), profile.maxLayers(), "generated layers at " + x + ", " + z); + } + } + + helper.succeed(); + } + + private static void drift(GameTestHelper helper) { + SnowProfile profile = new SnowProfile(HolderSet.empty(), DRIFT_BASE_LAYERS, DRIFT_BASE_LAYERS, 1f, 4f, 0); + ServerLevel level = helper.getLevel(); + for (int x = STEP_X; x < PLATFORM_SIZE; x++) { + for (int z = 0; z < PLATFORM_SIZE; z++) { + helper.setBlock(new BlockPos(x, 1, z), Blocks.STONE); + } + } + + helper.setBlock(CANOPY.below(), Blocks.OAK_LEAVES); + + helper.assertValueEqual(SnowDrift.sample(level, helper.absolutePos(FLAT)), 0f, "drift on flat ground"); + helper.assertTrue(SnowDrift.sample(level, helper.absolutePos(BELOW_STEP)) > 0f, "Snow should drift against the foot of a step"); + helper.assertTrue(SnowDrift.sample(level, helper.absolutePos(new BlockPos(STEP_X, 2, 4))) < 0f, "Snow should thin on the edge of a step"); + helper.assertValueEqual(SnowDrift.sample(level, helper.absolutePos(CANOPY)), 0f, "drift on leaves"); + + int flat = SnowGeneration.layers(level, helper.absolutePos(FLAT), profile); + int below = SnowGeneration.layers(level, helper.absolutePos(BELOW_STEP), profile); + helper.assertTrue(below > flat, "The foot of a step should generate deeper snow than flat ground"); + helper.assertValueEqual(SnowGeneration.layers(level, helper.absolutePos(FLAT), profile), flat, "repeated generation layers"); + helper.succeed(); + } + + private static void blockedOverflow(GameTestHelper helper) { + helper.setBlock(SURFACE.above(), Blocks.OAK_LEAVES); + SnowTestHelper.place(helper, SURFACE, SnowColumn.MAX_LAYERS); + SnowTestHelper.assertLayers(helper, SURFACE, SnowLayerBlock.MAX_HEIGHT); + helper.assertBlockPresent(Blocks.OAK_LEAVES, SURFACE.above()); + helper.succeed(); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/HarvestingTests.java b/snow/src/main/java/dev/imb11/snow/gametest/HarvestingTests.java new file mode 100644 index 0000000..b17034e --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/HarvestingTests.java @@ -0,0 +1,83 @@ +package dev.imb11.snow.gametest; + +import dev.imb11.snow.gametest.SnowGameTests.TestDefinition; +import dev.imb11.snow.gametest.support.SnowTestHelper; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.InteractionHand; +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.item.context.UseOnContext; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; + +import java.util.List; +import java.util.function.Consumer; + +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.HARVESTING_OFF; +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.MELTING; +import static dev.imb11.snow.gametest.support.SnowTestHelper.PLATFORM; +import static dev.imb11.snow.gametest.support.SnowTestHelper.SURFACE; + +public class HarvestingTests { + public static List all() { + return List.of( + new TestDefinition("harvesting/removes_layers", MELTING, PLATFORM, 20, HarvestingTests::removesLayers), + new TestDefinition("harvesting/blocked_above", MELTING, PLATFORM, 20, HarvestingTests::blockedAbove), + new TestDefinition("harvesting/stacked_column", MELTING, PLATFORM, 20, HarvestingTests::stackedColumn), + new TestDefinition("harvesting/requires_shovel", MELTING, PLATFORM, 20, keepsLayers(Items.STICK)), + new TestDefinition("harvesting/disabled", HARVESTING_OFF, PLATFORM, 20, keepsLayers(Items.IRON_SHOVEL)) + ); + } + + private static void removesLayers(GameTestHelper helper) { + SnowTestHelper.place(helper, SURFACE, 4); + ItemStack shovel = use(helper, SURFACE, Items.IRON_SHOVEL); + helper.assertBlockPresent(Blocks.AIR, SURFACE); + helper.assertItemEntityPresent(Items.SNOWBALL, SURFACE, 2); + helper.assertValueEqual(shovel.getDamageValue(), 1, "shovel damage"); + helper.succeed(); + } + + private static void blockedAbove(GameTestHelper helper) { + SnowTestHelper.place(helper, SURFACE, 4); + helper.setBlock(SURFACE.above(), Blocks.STONE); + use(helper, SURFACE, Items.IRON_SHOVEL); + SnowTestHelper.assertLayers(helper, SURFACE, 4); + helper.assertItemEntityNotPresent(Items.SNOWBALL, SURFACE, 2); + helper.succeed(); + } + + private static void stackedColumn(GameTestHelper helper) { + SnowTestHelper.place(helper, SURFACE, 12); + use(helper, SURFACE, Items.IRON_SHOVEL); + SnowTestHelper.assertLayers(helper, SURFACE, 12); + use(helper, SURFACE.above(), Items.IRON_SHOVEL); + SnowTestHelper.assertLayers(helper, SURFACE, 8); + helper.succeed(); + } + + private static Consumer keepsLayers(Item item) { + return helper -> { + SnowTestHelper.place(helper, SURFACE, 4); + use(helper, SURFACE, item); + SnowTestHelper.assertLayers(helper, SURFACE, 4); + helper.succeed(); + }; + } + + private static ItemStack use(GameTestHelper helper, BlockPos pos, Item item) { + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack stack = new ItemStack(item); + player.setItemInHand(InteractionHand.MAIN_HAND, stack); + BlockPos absolute = helper.absolutePos(pos); + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(absolute), Direction.UP, absolute, false); + stack.useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, hit)); + return stack; + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/MeltingTests.java b/snow/src/main/java/dev/imb11/snow/gametest/MeltingTests.java new file mode 100644 index 0000000..f261631 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/MeltingTests.java @@ -0,0 +1,66 @@ +package dev.imb11.snow.gametest; + +import dev.imb11.snow.gametest.SnowGameTests.TestDefinition; +import dev.imb11.snow.gametest.support.SnowTestHelper; +import dev.imb11.snow.melting.SnowMelting; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.biome.Biomes; +import net.minecraft.world.level.block.Blocks; + +import java.util.List; +import java.util.function.Consumer; + +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.MELTING; +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.WARM_MELTING; +import static dev.imb11.snow.gametest.support.SnowTestHelper.PLATFORM; +import static dev.imb11.snow.gametest.support.SnowTestHelper.SURFACE; + +public class MeltingTests { + public static List all() { + return List.of( + new TestDefinition("melting/gradual", MELTING, PLATFORM, 100, MeltingTests::gradual), + new TestDefinition("melting/top_down", WARM_MELTING, PLATFORM, 20, MeltingTests::topDown), + new TestDefinition("melting/warm_biome_off", MELTING, PLATFORM, 20, warmBiome(3)), + new TestDefinition("melting/warm_biome_on", WARM_MELTING, PLATFORM, 20, warmBiome(2)) + ); + } + + private static void gradual(GameTestHelper helper) { + helper.setBiome(Biomes.ICE_SPIKES); + SnowTestHelper.place(helper, SURFACE, 5); + helper.setBlock(SURFACE.east(), Blocks.GLOWSTONE); + helper.startSequence() + .thenWaitUntil(() -> { + for (int i = 0; i < 64; i++) { + helper.getLevel().getChunkSource().pollTask(); + } + helper.assertTrue(helper.getLevel().getBrightness(LightLayer.BLOCK, helper.absolutePos(SURFACE)) > SnowMelting.MELT_LIGHT_LEVEL, "Snow should be lit by the glowstone"); + }) + .thenExecute(() -> { + helper.randomTick(SURFACE); + SnowTestHelper.assertLayers(helper, SURFACE, 4); + }) + .thenSucceed(); + } + + private static void topDown(GameTestHelper helper) { + helper.setBiome(Biomes.PLAINS); + SnowTestHelper.place(helper, SURFACE, 12); + helper.randomTick(SURFACE); + SnowTestHelper.assertLayers(helper, SURFACE, 12); + helper.randomTick(SURFACE.above()); + SnowTestHelper.assertLayers(helper, SURFACE, 11); + helper.succeed(); + } + + private static Consumer warmBiome(int expected) { + return helper -> { + helper.setBiome(Biomes.PLAINS); + SnowTestHelper.place(helper, SURFACE, 3); + helper.randomTick(SURFACE); + SnowTestHelper.assertLayers(helper, SURFACE, expected); + helper.succeed(); + }; + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/SnowGameTests.java b/snow/src/main/java/dev/imb11/snow/gametest/SnowGameTests.java new file mode 100644 index 0000000..42bfe90 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/SnowGameTests.java @@ -0,0 +1,62 @@ +package dev.imb11.snow.gametest; + +import cc.cassian.mru.Platform; +import dev.imb11.snow.Snow; +import dev.imb11.snow.gametest.support.SnowTestEnvironments; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.FunctionGameTestInstance; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.gametest.framework.GameTestInstance; +import net.minecraft.gametest.framework.TestData; +import net.minecraft.gametest.framework.TestEnvironmentDefinition; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.block.Rotation; +//? if >=26.3 { +/*import net.minecraft.world.level.Level; +*///?} + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.stream.Stream; + +public class SnowGameTests { + private static final int PADDING = 4; + private static final List TESTS = Stream.of(AccumulationTests.all(), GenerationTests.all(), MeltingTests.all(), HarvestingTests.all(), SnowloggingTests.all()) + .flatMap(List::stream) + .toList(); + + public static boolean enabled() { + return Platform.INSTANCE.isDeveloperEnvironment(); + } + + public static List all() { + return TESTS; + } + + public static void registerInstances(BiFunction>, TestEnvironmentDefinition, Holder>> environments, BiConsumer instances) { + Map>, Holder>> holders = new HashMap<>(); + SnowTestEnvironments.ALL.forEach((key, environment) -> holders.put(key, environments.apply(key, environment))); + TESTS.forEach(test -> instances.accept(test.id(), test.instance(holders.get(test.environment())))); + } + + public record TestDefinition(Identifier id, ResourceKey> environment, Identifier structure, int maxTicks, Consumer function) { + public TestDefinition(String name, ResourceKey> environment, Identifier structure, int maxTicks, Consumer function) { + this(Snow.id(name), environment, structure, maxTicks, function); + } + + public GameTestInstance instance(Holder> environment) { + //? if >=26.3 { + /*TestData>> data = new TestData<>(environment, Level.OVERWORLD, this.structure, this.maxTicks, 0, true, Rotation.NONE, false, 1, 1, true, PADDING); + *///?} else { + TestData>> data = new TestData<>(environment, this.structure, this.maxTicks, 0, true, Rotation.NONE, false, 1, 1, true, PADDING); + //?} + return new FunctionGameTestInstance(ResourceKey.create(Registries.TEST_FUNCTION, this.id), data); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/SnowloggingTests.java b/snow/src/main/java/dev/imb11/snow/gametest/SnowloggingTests.java new file mode 100644 index 0000000..238da44 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/SnowloggingTests.java @@ -0,0 +1,203 @@ +package dev.imb11.snow.gametest; + +import dev.imb11.snow.accumulation.SnowAccumulation; +import dev.imb11.snow.gametest.SnowGameTests.TestDefinition; +import dev.imb11.snow.gametest.support.SnowTestHelper; +import dev.imb11.snow.snowlogging.SnowloggedChunkData; +import dev.imb11.snow.snowlogging.Snowlogging; +import dev.imb11.snow.snowlogging.SnowloggingStorage; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.SectionPos; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +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.item.context.UseOnContext; +import net.minecraft.world.level.GameType; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Objects; + +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.MELTING; +import static dev.imb11.snow.gametest.support.SnowTestEnvironments.SNOWLOGGING_OFF; +import static dev.imb11.snow.gametest.support.SnowTestHelper.PLATFORM; +import static dev.imb11.snow.gametest.support.SnowTestHelper.SURFACE; + +public class SnowloggingTests { + public static List all() { + return List.of( + new TestDefinition("snowlogging/place_on_flower", MELTING, PLATFORM, 20, SnowloggingTests::placeOnFlower), + new TestDefinition("snowlogging/place_on_grass", MELTING, PLATFORM, 20, SnowloggingTests::placeOnGrass), + new TestDefinition("snowlogging/adds_layers", MELTING, PLATFORM, 20, SnowloggingTests::addsLayers), + new TestDefinition("snowlogging/plant_into_snow", MELTING, PLATFORM, 20, SnowloggingTests::plantIntoSnow), + new TestDefinition("snowlogging/accumulation", MELTING, PLATFORM, 20, SnowloggingTests::accumulation), + new TestDefinition("snowlogging/break_restores_plant", MELTING, PLATFORM, 20, SnowloggingTests::breakRestoresPlant), + new TestDefinition("snowlogging/destroy_restores_plant", MELTING, PLATFORM, 20, SnowloggingTests::destroyRestoresPlant), + new TestDefinition("snowlogging/shovel_restores_plant", MELTING, PLATFORM, 20, SnowloggingTests::shovelRestoresPlant), + new TestDefinition("snowlogging/replaced_drops_plant", MELTING, PLATFORM, 20, SnowloggingTests::replacedDropsPlant), + new TestDefinition("snowlogging/command_clears_plant", MELTING, PLATFORM, 20, SnowloggingTests::commandClearsPlant), + new TestDefinition("snowlogging/saves_plant", MELTING, PLATFORM, 20, SnowloggingTests::savesPlant), + new TestDefinition("snowlogging/disabled", SNOWLOGGING_OFF, PLATFORM, 20, SnowloggingTests::disabled) + ); + } + + private static void placeOnFlower(GameTestHelper helper) { + plant(helper, Blocks.POPPY); + use(helper, Items.SNOW); + SnowTestHelper.assertLayers(helper, SURFACE, 1); + assertPlant(helper, Blocks.POPPY); + helper.succeed(); + } + + private static void placeOnGrass(GameTestHelper helper) { + plant(helper, Blocks.SHORT_GRASS); + use(helper, Items.SNOW); + SnowTestHelper.assertLayers(helper, SURFACE, 1); + assertPlant(helper, Blocks.SHORT_GRASS); + helper.succeed(); + } + + private static void addsLayers(GameTestHelper helper) { + plant(helper, Blocks.POPPY); + use(helper, Items.SNOW); + use(helper, Items.SNOW); + SnowTestHelper.assertLayers(helper, SURFACE, 2); + assertPlant(helper, Blocks.POPPY); + helper.succeed(); + } + + private static void plantIntoSnow(GameTestHelper helper) { + helper.setBlock(SURFACE.below(), Blocks.GRASS_BLOCK); + SnowTestHelper.place(helper, SURFACE, 3); + use(helper, Items.POPPY); + SnowTestHelper.assertLayers(helper, SURFACE, 3); + assertPlant(helper, Blocks.POPPY); + helper.succeed(); + } + + private static void accumulation(GameTestHelper helper) { + plant(helper, Blocks.POPPY); + SnowAccumulation.accumulate(helper.getLevel(), helper.absolutePos(SURFACE)); + SnowTestHelper.assertLayers(helper, SURFACE, 1); + assertPlant(helper, Blocks.POPPY); + helper.succeed(); + } + + private static void breakRestoresPlant(GameTestHelper helper) { + snowlog(helper, Blocks.POPPY, 3); + helper.getLevel().removeBlock(helper.absolutePos(SURFACE), false); + helper.assertBlockPresent(Blocks.POPPY, SURFACE); + assertPlant(helper, null); + helper.succeed(); + } + + private static void destroyRestoresPlant(GameTestHelper helper) { + snowlog(helper, Blocks.POPPY, 3); + helper.getLevel().destroyBlock(helper.absolutePos(SURFACE), true); + helper.assertBlockPresent(Blocks.POPPY, SURFACE); + helper.assertItemEntityNotPresent(Items.POPPY, SURFACE, 2); + helper.succeed(); + } + + private static void shovelRestoresPlant(GameTestHelper helper) { + snowlog(helper, Blocks.POPPY, 4); + use(helper, Items.IRON_SHOVEL); + helper.assertBlockPresent(Blocks.POPPY, SURFACE); + helper.assertItemEntityPresent(Items.SNOWBALL, SURFACE, 2); + helper.succeed(); + } + + private static void replacedDropsPlant(GameTestHelper helper) { + snowlog(helper, Blocks.POPPY, 1); + helper.setBlock(SURFACE, Blocks.STONE); + helper.assertBlockPresent(Blocks.STONE, SURFACE); + helper.assertItemEntityPresent(Items.POPPY, SURFACE, 2); + assertPlant(helper, null); + helper.succeed(); + } + + private static void commandClearsPlant(GameTestHelper helper) { + snowlog(helper, Blocks.POPPY, 1); + helper.getLevel().setBlock(helper.absolutePos(SURFACE), Blocks.AIR.defaultBlockState(), Block.UPDATE_CLIENTS); + helper.assertBlockPresent(Blocks.AIR, SURFACE); + helper.assertItemEntityNotPresent(Items.POPPY, SURFACE, 2); + assertPlant(helper, null); + helper.succeed(); + } + + private static void savesPlant(GameTestHelper helper) { + snowlog(helper, Blocks.POPPY, 5); + ServerLevel level = helper.getLevel(); + BlockPos pos = helper.absolutePos(SURFACE); + SerializableChunkData data = SerializableChunkData.copyOf(level, level.getChunkAt(pos)); + helper.assertValueEqual(sectionState(data, pos), Blocks.POPPY.defaultBlockState(), "saved block"); + SnowTestHelper.assertLayers(helper, SURFACE, 5); + + CompoundTag tag = data.write(); + helper.assertTrue(tag.contains(SnowloggingStorage.TAG), "Saved chunk should store snow layers"); + SerializableChunkData loaded = SerializableChunkData.parse(level, level.palettedContainerFactory(), tag); + helper.assertTrue(loaded != null, "Saved chunk should parse"); + SnowloggingStorage.restore(loaded, ((SnowloggedChunkData) (Object) loaded).snow$getLayers()); + helper.assertValueEqual(sectionState(loaded, pos), Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS, 5), "loaded block"); + helper.succeed(); + } + + private static void disabled(GameTestHelper helper) { + plant(helper, Blocks.SHORT_GRASS); + use(helper, Items.SNOW); + SnowTestHelper.assertLayers(helper, SURFACE, 1); + assertPlant(helper, null); + helper.getLevel().removeBlock(helper.absolutePos(SURFACE), false); + helper.assertBlockPresent(Blocks.AIR, SURFACE); + helper.succeed(); + } + + private static void plant(GameTestHelper helper, Block plant) { + helper.setBlock(SURFACE.below(), Blocks.GRASS_BLOCK); + helper.setBlock(SURFACE, plant); + } + + private static void snowlog(GameTestHelper helper, Block plant, int layers) { + plant(helper, plant); + SnowTestHelper.place(helper, SURFACE, layers); + assertPlant(helper, plant); + } + + private static void assertPlant(GameTestHelper helper, @Nullable Block expected) { + BlockState plant = Snowlogging.plantAt(helper.getLevel(), helper.absolutePos(SURFACE)); + Block actual = plant == null ? null : plant.getBlock(); + helper.assertTrue(Objects.equals(actual, expected), "Expected snowlogged plant " + expected + " but found " + actual); + } + + private static BlockState sectionState(SerializableChunkData data, BlockPos pos) { + LevelChunkSection section = data.sectionData().stream() + .filter(sectionData -> sectionData.y() == SectionPos.blockToSectionCoord(pos.getY())) + .findFirst() + .orElseThrow() + .chunkSection(); + return section.getBlockState(SectionPos.sectionRelative(pos.getX()), SectionPos.sectionRelative(pos.getY()), SectionPos.sectionRelative(pos.getZ())); + } + + private static void use(GameTestHelper helper, Item item) { + Player player = helper.makeMockPlayer(GameType.SURVIVAL); + ItemStack stack = new ItemStack(item); + player.setItemInHand(InteractionHand.MAIN_HAND, stack); + BlockPos absolute = helper.absolutePos(SURFACE); + BlockHitResult hit = new BlockHitResult(Vec3.atCenterOf(absolute), Direction.UP, absolute, false); + stack.useOn(new UseOnContext(player, InteractionHand.MAIN_HAND, hit)); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/support/SnowTestEnvironments.java b/snow/src/main/java/dev/imb11/snow/gametest/support/SnowTestEnvironments.java new file mode 100644 index 0000000..41a3821 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/support/SnowTestEnvironments.java @@ -0,0 +1,101 @@ +package dev.imb11.snow.gametest.support; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.imb11.snow.Snow; +import dev.imb11.snow.config.SnowConfig; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.TestEnvironmentDefinition; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.gamerules.GameRuleMap; +import net.minecraft.world.level.gamerules.GameRules; + +import java.util.Map; + +public class SnowTestEnvironments { + public static final Identifier CONFIG_TYPE = Snow.id("config"); + public static final ResourceKey> SNOWFALL = key("snowfall"); + public static final ResourceKey> SNOWFALL_CAPPED = key("snowfall_capped"); + public static final ResourceKey> MELTING = key("melting"); + public static final ResourceKey> WARM_MELTING = key("warm_melting"); + public static final ResourceKey> HARVESTING_OFF = key("harvesting_off"); + public static final ResourceKey> SNOWLOGGING_OFF = key("snowlogging_off"); + + public static final Map>, TestEnvironmentDefinition> ALL = Map.of( + SNOWFALL, new TestEnvironmentDefinition.AllOf(rain(), rules(), ConfigEnvironment.DEFAULT), + SNOWFALL_CAPPED, new TestEnvironmentDefinition.AllOf(rain(), rules(5), ConfigEnvironment.DEFAULT), + MELTING, new TestEnvironmentDefinition.AllOf(rules(), ConfigEnvironment.DEFAULT), + WARM_MELTING, new TestEnvironmentDefinition.AllOf(rules(), ConfigEnvironment.DEFAULT.withMeltInWarmBiomes(true)), + HARVESTING_OFF, new TestEnvironmentDefinition.AllOf(rules(), ConfigEnvironment.DEFAULT.withShovelHarvesting(false)), + SNOWLOGGING_OFF, new TestEnvironmentDefinition.AllOf(rules(), ConfigEnvironment.DEFAULT.withSnowlogging(false)) + ); + + private static ResourceKey> key(String name) { + return ResourceKey.create(Registries.TEST_ENVIRONMENT, Snow.id(name)); + } + + private static TestEnvironmentDefinition rain() { + return new TestEnvironmentDefinition.Weather(TestEnvironmentDefinition.Weather.Type.RAIN); + } + + private static TestEnvironmentDefinition rules() { + return rules(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT.defaultValue()); + } + + private static TestEnvironmentDefinition rules(int maxSnowAccumulationHeight) { + GameRuleMap rules = GameRuleMap.of(); + rules.set(GameRules.RANDOM_TICK_SPEED, 0); + rules.set(GameRules.MAX_SNOW_ACCUMULATION_HEIGHT, maxSnowAccumulationHeight); + return new TestEnvironmentDefinition.SetGameRules(rules); + } + + public record ConfigEnvironment(boolean accumulation, boolean generation, boolean gradualMelting, boolean meltInWarmBiomes, boolean shovelHarvesting, boolean snowlogging) implements TestEnvironmentDefinition { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + Codec.BOOL.fieldOf("accumulation").forGetter(ConfigEnvironment::accumulation), + Codec.BOOL.fieldOf("generation").forGetter(ConfigEnvironment::generation), + Codec.BOOL.fieldOf("gradual_melting").forGetter(ConfigEnvironment::gradualMelting), + Codec.BOOL.fieldOf("melt_in_warm_biomes").forGetter(ConfigEnvironment::meltInWarmBiomes), + Codec.BOOL.fieldOf("shovel_harvesting").forGetter(ConfigEnvironment::shovelHarvesting), + Codec.BOOL.fieldOf("snowlogging").forGetter(ConfigEnvironment::snowlogging) + ).apply(instance, ConfigEnvironment::new)); + public static final ConfigEnvironment DEFAULT = new ConfigEnvironment(true, true, true, false, true, true); + + public ConfigEnvironment withMeltInWarmBiomes(boolean meltInWarmBiomes) { + return new ConfigEnvironment(this.accumulation, this.generation, this.gradualMelting, meltInWarmBiomes, this.shovelHarvesting, this.snowlogging); + } + + public ConfigEnvironment withShovelHarvesting(boolean shovelHarvesting) { + return new ConfigEnvironment(this.accumulation, this.generation, this.gradualMelting, this.meltInWarmBiomes, shovelHarvesting, this.snowlogging); + } + + public ConfigEnvironment withSnowlogging(boolean snowlogging) { + return new ConfigEnvironment(this.accumulation, this.generation, this.gradualMelting, this.meltInWarmBiomes, this.shovelHarvesting, snowlogging); + } + + @Override + public ConfigEnvironment setup(ServerLevel level) { + SnowConfig config = SnowConfig.get(); + ConfigEnvironment previous = new ConfigEnvironment(config.accumulation, config.generation, config.gradualMelting, config.meltInWarmBiomes, config.shovelHarvesting, config.snowlogging); + config.accumulation = this.accumulation; + config.generation = this.generation; + config.gradualMelting = this.gradualMelting; + config.meltInWarmBiomes = this.meltInWarmBiomes; + config.shovelHarvesting = this.shovelHarvesting; + config.snowlogging = this.snowlogging; + return previous; + } + + @Override + public void teardown(ServerLevel level, ConfigEnvironment previous) { + previous.setup(level); + } + + @Override + public MapCodec codec() { + return CODEC; + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/gametest/support/SnowTestHelper.java b/snow/src/main/java/dev/imb11/snow/gametest/support/SnowTestHelper.java new file mode 100644 index 0000000..bc4c188 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/gametest/support/SnowTestHelper.java @@ -0,0 +1,65 @@ +package dev.imb11.snow.gametest.support; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import dev.imb11.snow.util.SnowColumn; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Holder; +import net.minecraft.core.registries.Registries; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.resources.Identifier; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowLayerBlock; + +import java.util.List; + +public class SnowTestHelper { + public static final Identifier PLATFORM = Snow.id("gametest/platform"); + public static final Identifier CHUNK_PLATFORM = Snow.id("gametest/chunk_platform"); + public static final BlockPos SURFACE = new BlockPos(4, 1, 4); + + public static List> profiledBiomes(GameTestHelper helper) { + List> biomes = helper.getLevel().registryAccess().lookupOrThrow(SnowProfiles.KEY).stream() + .flatMap(profile -> profile.biomes().stream()) + .distinct() + .toList(); + helper.assertFalse(biomes.isEmpty(), "No snow profiles are loaded"); + return biomes; + } + + public static Holder biome(GameTestHelper helper, ResourceKey key) { + return helper.getLevel().registryAccess().lookupOrThrow(Registries.BIOME).getOrThrow(key); + } + + public static SnowProfile setBiome(GameTestHelper helper, Holder biome) { + helper.setBiome(biome.unwrapKey().orElseThrow()); + return SnowProfiles.get(helper.getLevel(), biome).orElseThrow(); + } + + public static int layers(GameTestHelper helper, BlockPos pos) { + return SnowColumn.layers(helper.getLevel(), helper.absolutePos(pos)); + } + + public static double surfaceY(GameTestHelper helper, BlockPos pos) { + BlockPos top = helper.absolutePos(layers(helper, pos) > SnowLayerBlock.MAX_HEIGHT ? pos.above() : pos); + return top.getY() + helper.getLevel().getBlockState(top).getCollisionShape(helper.getLevel(), top).max(Direction.Axis.Y); + } + + public static void assertLayers(GameTestHelper helper, BlockPos pos, int expected) { + helper.assertValueEqual(layers(helper, pos), expected, "snow layers"); + } + + public static void place(GameTestHelper helper, BlockPos pos, int layers) { + SnowColumn.place(helper.getLevel(), helper.absolutePos(pos), layers, Block.UPDATE_ALL, false); + } + + public static void clear(GameTestHelper helper, BlockPos pos) { + helper.setBlock(pos.above(), Blocks.AIR); + helper.setBlock(pos, Blocks.AIR); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/generation/SnowDrift.java b/snow/src/main/java/dev/imb11/snow/generation/SnowDrift.java new file mode 100644 index 0000000..a0898ea --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/generation/SnowDrift.java @@ -0,0 +1,45 @@ +package dev.imb11.snow.generation; + +import net.minecraft.core.BlockPos; +import net.minecraft.tags.BlockTags; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +public class SnowDrift { + private static final int RADIUS = 2; + + public static float sample(BlockGetter level, BlockPos pos) { + BlockPos.MutableBlockPos cursor = new BlockPos.MutableBlockPos(); + if (!isGround(level.getBlockState(cursor.set(pos.getX(), pos.getY() - 1, pos.getZ())))) { + return 0f; + } + + int total = 0; + for (int dx = -RADIUS; dx <= RADIUS; dx++) { + for (int dz = -RADIUS; dz <= RADIUS; dz++) { + if (dx != 0 || dz != 0) { + total += step(level, cursor, pos.getX() + dx, pos.getY(), pos.getZ() + dz); + } + } + } + + return (float) total / ((RADIUS * 2 + 1) * (RADIUS * 2 + 1) - 1); + } + + private static int step(BlockGetter level, BlockPos.MutableBlockPos cursor, int x, int y, int z) { + if (isGround(level.getBlockState(cursor.set(x, y, z)))) { + return 1; + } + + return isGround(level.getBlockState(cursor.set(x, y - 1, z))) ? 0 : -1; + } + + private static boolean isGround(BlockState state) { + if (!state.getFluidState().isEmpty()) { + return true; + } + + return state.isSolid() && !state.is(Blocks.SNOW) && !state.is(BlockTags.LEAVES) && !state.is(BlockTags.LOGS); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/generation/SnowGeneration.java b/snow/src/main/java/dev/imb11/snow/generation/SnowGeneration.java new file mode 100644 index 0000000..bd6f67f --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/generation/SnowGeneration.java @@ -0,0 +1,30 @@ +package dev.imb11.snow.generation; + +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import dev.imb11.snow.util.SnowColumn; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.block.SnowLayerBlock; + +public class SnowGeneration { + public static boolean place(WorldGenLevel level, BlockPos pos, int flags) { + return SnowProfiles.get(level, level.getBiome(pos)).map(profile -> { + SnowColumn.place(level, pos, layers(level, pos, profile), flags, false); + return true; + }).orElse(false); + } + + public static int layers(WorldGenLevel level, BlockPos pos, SnowProfile profile) { + long seed = level.getSeed(); + int x = pos.getX(); + int z = pos.getZ(); + float depth = Mth.clamp(0.5f + 0.75f * SnowNoise.DEPTH.sample(seed, x, z), 0f, 1f); + float detail = 1.5f * SnowNoise.DETAIL.sample(seed, x, z); + float surface = Mth.clamp(Mth.lerp(depth, profile.minLayers(), profile.maxLayers()) + detail, profile.minLayers(), profile.maxLayers()); + float driftScale = Mth.clamp(0.8f + 0.9f * SnowNoise.DRIFT.sample(seed, x, z), 0.2f, 1.4f); + float drift = SnowDrift.sample(level, pos) * SnowLayerBlock.MAX_HEIGHT * profile.driftStrength() * driftScale; + return Mth.clamp(Math.round(surface + drift), 1, SnowColumn.MAX_LAYERS); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/generation/SnowNoise.java b/snow/src/main/java/dev/imb11/snow/generation/SnowNoise.java new file mode 100644 index 0000000..05d5d57 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/generation/SnowNoise.java @@ -0,0 +1,64 @@ +package dev.imb11.snow.generation; + +import dev.imb11.snow.Snow; +import it.unimi.dsi.fastutil.doubles.DoubleList; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; +import net.minecraft.world.level.levelgen.synth.NormalNoise; +//? if >=26.3 { +/*import net.minecraft.world.level.levelgen.synth.Noise; +*///?} + +public class SnowNoise { + public static final SnowNoise DEPTH = new SnowNoise("snow_depth", -7, 1.0, 1.0, 0.5); + public static final SnowNoise DETAIL = new SnowNoise("snow_detail", -4, 1.0, 1.0); + public static final SnowNoise DRIFT = new SnowNoise("snow_drift", -6, 1.0, 0.5); + + private final String name; + private final int firstOctave; + private final DoubleList amplitudes; + private volatile Sampler sampler; + + private SnowNoise(String name, int firstOctave, double... amplitudes) { + this.name = Snow.id(name).toString(); + this.firstOctave = firstOctave; + this.amplitudes = DoubleList.of(amplitudes); + } + + public float sample(long seed, int x, int z) { + Sampler current = this.sampler; + if (current == null || current.seed != seed) { + current = new Sampler(seed); + this.sampler = current; + } + + return current.value(x, z); + } + + private final class Sampler { + private final long seed; + //? if >=26.3 { + /*private final Noise noise; + *///?} else { + private final NormalNoise noise; + //?} + + private Sampler(long seed) { + this.seed = seed; + RandomSource random = new XoroshiroRandomSource(seed).forkPositional().fromHashOf(SnowNoise.this.name); + //? if >=26.3 { + /*this.noise = NormalNoise.createParity(SnowNoise.this.firstOctave, SnowNoise.this.amplitudes).create(random); + *///?} else { + this.noise = NormalNoise.create(random, new NormalNoise.NoiseParameters(SnowNoise.this.firstOctave, SnowNoise.this.amplitudes)); + //?} + } + + private float value(int x, int z) { + //? if >=26.3 { + /*return this.noise.get(x, 0, z); + *///?} else { + return (float) this.noise.getValue(x, 0, z); + //?} + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/harvesting/SnowHarvesting.java b/snow/src/main/java/dev/imb11/snow/harvesting/SnowHarvesting.java new file mode 100644 index 0000000..c49d189 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/harvesting/SnowHarvesting.java @@ -0,0 +1,48 @@ +package dev.imb11.snow.harvesting; + +import dev.imb11.snow.config.SnowConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; +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.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LevelEvent; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEvent; + +public class SnowHarvesting { + public static boolean useOn(UseOnContext context) { + if (!SnowConfig.get().shovelHarvesting || context.getClickedFace() == Direction.DOWN) { + return false; + } + + Level level = context.getLevel(); + if (level.isClientSide() && !SnowConfig.SYNC.serverPresent()) { + return false; + } + + BlockPos pos = context.getClickedPos(); + BlockState state = level.getBlockState(pos); + if (!state.is(Blocks.SNOW) || !level.getBlockState(pos.above()).isAir()) { + return false; + } + + if (level instanceof ServerLevel serverLevel) { + Player player = context.getPlayer(); + ItemStack tool = context.getItemInHand(); + Block.dropResources(state, serverLevel, pos, null, player, tool); + level.removeBlock(pos, false); + level.levelEvent(LevelEvent.PARTICLES_DESTROY_BLOCK, pos, Block.getId(state)); + level.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(player, state)); + if (player != null) { + tool.hurtAndBreak(1, player, context.getHand().asEquipmentSlot()); + } + } + + return true; + } +} diff --git a/snow/src/main/java/dev/imb11/snow/melting/SnowMelting.java b/snow/src/main/java/dev/imb11/snow/melting/SnowMelting.java new file mode 100644 index 0000000..e1c4e4e --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/melting/SnowMelting.java @@ -0,0 +1,49 @@ +package dev.imb11.snow.melting; + +import dev.imb11.snow.config.SnowConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.level.block.state.BlockState; +//? if >=26.3 { +/*import net.minecraft.world.level.gameevent.GameEvent; +*///?} + +public class SnowMelting { + public static final int MELT_LIGHT_LEVEL = 11; + + public static boolean randomTick(BlockState state, ServerLevel level, BlockPos pos) { + SnowConfig config = SnowConfig.get(); + if (!config.gradualMelting && !config.meltInWarmBiomes) { + return false; + } + + if (!shouldMelt(level, pos, config) || level.getBlockState(pos.above()).is(Blocks.SNOW)) { + return true; + } + + int layers = state.getValue(SnowLayerBlock.LAYERS); + if (config.gradualMelting && layers > 1) { + level.setBlockAndUpdate(pos, state.setValue(SnowLayerBlock.LAYERS, layers - 1)); + } else { + Block.dropResources(state, level, pos); + level.removeBlock(pos, false); + //? if >=26.3 { + /*level.gameEvent(GameEvent.BLOCK_DESTROY, pos, GameEvent.Context.of(state)); + *///?} + } + + return true; + } + + private static boolean shouldMelt(ServerLevel level, BlockPos pos, SnowConfig config) { + if (level.getBrightness(LightLayer.BLOCK, pos) > MELT_LIGHT_LEVEL) { + return true; + } + + return config.meltInWarmBiomes && level.getBiome(pos).value().warmEnoughToRain(pos, level.getSeaLevel()); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/accumulation/ServerLevelMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/accumulation/ServerLevelMixin.java new file mode 100644 index 0000000..99aa6e5 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/accumulation/ServerLevelMixin.java @@ -0,0 +1,27 @@ +package dev.imb11.snow.mixin.accumulation; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.imb11.snow.accumulation.SnowAccumulation; +import dev.imb11.snow.config.SnowConfig; +import dev.imb11.snow.snowlogging.Snowlogging; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.biome.Biome; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ServerLevel.class) +public class ServerLevelMixin { + @WrapOperation(method = "tickPrecipitation", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/Biome;shouldSnow(Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z")) + private boolean snow$accumulate(Biome biome, LevelReader level, BlockPos pos, Operation original) { + boolean shouldSnow = original.call(biome, level, pos) || Snowlogging.shouldSnow(biome, level, pos); + if (!shouldSnow || !SnowConfig.get().accumulation) { + return shouldSnow; + } + + SnowAccumulation.accumulate((ServerLevel) (Object) this, pos); + return false; + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/accumulation/client/LeavesBlockMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/accumulation/client/LeavesBlockMixin.java new file mode 100644 index 0000000..25da1f6 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/accumulation/client/LeavesBlockMixin.java @@ -0,0 +1,24 @@ +package dev.imb11.snow.mixin.accumulation.client; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.util.ParticleUtils; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LeavesBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(LeavesBlock.class) +public class LeavesBlockMixin { + @Inject(method = "animateTick", at = @At("TAIL")) + private void snow$dropSnowThroughLeaves(BlockState state, Level level, BlockPos pos, RandomSource random, CallbackInfo ci) { + if (random.nextInt(8) == 0 && level.getBlockState(pos.above()).is(Blocks.SNOW)) { + ParticleUtils.spawnParticleBelow(level, pos, random, ParticleTypes.SNOWFLAKE); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/client/ClientLevelMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/client/ClientLevelMixin.java new file mode 100644 index 0000000..0f953f1 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/client/ClientLevelMixin.java @@ -0,0 +1,31 @@ +package dev.imb11.snow.mixin.client; + +import dev.imb11.snow.client.SnowClient; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.BooleanSupplier; + +@Mixin(ClientLevel.class) +public class ClientLevelMixin { + @Inject(method = "tick", at = @At("TAIL")) + private void snow$tick(BooleanSupplier haveTime, CallbackInfo ci) { + SnowClient.tick((ClientLevel) (Object) this); + } + + @Inject(method = "unload", at = @At("HEAD")) + private void snow$chunkUnloaded(LevelChunk chunk, CallbackInfo ci) { + SnowClient.chunkUnloaded((ClientLevel) (Object) this, chunk); + } + + @Inject(method = "setBlocksDirty", at = @At("HEAD")) + private void snow$blockChanged(BlockPos pos, BlockState oldState, BlockState newState, CallbackInfo ci) { + SnowClient.blockChanged((ClientLevel) (Object) this, pos, oldState, newState); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/client/MinecraftMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/client/MinecraftMixin.java new file mode 100644 index 0000000..e576269 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/client/MinecraftMixin.java @@ -0,0 +1,18 @@ +package dev.imb11.snow.mixin.client; + +import dev.imb11.snow.client.SnowClient; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Minecraft.class) +public class MinecraftMixin { + @Inject(method = "updateLevelInEngines(Lnet/minecraft/client/multiplayer/ClientLevel;Z)V", at = @At("HEAD")) + private void snow$levelChanged(@Nullable ClientLevel level, boolean stopSound, CallbackInfo ci) { + SnowClient.levelChanged(level); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/frost/client/BlockColorsMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/frost/client/BlockColorsMixin.java new file mode 100644 index 0000000..8efea06 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/frost/client/BlockColorsMixin.java @@ -0,0 +1,35 @@ +package dev.imb11.snow.mixin.frost.client; + +import dev.imb11.snow.client.frost.FrostedLeafTintSource; +import dev.imb11.snow.frost.FrostableLeaves; +import net.minecraft.client.color.block.BlockColors; +import net.minecraft.client.color.block.BlockTintSource; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Mixin(BlockColors.class) +public class BlockColorsMixin { + @Unique + private final Map, List> snow$frostedSources = new ConcurrentHashMap<>(); + + @Inject(method = "getTintSources", at = @At("RETURN"), cancellable = true) + private void snow$frostLeaves(BlockState state, CallbackInfoReturnable> cir) { + List sources = cir.getReturnValue(); + if (!sources.isEmpty() && FrostableLeaves.contains(state)) { + cir.setReturnValue(this.snow$frostedSources.computeIfAbsent(sources, original -> { + List frosted = new ArrayList<>(original); + frosted.set(0, new FrostedLeafTintSource(original.getFirst())); + return List.copyOf(frosted); + })); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/frost/client/GlobalSettingsUniformMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/frost/client/GlobalSettingsUniformMixin.java new file mode 100644 index 0000000..69da9c2 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/frost/client/GlobalSettingsUniformMixin.java @@ -0,0 +1,23 @@ +package dev.imb11.snow.mixin.frost.client; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.blaze3d.buffers.Std140Builder; +import dev.imb11.snow.client.frost.FrostBackend; +import dev.imb11.snow.client.frost.FrostWeather; +import net.minecraft.client.renderer.GlobalSettingsUniform; +import net.minecraft.util.Mth; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(GlobalSettingsUniform.class) +public class GlobalSettingsUniformMixin { + @ModifyExpressionValue(method = "", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/buffers/Std140SizeCalculator;get()I")) + private static int snow$reserveFrost(int size) { + return Mth.roundToward(size + Float.BYTES, 16); + } + + @ModifyExpressionValue(method = "update", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/buffers/Std140Builder;putInt(I)Lcom/mojang/blaze3d/buffers/Std140Builder;", ordinal = 1)) + private Std140Builder snow$appendFrost(Std140Builder builder) { + return builder.putFloat(FrostBackend.INSTANCE.useShader() ? FrostWeather.INSTANCE.interpolatedProgress() : 0.0F); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/ShaderIncludeMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/ShaderIncludeMixin.java new file mode 100644 index 0000000..ba6cac4 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/ShaderIncludeMixin.java @@ -0,0 +1,16 @@ +package dev.imb11.snow.mixin.frost.compat; + +import dev.imb11.snow.client.frost.FrostShader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Pseudo +@Mixin(targets = "com.mojang.renderpearl.api.pipeline.ShaderSource$CachedIncludeSource") +public class ShaderIncludeMixin { + @ModifyVariable(method = "create", at = @At("HEAD"), argsOnly = true) + private static String snow$patchFrostInclude(String source) { + return FrostShader.patchGlobals(source); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/ShaderManagerMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/ShaderManagerMixin.java new file mode 100644 index 0000000..836ba54 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/ShaderManagerMixin.java @@ -0,0 +1,37 @@ +package dev.imb11.snow.mixin.frost.compat; + +import com.google.common.collect.ImmutableMap; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +//? if >=26.3 { +/*import com.mojang.renderpearl.api.pipeline.ShaderType; +*///?} else { +import com.mojang.blaze3d.shaders.ShaderType; +import org.spongepowered.asm.mixin.injection.ModifyArg; +//?} +import dev.imb11.snow.client.frost.FrostShader; +import net.minecraft.client.renderer.ShaderManager; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ShaderManager.class) +public class ShaderManagerMixin { + //? if >=26.3 { + /*@WrapOperation(method = "loadShader", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/ImmutableMap$Builder;put(Ljava/lang/Object;Ljava/lang/Object;)Lcom/google/common/collect/ImmutableMap$Builder;")) + private static ImmutableMap.Builder snow$patchFrostVertex(ImmutableMap.Builder output, Object key, Object source, Operation> original, @Local(argsOnly = true) Identifier location, @Local(argsOnly = true) ShaderType type) { + return original.call(output, key, FrostShader.patchVertex(type.idConverter().fileToId(location), type == ShaderType.VERTEX, (String) source)); + } + *///?} else { + @ModifyArg(method = "loadShader", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/preprocessor/GlslPreprocessor;process(Ljava/lang/String;)Ljava/util/List;")) + private static String snow$patchFrostVertex(String source, @Local(argsOnly = true) Identifier location, @Local(argsOnly = true) ShaderType type) { + return FrostShader.patchVertex(type.idConverter().fileToId(location), type == ShaderType.VERTEX, source); + } + + @WrapOperation(method = "loadShader", at = @At(value = "INVOKE", target = "Lcom/google/common/collect/ImmutableMap$Builder;put(Ljava/lang/Object;Ljava/lang/Object;)Lcom/google/common/collect/ImmutableMap$Builder;")) + private static ImmutableMap.Builder snow$patchFrostGlobals(ImmutableMap.Builder output, Object key, Object source, Operation> original) { + return original.call(output, key, FrostShader.patchGlobals((String) source)); + } + //?} +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/SodiumColorProviderRegistryMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/SodiumColorProviderRegistryMixin.java new file mode 100644 index 0000000..8c71e64 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/SodiumColorProviderRegistryMixin.java @@ -0,0 +1,35 @@ +package dev.imb11.snow.mixin.frost.compat; + +import dev.imb11.snow.client.frost.compat.SodiumFrostColorProvider; +import dev.imb11.snow.frost.FrostableLeaves; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import net.caffeinemc.mods.sodium.client.model.color.ColorProvider; +import net.minecraft.client.color.block.BlockColors; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Pseudo +@Mixin(targets = "net.caffeinemc.mods.sodium.client.model.color.ColorProviderRegistry", remap = false) +abstract class SodiumColorProviderRegistryMixin { + @Shadow + @Final + private Reference2ReferenceMap> blocks; + + @Inject(method = "", at = @At("TAIL")) + private void snow$frostLeaves(BlockColors colors, CallbackInfo ci) { + for (Block block : BuiltInRegistries.BLOCK) { + ColorProvider provider = this.blocks.get(block); + if (provider != null && FrostableLeaves.contains(block.defaultBlockState())) { + this.blocks.put(block, new SodiumFrostColorProvider(provider)); + } + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/SodiumGlobalUniformsMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/SodiumGlobalUniformsMixin.java new file mode 100644 index 0000000..2458d9f --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/frost/compat/SodiumGlobalUniformsMixin.java @@ -0,0 +1,18 @@ +package dev.imb11.snow.mixin.frost.compat; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.mojang.blaze3d.buffers.Std140Builder; +import dev.imb11.snow.client.frost.FrostBackend; +import dev.imb11.snow.client.frost.FrostWeather; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.injection.At; + +@Pseudo +@Mixin(targets = "net.caffeinemc.mods.sodium.client.render.chunk.UniformBufferManager$GlobalUniforms", remap = false) +abstract class SodiumGlobalUniformsMixin { + @ModifyExpressionValue(method = "write", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/buffers/Std140Builder;putInt(I)Lcom/mojang/blaze3d/buffers/Std140Builder;")) + private Std140Builder snow$appendFrost(Std140Builder builder) { + return builder.putFloat(FrostBackend.INSTANCE.useShader() ? FrostWeather.INSTANCE.interpolatedProgress() : 0.0F); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/generation/SnowAndFreezeFeatureMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/generation/SnowAndFreezeFeatureMixin.java new file mode 100644 index 0000000..aa540c5 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/generation/SnowAndFreezeFeatureMixin.java @@ -0,0 +1,36 @@ +package dev.imb11.snow.mixin.generation; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.imb11.snow.config.SnowConfig; +import dev.imb11.snow.generation.SnowGeneration; +import dev.imb11.snow.snowlogging.Snowlogging; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.WorldGenLevel; +import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.SnowAndFreezeFeature; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(SnowAndFreezeFeature.class) +public class SnowAndFreezeFeatureMixin { + @WrapOperation(method = "place", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/WorldGenLevel;setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;I)Z")) + private boolean snow$placeLayers(WorldGenLevel level, BlockPos pos, BlockState state, int flags, Operation original) { + if (state.is(Blocks.SNOW)) { + Snowlogging.snowlog(level, pos); + if (SnowConfig.get().generation && SnowGeneration.place(level, pos, flags)) { + return true; + } + } + + return original.call(level, pos, state, flags); + } + + @WrapOperation(method = "place", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/Biome;shouldSnow(Lnet/minecraft/world/level/LevelReader;Lnet/minecraft/core/BlockPos;)Z")) + private boolean snow$snowOnPlants(Biome biome, LevelReader level, BlockPos pos, Operation original) { + return original.call(biome, level, pos) || Snowlogging.shouldSnow(biome, level, pos); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/harvesting/ShovelUseMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/harvesting/ShovelUseMixin.java new file mode 100644 index 0000000..2833784 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/harvesting/ShovelUseMixin.java @@ -0,0 +1,34 @@ +package dev.imb11.snow.mixin.harvesting; + +import dev.imb11.snow.harvesting.SnowHarvesting; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.context.UseOnContext; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +//? if >=26.3 { +/*import net.minecraft.tags.ItemTags; +import net.minecraft.world.item.Item; +*///?} else { +import net.minecraft.world.item.ShovelItem; +//?} + +//? if >=26.3 { +/*@Mixin(Item.class) +*///?} else { +@Mixin(ShovelItem.class) +//?} +public class ShovelUseMixin { + @Inject(method = "useOn", at = @At("HEAD"), cancellable = true) + private void snow$harvest(UseOnContext context, CallbackInfoReturnable cir) { + //? if >=26.3 { + /*if (!context.getItemInHand().is(ItemTags.SHOVELS)) { + return; + } + *///?} + if (SnowHarvesting.useOn(context)) { + cir.setReturnValue(InteractionResult.SUCCESS); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/melting/SnowLayerBlockMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/melting/SnowLayerBlockMixin.java new file mode 100644 index 0000000..c750d4a --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/melting/SnowLayerBlockMixin.java @@ -0,0 +1,22 @@ +package dev.imb11.snow.mixin.melting; + +import dev.imb11.snow.melting.SnowMelting; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(SnowLayerBlock.class) +public class SnowLayerBlockMixin { + @Inject(method = "randomTick", at = @At("HEAD"), cancellable = true) + private void snow$melt(BlockState state, ServerLevel level, BlockPos pos, RandomSource random, CallbackInfo ci) { + if (SnowMelting.randomTick(state, level, pos)) { + ci.cancel(); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/BlockBehaviourMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/BlockBehaviourMixin.java new file mode 100644 index 0000000..baf58e2 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/BlockBehaviourMixin.java @@ -0,0 +1,35 @@ +package dev.imb11.snow.mixin.snowlogging; + +import dev.imb11.snow.snowlogging.Snowlogging; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Explosion; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.function.BiConsumer; + +@Mixin(BlockBehaviour.class) +public class BlockBehaviourMixin { + @Inject(method = "canBeReplaced(Lnet/minecraft/world/level/block/state/BlockState;Lnet/minecraft/world/item/context/BlockPlaceContext;)Z", at = @At("HEAD"), cancellable = true) + private void snow$replaceWithSnow(BlockState state, BlockPlaceContext context, CallbackInfoReturnable cir) { + if (Snowlogging.canPlaceInto(state, context)) { + cir.setReturnValue(true); + } + } + + @Inject(method = "onExplosionHit", at = @At("HEAD")) + private void snow$destroyPlant(BlockState state, ServerLevel level, BlockPos pos, Explosion explosion, BiConsumer onHit, CallbackInfo ci) { + if (state.is(Blocks.SNOW)) { + Snowlogging.destroy(level, pos); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/BlockItemMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/BlockItemMixin.java new file mode 100644 index 0000000..8efe68b --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/BlockItemMixin.java @@ -0,0 +1,41 @@ +package dev.imb11.snow.mixin.snowlogging; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import dev.imb11.snow.snowlogging.Snowlogging; +import net.minecraft.core.BlockPos; +import net.minecraft.sounds.SoundEvent; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SoundType; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(BlockItem.class) +public class BlockItemMixin { + @Inject(method = "placeBlock", at = @At("HEAD"), cancellable = true) + private void snow$placeIntoSnow(BlockPlaceContext context, BlockState placementState, CallbackInfoReturnable cir) { + if (Snowlogging.placeInto(context, placementState)) { + cir.setReturnValue(true); + } + } + + @WrapOperation(method = "place", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/Level;playSound(Lnet/minecraft/world/entity/Entity;Lnet/minecraft/core/BlockPos;Lnet/minecraft/sounds/SoundEvent;Lnet/minecraft/sounds/SoundSource;FF)V")) + private void snow$playPlantSound(Level level, Entity except, BlockPos pos, SoundEvent sound, SoundSource source, float volume, float pitch, Operation original, @Local(ordinal = 0) BlockState placementState, @Local(ordinal = 1) BlockState placedState) { + if (placedState.is(Blocks.SNOW) && placementState.is(Snowlogging.SNOWLOGGABLE)) { + SoundType soundType = placementState.getSoundType(); + original.call(level, except, pos, soundType.getPlaceSound(), source, (soundType.getVolume() + 1.0F) / 2.0F, soundType.getPitch() * 0.8F); + return; + } + + original.call(level, except, pos, sound, source, volume, pitch); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/LevelMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/LevelMixin.java new file mode 100644 index 0000000..975a7f2 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/LevelMixin.java @@ -0,0 +1,18 @@ +package dev.imb11.snow.mixin.snowlogging; + +import com.llamalad7.mixinextras.sugar.Local; +import dev.imb11.snow.snowlogging.Snowlogging; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +@Mixin(Level.class) +public class LevelMixin { + @ModifyVariable(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z", at = @At("HEAD"), argsOnly = true) + private BlockState snow$snowlog(BlockState state, @Local(argsOnly = true) BlockPos pos, @Local(argsOnly = true, ordinal = 0) int flags) { + return Snowlogging.setBlock((Level) (Object) this, pos, state, flags); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/SerializableChunkDataMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/SerializableChunkDataMixin.java new file mode 100644 index 0000000..0abaeef --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/SerializableChunkDataMixin.java @@ -0,0 +1,56 @@ +package dev.imb11.snow.mixin.snowlogging; + +import com.llamalad7.mixinextras.sugar.Local; +import dev.imb11.snow.snowlogging.SnowloggedChunkData; +import dev.imb11.snow.snowlogging.SnowloggingStorage; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.ProtoChunk; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; + +@Mixin(SerializableChunkData.class) +public class SerializableChunkDataMixin implements SnowloggedChunkData { + @Unique + private List snow$layers = List.of(); + + @Override + public List snow$getLayers() { + return this.snow$layers; + } + + @Override + public void snow$setLayers(List layers) { + this.snow$layers = layers; + } + + @Inject(method = "copyOf", at = @At("RETURN")) + private static void snow$storePlants(CallbackInfoReturnable cir, @Local(argsOnly = true) ChunkAccess chunk) { + SerializableChunkData data = cir.getReturnValue(); + ((SnowloggedChunkData) (Object) data).snow$setLayers(SnowloggingStorage.store(chunk, data)); + } + + @Inject(method = "write", at = @At("RETURN")) + private void snow$writeLayers(CallbackInfoReturnable cir) { + SnowloggingStorage.write(cir.getReturnValue(), this.snow$layers); + } + + @Inject(method = "parse", at = @At("RETURN")) + private static void snow$readLayers(CallbackInfoReturnable cir, @Local(argsOnly = true) CompoundTag tag) { + SerializableChunkData data = cir.getReturnValue(); + if (data != null) { + ((SnowloggedChunkData) (Object) data).snow$setLayers(SnowloggingStorage.read(tag)); + } + } + + @Inject(method = "read", at = @At("HEAD")) + private void snow$restorePlants(CallbackInfoReturnable cir) { + SnowloggingStorage.restore((SerializableChunkData) (Object) this, this.snow$layers); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/SnowLayerBlockMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/SnowLayerBlockMixin.java new file mode 100644 index 0000000..77e5588 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/SnowLayerBlockMixin.java @@ -0,0 +1,20 @@ +package dev.imb11.snow.mixin.snowlogging; + +import dev.imb11.snow.snowlogging.Snowlogging; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(SnowLayerBlock.class) +public class SnowLayerBlockMixin { + @Inject(method = "canBeReplaced", at = @At("HEAD"), cancellable = true) + private void snow$replaceWithPlant(BlockState state, BlockPlaceContext context, CallbackInfoReturnable cir) { + if (Snowlogging.canPlantInto(state, context)) { + cir.setReturnValue(true); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/client/BlockColorsMixin.java b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/client/BlockColorsMixin.java new file mode 100644 index 0000000..4e0d350 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/mixin/snowlogging/client/BlockColorsMixin.java @@ -0,0 +1,19 @@ +package dev.imb11.snow.mixin.snowlogging.client; + +import dev.imb11.snow.client.SnowloggedTintSource; +import net.minecraft.client.color.block.BlockColors; +import net.minecraft.world.level.block.Blocks; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; + +@Mixin(BlockColors.class) +public class BlockColorsMixin { + @Inject(method = "createDefault", at = @At("RETURN")) + private static void snow$tintSnowloggedPlants(CallbackInfoReturnable cir) { + cir.getReturnValue().register(List.of(new SnowloggedTintSource()), Blocks.SNOW); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForge.java b/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForge.java new file mode 100644 index 0000000..9df4848 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForge.java @@ -0,0 +1,32 @@ +//? neoforge { +/*package dev.imb11.snow.neoforge; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.profile.SnowProfile; +import dev.imb11.snow.profile.SnowProfiles; +import dev.imb11.snow.gametest.SnowGameTests; +import dev.imb11.snow.snowlogging.SnowloggingAttachment; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.fml.common.Mod; +//? if >=26.3 { +/^import net.neoforged.neoforge.registries.NewDatapackRegistryEvent; +^///?} else { +import net.neoforged.neoforge.registries.DataPackRegistryEvent; +//?} + +@Mod(Snow.MOD_ID) +public class SnowNeoForge { + public SnowNeoForge(IEventBus bus) { + //? if >=26.3 { + /^bus.addListener(NewDatapackRegistryEvent.class, event -> event.worldRegistry(SnowProfiles.KEY, SnowProfile.CODEC)); + ^///?} else { + bus.addListener(DataPackRegistryEvent.NewRegistry.class, event -> event.dataPackRegistry(SnowProfiles.KEY, SnowProfile.CODEC)); + //?} + Snow.init(); + SnowloggingAttachment.register(bus); + if (SnowGameTests.enabled()) { + SnowNeoForgeGameTests.register(bus); + } + } +} +*///?} diff --git a/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForgeClient.java b/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForgeClient.java new file mode 100644 index 0000000..0fe6435 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForgeClient.java @@ -0,0 +1,37 @@ +//? neoforge { +/*package dev.imb11.snow.neoforge; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.client.SnowClient; +import dev.imb11.snow.client.SnowloggedBlockStateModel; +import dev.imb11.snow.client.frost.BiomeTemperatureResolver; +import dev.imb11.snow.config.SnowConfig; +import net.minecraft.world.level.block.Blocks; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.ModContainer; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.fml.common.Mod; +import net.neoforged.neoforge.client.event.ModelEvent; +import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; +import net.neoforged.neoforge.client.gui.IConfigScreenFactory; + +@Mod(value = Snow.MOD_ID, dist = Dist.CLIENT) +@EventBusSubscriber(modid = Snow.MOD_ID, value = Dist.CLIENT) +public class SnowNeoForgeClient { + public SnowNeoForgeClient(ModContainer container) { + SnowClient.init(); + container.registerExtensionPoint(IConfigScreenFactory.class, (client, parent) -> SnowConfig.getYACL().generateScreen(parent)); + } + + @SubscribeEvent + public static void registerColorResolvers(RegisterColorHandlersEvent.ColorResolvers event) { + event.register(BiomeTemperatureResolver.RESOLVER); + } + + @SubscribeEvent + public static void wrapSnowModels(ModelEvent.ModifyBakingResult event) { + event.getBakingResult().blockStateModels().replaceAll((state, model) -> state.is(Blocks.SNOW) ? new SnowloggedBlockStateModel(model) : model); + } +} +*///?} diff --git a/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForgeGameTests.java b/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForgeGameTests.java new file mode 100644 index 0000000..e62badb --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/neoforge/SnowNeoForgeGameTests.java @@ -0,0 +1,23 @@ +//? neoforge { +/*package dev.imb11.snow.neoforge; + +import dev.imb11.snow.gametest.SnowGameTests; +import dev.imb11.snow.gametest.support.SnowTestEnvironments; +import net.minecraft.core.registries.Registries; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.event.RegisterGameTestsEvent; +import net.neoforged.neoforge.registries.RegisterEvent; + +public class SnowNeoForgeGameTests { + public static void register(IEventBus bus) { + bus.addListener(RegisterEvent.class, event -> { + event.register(Registries.TEST_ENVIRONMENT_DEFINITION_TYPE, helper -> helper.register(SnowTestEnvironments.CONFIG_TYPE, SnowTestEnvironments.ConfigEnvironment.CODEC)); + event.register(Registries.TEST_FUNCTION, helper -> SnowGameTests.all().forEach(test -> helper.register(test.id(), test.function()))); + }); + + bus.addListener(RegisterGameTestsEvent.class, event -> { + SnowGameTests.registerInstances((key, environment) -> event.registerEnvironment(key.identifier(), environment), (id, instance) -> event.registerTest(id, instance)); + }); + } +} +*///?} diff --git a/snow/src/main/java/dev/imb11/snow/profile/SnowProfile.java b/snow/src/main/java/dev/imb11/snow/profile/SnowProfile.java new file mode 100644 index 0000000..ab19790 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/profile/SnowProfile.java @@ -0,0 +1,32 @@ +package dev.imb11.snow.profile; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.imb11.snow.util.SnowColumn; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +//? if >=26.3 { +/*import net.minecraft.core.registries.codec.RegistryCodecs; +*///?} else { +import net.minecraft.core.RegistryCodecs; +//?} +import net.minecraft.world.level.biome.Biome; + +public record SnowProfile(HolderSet biomes, int minLayers, int maxLayers, float accumulationRate, float driftStrength, int priority) { + public static final float DEFAULT_DRIFT_STRENGTH = 1f; + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + //? if >=26.3 { + /*RegistryCodecs.holderSet(Registries.BIOME).fieldOf("biomes").forGetter(SnowProfile::biomes), + *///?} else { + RegistryCodecs.homogeneousList(Registries.BIOME).fieldOf("biomes").forGetter(SnowProfile::biomes), + //?} + Codec.intRange(1, SnowColumn.MAX_LAYERS).fieldOf("min_layers").forGetter(SnowProfile::minLayers), + Codec.intRange(1, SnowColumn.MAX_LAYERS).fieldOf("max_layers").forGetter(SnowProfile::maxLayers), + Codec.floatRange(0, 8).optionalFieldOf("accumulation_rate", 1f).forGetter(SnowProfile::accumulationRate), + Codec.floatRange(0, 4).optionalFieldOf("drift_strength", DEFAULT_DRIFT_STRENGTH).forGetter(SnowProfile::driftStrength), + Codec.INT.optionalFieldOf("priority", 0).forGetter(SnowProfile::priority) + ).apply(instance, SnowProfile::new)).validate(profile -> profile.minLayers > profile.maxLayers + ? DataResult.error(() -> "min_layers must not exceed max_layers") + : DataResult.success(profile)); +} diff --git a/snow/src/main/java/dev/imb11/snow/profile/SnowProfiles.java b/snow/src/main/java/dev/imb11/snow/profile/SnowProfiles.java new file mode 100644 index 0000000..481820b --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/profile/SnowProfiles.java @@ -0,0 +1,26 @@ +package dev.imb11.snow.profile; + +import dev.imb11.snow.Snow; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.biome.Biome; + +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Stream; + +public class SnowProfiles { + public static final ResourceKey> KEY = ResourceKey.createRegistryKey(Snow.id("snow_profile")); + + public static Optional get(LevelReader level, Holder biome) { + return level.registryAccess().lookup(KEY).flatMap(registry -> resolve(registry.stream(), biome)); + } + + public static Optional resolve(Stream profiles, Holder biome) { + return profiles + .filter(profile -> profile.biomes().contains(biome)) + .max(Comparator.comparingInt(SnowProfile::priority)); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggedChunkData.java b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggedChunkData.java new file mode 100644 index 0000000..a14eba1 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggedChunkData.java @@ -0,0 +1,9 @@ +package dev.imb11.snow.snowlogging; + +import java.util.List; + +public interface SnowloggedChunkData { + List snow$getLayers(); + + void snow$setLayers(List layers); +} diff --git a/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggedPlants.java b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggedPlants.java new file mode 100644 index 0000000..3a89998 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggedPlants.java @@ -0,0 +1,73 @@ +package dev.imb11.snow.snowlogging; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +public record SnowloggedPlants(Map plants) { + public static final SnowloggedPlants EMPTY = new SnowloggedPlants(Map.of()); + public static final MapCodec MAP_CODEC = Entry.CODEC.listOf().fieldOf("plants").xmap(SnowloggedPlants::fromEntries, SnowloggedPlants::entries); + public static final Codec CODEC = MAP_CODEC.codec(); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.>map(HashMap::new, BlockPos.STREAM_CODEC, ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY)) + .map(SnowloggedPlants::new, SnowloggedPlants::plants); + + public SnowloggedPlants { + plants = Map.copyOf(plants); + } + + public @Nullable BlockState get(BlockPos pos) { + return this.plants.get(pos); + } + + public boolean isEmpty() { + return this.plants.isEmpty(); + } + + public SnowloggedPlants with(BlockPos pos, BlockState plant) { + if (Objects.equals(this.plants.get(pos), plant)) { + return this; + } + + Map plants = new HashMap<>(this.plants); + plants.put(pos.immutable(), plant); + return new SnowloggedPlants(plants); + } + + public SnowloggedPlants without(BlockPos pos) { + if (!this.plants.containsKey(pos)) { + return this; + } + + Map plants = new HashMap<>(this.plants); + plants.remove(pos); + return new SnowloggedPlants(plants); + } + + private static SnowloggedPlants fromEntries(List entries) { + return new SnowloggedPlants(entries.stream().collect(Collectors.toMap(Entry::pos, Entry::plant, (first, second) -> second))); + } + + private List entries() { + return this.plants.entrySet().stream().map(entry -> new Entry(entry.getKey(), entry.getValue())).toList(); + } + + private record Entry(BlockPos pos, BlockState plant) { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + BlockPos.CODEC.fieldOf("pos").forGetter(Entry::pos), + BlockState.CODEC.fieldOf("plant").forGetter(Entry::plant) + ).apply(instance, Entry::new)); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/snowlogging/Snowlogging.java b/snow/src/main/java/dev/imb11/snow/snowlogging/Snowlogging.java new file mode 100644 index 0000000..a57418d --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/snowlogging/Snowlogging.java @@ -0,0 +1,155 @@ +package dev.imb11.snow.snowlogging; + +import dev.imb11.snow.Snow; +import dev.imb11.snow.config.SnowConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.LightLayer; +import net.minecraft.world.level.biome.Biome; +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.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import org.jspecify.annotations.Nullable; + +public class Snowlogging { + public static final TagKey SNOWLOGGABLE = TagKey.create(Registries.BLOCK, Snow.id("snowloggable")); + private static SnowloggingListener listener = SnowloggingListener.NONE; + + public static SnowloggingListener listener() { + return listener; + } + + public static void setListener(SnowloggingListener listener) { + Snowlogging.listener = listener; + } + + public static boolean canSnowlog(BlockState state) { + return SnowConfig.get().snowlogging && state.is(SNOWLOGGABLE); + } + + public static boolean canPlaceInto(BlockState state, BlockPlaceContext context) { + if (!context.getItemInHand().is(Items.SNOW) || !canSnowlog(state)) { + return false; + } + + return !context.getLevel().isClientSide() || SnowConfig.SYNC.serverPresent(); + } + + public static boolean canPlantInto(BlockState state, BlockPlaceContext context) { + if (!state.is(Blocks.SNOW) || !(context.getItemInHand().getItem() instanceof BlockItem item) || !canSnowlog(item.getBlock().defaultBlockState())) { + return false; + } + + Level level = context.getLevel(); + if (level.isClientSide() && !SnowConfig.SYNC.serverPresent()) { + return false; + } + + return plantAt(level, context.getClickedPos()) == null; + } + + public static boolean placeInto(BlockPlaceContext context, BlockState plant) { + Level level = context.getLevel(); + BlockPos pos = context.getClickedPos(); + if (!canPlantInto(level.getBlockState(pos), context) || !canSnowlog(plant)) { + return false; + } + + if (level.isClientSide()) { + listener.plantPredicted(level, pos, plant); + } else { + track(level.getChunk(pos), pos, plant); + } + + return true; + } + + public static boolean shouldSnow(Biome biome, LevelReader level, BlockPos pos) { + if (!canSnowlog(level.getBlockState(pos)) || biome.getPrecipitationAt(pos, level.getSeaLevel()) != Biome.Precipitation.SNOW) { + return false; + } + + return level.isInsideBuildHeight(pos.getY()) + && level.getBrightness(LightLayer.BLOCK, pos) < 10 + && Blocks.SNOW.defaultBlockState().canSurvive(level, pos); + } + + public static @Nullable BlockState plantAt(LevelAccessor level, BlockPos pos) { + return SnowloggingAttachment.get(level.getChunk(pos)).get(pos); + } + + public static void snowlog(LevelAccessor level, BlockPos pos) { + ChunkAccess chunk = level.getChunk(pos); + BlockState state = chunk.getBlockState(pos); + if (canSnowlog(state)) { + track(chunk, pos, state); + } + } + + public static BlockState setBlock(Level level, BlockPos pos, BlockState state, int flags) { + if (!level.isInValidBounds(pos)) { + return state; + } + + LevelChunk chunk = level.getChunkAt(pos); + BlockState plant = SnowloggingAttachment.get(chunk).get(pos); + if (plant == null) { + if (!level.isClientSide() && state.is(Blocks.SNOW)) { + snowlog(level, pos); + } + + return state; + } + + return state.is(Blocks.SNOW) ? state : uncover(level, chunk, pos, plant, state, flags); + } + + private static BlockState uncover(Level level, LevelChunk chunk, BlockPos pos, BlockState plant, BlockState state, int flags) { + boolean snowRemoved = chunk.getBlockState(pos).is(Blocks.SNOW) && (flags & Block.UPDATE_NEIGHBORS) != 0; + boolean plantRestored = snowRemoved && (state.isAir() || state == plant); + if (!level.isClientSide()) { + untrack(chunk, pos); + if (snowRemoved && !plantRestored) { + Block.dropResources(plant, level, pos); + } + } + + return plantRestored ? plant : state; + } + + public static void destroy(Level level, BlockPos pos) { + LevelChunk chunk = level.getChunkAt(pos); + BlockState plant = SnowloggingAttachment.get(chunk).get(pos); + if (plant != null) { + untrack(chunk, pos); + Block.dropResources(plant, level, pos); + } + } + + private static void track(ChunkAccess chunk, BlockPos pos, BlockState plant) { + SnowloggedPlants plants = SnowloggingAttachment.get(chunk); + SnowloggedPlants updated = plants.with(pos, plant); + if (updated != plants) { + SnowloggingAttachment.set(chunk, updated); + chunk.markUnsaved(); + } + } + + private static void untrack(ChunkAccess chunk, BlockPos pos) { + SnowloggedPlants plants = SnowloggingAttachment.get(chunk); + SnowloggedPlants updated = plants.without(pos); + if (updated != plants) { + SnowloggingAttachment.set(chunk, updated); + chunk.markUnsaved(); + } + } +} diff --git a/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingAttachment.java b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingAttachment.java new file mode 100644 index 0000000..9de2a63 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingAttachment.java @@ -0,0 +1,83 @@ +//? fabric { +package dev.imb11.snow.snowlogging; + +import dev.imb11.snow.Snow; +import net.fabricmc.fabric.api.attachment.v1.AttachmentRegistry; +import net.fabricmc.fabric.api.attachment.v1.AttachmentSyncPredicate; +import net.fabricmc.fabric.api.attachment.v1.AttachmentType; +import net.minecraft.world.level.chunk.ChunkAccess; + +public class SnowloggingAttachment { + public static final AttachmentType TYPE = AttachmentRegistry.create(Snow.id("snowlogged_plants"), builder -> builder + .persistent(SnowloggedPlants.CODEC) + .syncWith(SnowloggedPlants.STREAM_CODEC, AttachmentSyncPredicate.all())); + + public static void init() { + } + + public static SnowloggedPlants get(ChunkAccess chunk) { + return chunk.getAttachedOrElse(TYPE, SnowloggedPlants.EMPTY); + } + + public static void set(ChunkAccess chunk, SnowloggedPlants plants) { + chunk.setAttached(TYPE, plants); + } +} +//?} else { +/*package dev.imb11.snow.snowlogging; + +import dev.imb11.snow.Snow; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.attachment.AttachmentSyncHandler; +import net.neoforged.neoforge.attachment.AttachmentType; +import net.neoforged.neoforge.attachment.IAttachmentHolder; +import net.neoforged.neoforge.registries.DeferredRegister; +import net.neoforged.neoforge.registries.NeoForgeRegistries; +import org.jspecify.annotations.Nullable; + +import java.util.function.Supplier; + +public class SnowloggingAttachment { + private static final DeferredRegister> ATTACHMENT_TYPES = DeferredRegister.create(NeoForgeRegistries.Keys.ATTACHMENT_TYPES, Snow.MOD_ID); + public static final Supplier> TYPE = ATTACHMENT_TYPES.register("snowlogged_plants", () -> AttachmentType.builder(() -> SnowloggedPlants.EMPTY) + .serialize(SnowloggedPlants.MAP_CODEC, plants -> !plants.isEmpty()) + .sync(new SyncHandler()) + .build()); + + public static void register(IEventBus bus) { + ATTACHMENT_TYPES.register(bus); + } + + public static SnowloggedPlants get(ChunkAccess chunk) { + SnowloggedPlants plants = chunk.getExistingDataOrNull(TYPE); + return plants == null ? SnowloggedPlants.EMPTY : plants; + } + + public static void set(ChunkAccess chunk, SnowloggedPlants plants) { + chunk.setData(TYPE, plants); + if (chunk instanceof LevelChunk levelChunk && !levelChunk.getLevel().isClientSide()) { + levelChunk.syncData(TYPE); + } + } + + private static class SyncHandler implements AttachmentSyncHandler { + @Override + public void write(RegistryFriendlyByteBuf buf, SnowloggedPlants attachment, boolean initialSync) { + SnowloggedPlants.STREAM_CODEC.encode(buf, attachment); + } + + @Override + public SnowloggedPlants read(IAttachmentHolder holder, RegistryFriendlyByteBuf buf, @Nullable SnowloggedPlants previousValue) { + SnowloggedPlants plants = SnowloggedPlants.STREAM_CODEC.decode(buf); + if (holder instanceof LevelChunk chunk) { + Snowlogging.listener().plantsChanged(chunk.getLevel(), previousValue, plants); + } + + return plants; + } + } +} +*///?} diff --git a/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingListener.java b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingListener.java new file mode 100644 index 0000000..2ef4aa1 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingListener.java @@ -0,0 +1,17 @@ +package dev.imb11.snow.snowlogging; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.jspecify.annotations.Nullable; + +public interface SnowloggingListener { + SnowloggingListener NONE = new SnowloggingListener() { + }; + + default void plantsChanged(Level level, @Nullable SnowloggedPlants previous, @Nullable SnowloggedPlants current) { + } + + default void plantPredicted(Level level, BlockPos pos, BlockState plant) { + } +} diff --git a/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingStorage.java b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingStorage.java new file mode 100644 index 0000000..8e87ff1 --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/snowlogging/SnowloggingStorage.java @@ -0,0 +1,85 @@ +package dev.imb11.snow.snowlogging; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.storage.SerializableChunkData; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class SnowloggingStorage { + public static final String TAG = "snow:snowlogged_layers"; + private static final Codec> CODEC = Layers.CODEC.listOf(); + + public static List store(ChunkAccess chunk, SerializableChunkData data) { + List layers = new ArrayList<>(); + for (Map.Entry entry : SnowloggingAttachment.get(chunk).plants().entrySet()) { + BlockPos pos = entry.getKey(); + BlockState snow = chunk.getBlockState(pos); + LevelChunkSection section = section(data, pos); + if (!snow.is(Blocks.SNOW) || section == null) { + continue; + } + + setBlockState(section, pos, entry.getValue()); + layers.add(new Layers(pos, snow.getValue(SnowLayerBlock.LAYERS))); + } + + return layers; + } + + public static void restore(SerializableChunkData data, List layers) { + for (Layers entry : layers) { + LevelChunkSection section = section(data, entry.pos()); + if (section != null && getBlockState(section, entry.pos()).is(Snowlogging.SNOWLOGGABLE)) { + setBlockState(section, entry.pos(), Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS, entry.layers())); + } + } + } + + public static void write(CompoundTag tag, List layers) { + if (!layers.isEmpty()) { + tag.store(TAG, CODEC, layers); + } + } + + public static List read(CompoundTag tag) { + return tag.read(TAG, CODEC).orElse(List.of()); + } + + private static @Nullable LevelChunkSection section(SerializableChunkData data, BlockPos pos) { + int sectionY = SectionPos.blockToSectionCoord(pos.getY()); + for (SerializableChunkData.SectionData section : data.sectionData()) { + if (section.y() == sectionY) { + return section.chunkSection(); + } + } + + return null; + } + + private static BlockState getBlockState(LevelChunkSection section, BlockPos pos) { + return section.getBlockState(SectionPos.sectionRelative(pos.getX()), SectionPos.sectionRelative(pos.getY()), SectionPos.sectionRelative(pos.getZ())); + } + + private static void setBlockState(LevelChunkSection section, BlockPos pos, BlockState state) { + section.setBlockState(SectionPos.sectionRelative(pos.getX()), SectionPos.sectionRelative(pos.getY()), SectionPos.sectionRelative(pos.getZ()), state, false); + } + + public record Layers(BlockPos pos, int layers) { + private static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + BlockPos.CODEC.fieldOf("pos").forGetter(Layers::pos), + Codec.intRange(1, SnowLayerBlock.MAX_HEIGHT).fieldOf("layers").forGetter(Layers::layers) + ).apply(instance, Layers::new)); + } +} diff --git a/snow/src/main/java/dev/imb11/snow/util/SnowColumn.java b/snow/src/main/java/dev/imb11/snow/util/SnowColumn.java new file mode 100644 index 0000000..60f689d --- /dev/null +++ b/snow/src/main/java/dev/imb11/snow/util/SnowColumn.java @@ -0,0 +1,50 @@ +package dev.imb11.snow.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SnowLayerBlock; +import net.minecraft.world.level.block.state.BlockState; + +public class SnowColumn { + public static final int MAX_LAYERS = 12; + + public static int layers(BlockGetter level, BlockPos base) { + BlockState state = level.getBlockState(base); + if (!state.is(Blocks.SNOW)) { + return 0; + } + + int layers = state.getValue(SnowLayerBlock.LAYERS); + BlockState above = level.getBlockState(base.above()); + if (layers == SnowLayerBlock.MAX_HEIGHT && above.is(Blocks.SNOW)) { + layers += above.getValue(SnowLayerBlock.LAYERS); + } + + return layers; + } + + public static void place(LevelAccessor level, BlockPos base, int layers, int flags, boolean pushEntities) { + setLayers(level, base, Math.min(layers, SnowLayerBlock.MAX_HEIGHT), flags, pushEntities); + BlockPos above = base.above(); + if (layers > SnowLayerBlock.MAX_HEIGHT && canOverflowInto(level.getBlockState(above))) { + setLayers(level, above, layers - SnowLayerBlock.MAX_HEIGHT, flags, pushEntities); + } + } + + private static boolean canOverflowInto(BlockState state) { + return state.isAir() || state.is(Blocks.SNOW); + } + + private static void setLayers(LevelAccessor level, BlockPos pos, int layers, int flags, boolean pushEntities) { + BlockState state = level.getBlockState(pos); + BlockState newState = Blocks.SNOW.defaultBlockState().setValue(SnowLayerBlock.LAYERS, layers); + if (pushEntities) { + Block.pushEntitiesUp(state, newState, level, pos); + } + + level.setBlock(pos, newState, flags); + } +} diff --git a/snow/src/main/resources/META-INF/neoforge.mods.toml b/snow/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 0000000..27411ec --- /dev/null +++ b/snow/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,48 @@ +modLoader = "javafml" +loaderVersion = "[2,)" +license = "All-Rights-Reserved" + +[[mods]] +modId = "snow" +displayName="${mod_name}" +version = "${version}" +logoFile = "assets/snow/icon.png" +authors = "IMB11" +description="${mod_description}" + +[[mixins]] +config = "snow.mixins.json" + +[[dependencies.snow]] +modId = "neoforge" +type = "required" +versionRange="${target_loader}" +ordering = "NONE" +side = "BOTH" + +[[dependencies.snow]] +modId = "minecraft" +type = "required" +versionRange="${target_minecraft}" + +[[dependencies.snow]] +modId="mru" +type="required" +versionRange="[${target_mru},)" +ordering="NONE" +side="BOTH" + +[[dependencies.snow]] +modId="yet_another_config_lib_v3" +type="required" +versionRange="[${target_yacl},)" +ordering="NONE" +side="BOTH" + +[[dependencies.snow]] +modId="snowrealmagic" +type="incompatible" +reason="Snow already provides snowlogging and even more features compared to Snow! Real Magic! which is incompatible." +versionRange="[0,)" +ordering="NONE" +side="BOTH" diff --git a/snow/src/main/resources/assets/snow/icon.png b/snow/src/main/resources/assets/snow/icon.png new file mode 100644 index 0000000..ed653fa Binary files /dev/null and b/snow/src/main/resources/assets/snow/icon.png differ diff --git a/snow/src/main/resources/assets/snow/lang/en_us.json b/snow/src/main/resources/assets/snow/lang/en_us.json new file mode 100644 index 0000000..7f24236 --- /dev/null +++ b/snow/src/main/resources/assets/snow/lang/en_us.json @@ -0,0 +1,15 @@ +{ + "snow.config.title": "Snow", + "snow.config.option.accumulation.enabled": "Variable Snowfall", + "snow.config.option.description.accumulation.enabled": "Should snow fall faster in certain biomes compared to others?", + "snow.config.option.generation.enabled": "Multi-Layer Generation", + "snow.config.option.description.generation.enabled": "Should snow generate with multiple layers in certain biomes?", + "snow.config.option.melting.gradual_melting": "Gradual Melting", + "snow.config.option.description.melting.gradual_melting": "Should snow melt one layer at a time instead of all at once?", + "snow.config.option.melting.melt_in_warm_biomes": "Melt in Warm Biomes", + "snow.config.option.description.melting.melt_in_warm_biomes": "Should snow melt in biomes that are warm enough to rain?", + "snow.config.option.harvesting.shovel_harvesting": "Shovel Harvesting", + "snow.config.option.description.harvesting.shovel_harvesting": "Should using a shovel on snow clear it and drop snowballs?", + "snow.config.option.snowlogging.enabled": "Snowlogging", + "snow.config.option.description.snowlogging.enabled": "Should snow be able to cover small plants like flowers and grass?" +} diff --git a/snow/src/main/resources/assets/snow/shaders/include/frost.glsl b/snow/src/main/resources/assets/snow/shaders/include/frost.glsl new file mode 100644 index 0000000..2483629 --- /dev/null +++ b/snow/src/main/resources/assets/snow/shaders/include/frost.glsl @@ -0,0 +1,11 @@ +vec4 snow_applyFrost(vec4 color, float progress) { + uint bits = uint(color.a * 255.0 + 0.5); + uint weight = bits >> 4u; + if (weight == 0u || bits == 255u) { + return color; + } + + float tint = float((bits & 15u) + 1u) / 16.0; + float shade = min(max(color.r, max(color.g, color.b)) / tint, 1.0); + return vec4(mix(color.rgb, vec3(shade), float(weight) / 15.0 * progress), 1.0); +} diff --git a/snow/src/main/resources/fabric.mod.json b/snow/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..61798d1 --- /dev/null +++ b/snow/src/main/resources/fabric.mod.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "id": "snow", + "version": "${mod_version}", + "name": "Snow", + "description": "Brings Bedrock Edition's snow mechanics to Java Edition.", + "authors": [ + "IMB11" + ], + "contact": { + "sources": "https://github.com/IMB11-Mods/JavaEditionMods", + "issues": "https://github.com/IMB11-Mods/JavaEditionMods/issues" + }, + "license": "All-Rights-Reserved", + "icon": "assets/snow/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "dev.imb11.snow.fabric.SnowFabric" + ], + "client": [ + "dev.imb11.snow.fabric.SnowFabricClient" + ], + "modmenu": [ + "dev.imb11.snow.fabric.ModMenuCompat" + ], + "fabric-datagen": [ + "dev.imb11.snow.datagen.SnowDatagen" + ] + }, + "mixins": [ + "snow.mixins.json" + ], + "depends": { + "fabricloader": ">=${target_loader}", + "minecraft": "${target_minecraft}", + "fabric-api": "*", + "mru": "${target_mru}", + "yet_another_config_lib_v3": "${target_yacl}" + }, + "breaks": { + "snowrealmagic": "*" + } +} diff --git a/snow/src/main/resources/snow.mixins.json b/snow/src/main/resources/snow.mixins.json new file mode 100644 index 0000000..f910f6f --- /dev/null +++ b/snow/src/main/resources/snow.mixins.json @@ -0,0 +1,32 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "dev.imb11.snow.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [ + "accumulation.ServerLevelMixin", + "generation.SnowAndFreezeFeatureMixin", + "harvesting.ShovelUseMixin", + "melting.SnowLayerBlockMixin", + "snowlogging.BlockBehaviourMixin", + "snowlogging.BlockItemMixin", + "snowlogging.LevelMixin", + "snowlogging.SerializableChunkDataMixin", + "snowlogging.SnowLayerBlockMixin" + ], + "client": [ + "accumulation.client.LeavesBlockMixin", + "client.ClientLevelMixin", + "client.MinecraftMixin", + "frost.client.BlockColorsMixin", + "frost.client.GlobalSettingsUniformMixin", + "frost.compat.ShaderIncludeMixin", + "frost.compat.ShaderManagerMixin", + "frost.compat.SodiumColorProviderRegistryMixin", + "frost.compat.SodiumGlobalUniformsMixin", + "snowlogging.client.BlockColorsMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}