{ pkgs, lib }: let # Static per-Minecraft-version pins. Real "latest-recommended" resolution # would need network access at eval time, so for now every supported # targetVersion needs an entry here. versionTable = { "1.21.1" = { neoVersion = "21.1.244"; minecraftVersionRange = "[1.21.1]"; loaderVersionRange = "[1,)"; parchmentMinecraftVersion = "1.21.1"; parchmentMappingsVersion = "2024.11.17"; templateDir = "1.21.x"; }; }; trace = value: builtins.trace value value; in { modPath }: let evaluated = lib.evalModules { modules = [ ../modules/mod-meta.nix ../modules/mod-blocks.nix modPath ]; }; cfg = evaluated.config; versionInfo = versionTable.${cfg.targetVersion} or (throw "snowy: unsupported targetVersion '${cfg.targetVersion}'. Supported versions: ${lib.concatStringsSep ", " (lib.attrNames versionTable)}"); resolvedNeoVersion = if cfg.frameworkVersion == "latest-recommended" then versionInfo.neoVersion else cfg.frameworkVersion; # Hand-written files (customClass, blocks.*.customClass, ...) are given as plain # strings, e.g. "./src/CreateSilicon.java" -- snowy never reads their content or # location, it only needs the class name (the file's basename) to generate the # matching Base class and wire up imports/registration on the generated side. The # actual file is expected to already live at the real Gradle-conventional path # (src/main/java/{package}/...) in the project directory the shellHook runs from. classNameFromPath = p: if p == null then null else lib.removeSuffix ".java" (baseNameOf p); splitBlockId = id: let parts = lib.splitString ":" id; in assert lib.assertMsg (builtins.length parts == 2) "snowy: block id '${id}' must be of the form ':'"; { namespace = builtins.elemAt parts 0; path = builtins.elemAt parts 1; }; blocksList = lib.mapAttrsToList (id: blockCfg: let split = splitBlockId id; hasEnoughTextures = blockCfg.textures != null && ( blockCfg.textures.all != null || (blockCfg.textures.top != null && blockCfg.textures.bottom != null && (blockCfg.textures.side != null || (blockCfg.textures.north != null && blockCfg.textures.south != null && blockCfg.textures.east != null && blockCfg.textures.west != null))) ); in assert lib.assertMsg (split.namespace == cfg.modid) "snowy: block '${id}' namespace must match modid '${cfg.modid}'"; assert lib.assertMsg (!(blockCfg.textures != null && blockCfg.customModel)) "snowy: block '${id}': textures and customModel are mutually exclusive"; assert lib.assertMsg (blockCfg.customModel || hasEnoughTextures) "snowy: block '${id}': textures must set 'all', or 'top'+'bottom'+('side' or all four side faces), unless customModel is set"; assert lib.assertMsg (!(blockCfg.item.texture != null && blockCfg.item.customModel)) "snowy: block '${id}': item.texture and item.customModel are mutually exclusive"; { inherit id; inherit (split) path; displayName = blockCfg.displayName; properties = blockCfg.properties; customClassName = classNameFromPath blockCfg.customClass; textures = blockCfg.textures; customModel = blockCfg.customModel; item = blockCfg.item; blockEntity = if blockCfg.blockEntity == null then null else { customClassName = classNameFromPath blockCfg.blockEntity.class; tickerClient = blockCfg.blockEntity.ticker.client; tickerServer = blockCfg.blockEntity.ticker.server; }; }) cfg.blocks; capitalizedModid = lib.toUpper (lib.substring 0 1 cfg.modid) + lib.substring 1 (-1) cfg.modid; templateContext = cfg // { neoVersion = resolvedNeoVersion; inherit (versionInfo) minecraftVersionRange loaderVersionRange parchmentMinecraftVersion parchmentMappingsVersion; customClassName = classNameFromPath cfg.customClass; customClientClassName = classNameFromPath cfg.customClientClass; blocks = blocksList; blocksClassName = "${capitalizedModid}Blocks"; blockEntitiesClassName = "${capitalizedModid}BlockEntities"; }; configJson = pkgs.writeText "snowy-mod-config-${cfg.modid}.json" (builtins.toJSON templateContext); packagePath = builtins.replaceStrings [ "." ] [ "/" ] cfg.javaPackage; pythonEnv = pkgs.python3.withPackages (ps: [ ps.jinja2 ]); generated = pkgs.runCommand "snowy-generated-${cfg.modid}" { nativeBuildInputs = [ pythonEnv ]; } '' mkdir -p $out python3 ${../codegen/main.py} \ --config ${configJson} \ --templates ${../templates}/${versionInfo.templateDir} \ --out $out ''; # The `gradle wrapper` task validates its distribution URL over the network, # which the (non-fixed-output) Nix build sandbox blocks. Instead, fetch the # three wrapper artifacts verbatim (fixed-output derivations may use the # network) from the upstream NeoForge MDK, which already pins Gradle 9.2.1, # and write gradle-wrapper.properties ourselves. gradleWrapperVersion = "9.2.1"; mdkWrapperBase = "https://raw.githubusercontent.com/NeoForgeMDKs/MDK-1.21.1-ModDevGradle/main"; gradlew = pkgs.fetchurl { url = "${mdkWrapperBase}/gradlew"; sha256 = "0284687nawivy3gz16r4zcssv61a3g1ssvyxmxh5g7vl5v2v91xi"; }; gradlewBat = pkgs.fetchurl { url = "${mdkWrapperBase}/gradlew.bat"; sha256 = "1kg2qjh904cz6da71l30pa1sfbdb707lr4lp4c1jvclgxc9jf44l"; }; gradleWrapperJar = pkgs.fetchurl { url = "${mdkWrapperBase}/gradle/wrapper/gradle-wrapper.jar"; sha256 = "0wk157gn9nh2d7v9j7mfy46csb31iv5qxsx4qsdvachwvv24lfkx"; }; gradleWrapperProperties = pkgs.writeText "gradle-wrapper.properties" '' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-${gradleWrapperVersion}-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ''; gradleWrapper = pkgs.runCommand "snowy-gradle-wrapper" { } '' mkdir -p $out/gradle/wrapper install -m755 ${gradlew} $out/gradlew install -m755 ${gradlewBat} $out/gradlew.bat install -m644 ${gradleWrapperJar} $out/gradle/wrapper/gradle-wrapper.jar install -m644 ${gradleWrapperProperties} $out/gradle/wrapper/gradle-wrapper.properties ''; in assert lib.assertMsg (cfg.targetFramework == [ "neoforge" ]) "snowy: only targetFramework = [ \"neoforge\" ] is currently supported, got: [ ${lib.concatStringsSep " " cfg.targetFramework} ]"; pkgs.mkShell { packages = [ pkgs.jdk21 pkgs.gradle ]; shellHook = '' echo "snowy: refreshing generated NeoForge project files in $(pwd)" # Only ever touch this explicit, framework-owned set of paths -- this is the # real project root now (not an isolated throwaway directory), so it may also # contain the mod author's own unrelated files (.git, flake.nix, an IDE config, # hand-written sources outside src/main/java/**/generated, ...) that must never # be swept up by this cleanup. rm -rf build.gradle settings.gradle gradle.properties .gitignore \ gradlew gradlew.bat gradle \ src/main/resources/META-INF \ "src/main/resources/assets/${cfg.modid}/blockstates" \ "src/main/java/${packagePath}/generated" cp -r --no-preserve=mode,ownership ${generated}/. ./ cp -r --no-preserve=mode,ownership ${gradleWrapper}/. ./ chmod +x gradlew echo "snowy: done. ./gradlew build" ''; }