diff --git a/.github/workflows/capture-baselines.yml b/.github/workflows/capture-baselines.yml deleted file mode 100644 index 066f003..0000000 --- a/.github/workflows/capture-baselines.yml +++ /dev/null @@ -1,138 +0,0 @@ -name: capture-baselines - -# Manual dispatch ONLY. No push / pull_request triggers. -# The workflow never commits back to master and never opens an auto-PR - baselines -# land via a human-reviewed contributor PR. See CONTRIBUTING-BASELINES.md for the -# procedure. -on: - workflow_dispatch: - inputs: - mc_version: - description: "Minecraft version to baseline (default 26.1.2; Tessera 1.0.0 ships with the 26.1.2 AssetProvider)" - required: false - default: "26.1.2" - -concurrency: - # Serialise manual dispatches on the same ref so two contributors cannot race - # each other to produce conflicting baseline zips. cancel-in-progress=false - # because baseline captures are expensive (assets download + full fixture - # render pass) and there is no value in killing a run mid-way through. - group: capture-baselines-${{ github.ref }} - cancel-in-progress: false - -jobs: - capture: - name: capture baselines on JDK 21 LTS - runs-on: ubuntu-latest - # Single-JDK job: baselines anchor on JDK 21 LTS. The cross-JDK - # drift check lives in build-test.yml (the 21+25 matrix), not here. Dispatching - # this workflow twice on different JDKs is not a supported mode. - permissions: - # Read-only: the workflow uploads an artifact but NEVER pushes a commit or - # opens a PR. If a future PR flips this to write, CODEOWNERS review on - # .github/workflows/** is the backstop against accidental auto-commit drift. - contents: read - steps: - - name: Checkout code - uses: actions/checkout@v7 - - - name: Set up JDK 21 (Temurin) - uses: actions/setup-java@v6.0.1 - with: - distribution: 'temurin' - java-version: '21' - cache: 'maven' - - - name: Install reactor up to tessera-testkit - # Installs sibling SNAPSHOT jars (tessera-api, tessera-spi, tessera-bom, - # tessera-assets-26.1.2, tessera-core) into ~/.m2 so that later `mvn -pl - # tessera-testkit ...` invocations (which run in fresh reactor contexts) - # resolve the in-repo deps locally instead of hitting JitPack. `test-compile` - # alone only produces target/classes per module; it does not publish jars, - # so subsequent -pl calls without -am would 404 on JitPack. - run: ./mvnw -B -ntp -pl tessera-testkit -am install -DskipTests - - - name: Hydrate ~/.tessera/assets/${{ github.event.inputs.mc_version }} - # Why this approach and not the plan's original /tmp/Hydrate.java idea: - # 1. TesseraAssets lives in net.aerh.tessera.api.assets (not - # net.aerh.tessera.assets as the plan guessed). An inline /tmp - # launcher with the wrong import would silently fail at runtime. - # 2. Engine.builder().build() does NOT auto-fetch on cache-miss; it - # verifies and throws TesseraAssetsMissingException. So the plan's - # fallback "let BeforeAll engine.build() hydrate" is non-functional. - # Solution: a dedicated CaptureBaselinesHydrator class under test-scope - # in tessera-testkit that wraps TesseraAssets.fetch. Maven's exec-maven- - # plugin resolves the plugin jar on-demand (no pom wiring needed) and - # -Dexec.classpathScope=test picks up the test-compiled hydrator. - run: | - ./mvnw -B -ntp -pl tessera-testkit \ - org.codehaus.mojo:exec-maven-plugin:3.5.0:java \ - -Dexec.mainClass=net.aerh.tessera.testkit.cli.CaptureBaselinesHydrator \ - -Dexec.classpathScope=test \ - -Dexec.args="${{ github.event.inputs.mc_version }} --accept-eula" - env: - # Defence in depth: CaptureBaselinesHydrator passes --accept-eula to the - # builder-flag overload, but TesseraAssets also honours the env var. Both - # paths close so if --accept-eula is ever dropped from the arg list the - # env still permits the download. Matches the three EULA-acceptance - # channels documented on TesseraAssets. - TESSERA_ACCEPT_MOJANG_EULA: "true" - - - name: Mark assets available for GoldenFixturesTest env-gate - # GoldenFixturesTest is gated by @EnabledIfEnvironmentVariable("TESSERA_ASSETS_AVAILABLE=true"). - # Setting it AFTER the hydration step guarantees we only enable the test - # class once the cache is actually populated - a failed hydrate will trip - # the previous step and this one will not run. - run: echo "TESSERA_ASSETS_AVAILABLE=true" >> "$GITHUB_ENV" - - - name: Run GoldenFixturesTest with -Dtessera.golden.update=true - # The update flag toggles GoldenImageAssertion.updateIfRequested into - # regeneration mode, writing the freshly-rendered PNG under - # tessera-testkit/src/test/resources/golden// as the new baseline. - # - # regenerateBaseline refuses to run when env CI=true. We DELIBERATELY do NOT - # set CI=true here (unlike build-test.yml which sets CI=true to enforce the - # guard across normal CI). Setting the flag WITHOUT CI=true is the whole - # point of this workflow - captured baselines are a contributor-reviewed - # output, not a silent overwrite of master. - # - # Do NOT add `continue-on-error: true` - if the render itself fails, the - # diff artifacts still get picked up by the subsequent upload-artifact@v4 - # step's `if: always()` guard and the contributor can inspect what went - # wrong before re-dispatching. - run: | - ./mvnw -B -ntp -pl tessera-testkit test \ - -Dtest=GoldenFixturesTest \ - -Dtessera.golden.update=true - - - name: Upload captured baselines as workflow artifact - if: always() - # `if: always()` so even a partial failure (e.g. 11 of 12 fixtures rendered, - # 1 threw) still uploads whatever PNGs did land, making it easier to - # triage what went wrong. The `if-no-files-found: warn` keeps the job - # green-but-noisy if the entire capture bombed and no PNGs exist. - uses: actions/upload-artifact@v7 - with: - name: tessera-golden-baselines-${{ github.event.inputs.mc_version }}-${{ github.sha }} - path: | - tessera-testkit/src/test/resources/golden/${{ github.event.inputs.mc_version }}/**/*.png - tessera-testkit/src/test/resources/golden/${{ github.event.inputs.mc_version }}/**/*.webp - tessera-testkit/src/test/resources/golden/${{ github.event.inputs.mc_version }}/**/*.gif - retention-days: 14 - if-no-files-found: warn - - - name: Upload surefire reports for SSIM log inspection - if: always() - # Separate artifact (not bundled with the baselines) because the SSIM - # calibration pass only needs the surefire stdout (`SSIM for : ` - # lines from GoldenImageAssertion) and contributors reviewing visuals do - # not need a reports tarball cluttering their download. Two-artifact - # pattern also makes the retention policy obvious: if we ever bump the - # baselines to 30 days we can leave the reports at 14 independently. - uses: actions/upload-artifact@v7 - with: - name: capture-surefire-reports-${{ github.event.inputs.mc_version }}-${{ github.sha }} - path: | - tessera-testkit/target/surefire-reports/** - retention-days: 14 - if-no-files-found: warn diff --git a/.github/workflows/generate-spritesheet.yml b/.github/workflows/generate-spritesheet.yml index d9a7d50..f58a08d 100644 --- a/.github/workflows/generate-spritesheet.yml +++ b/.github/workflows/generate-spritesheet.yml @@ -1,5 +1,7 @@ name: Generate Spritesheet +# Dispatch this on any branch. The regenerated assets are committed back to the branch it ran on, +# or a pull request is opened against that branch, so it works inside an open PR as well as on master. on: workflow_dispatch: inputs: @@ -42,25 +44,23 @@ jobs: generate: runs-on: ubuntu-latest + env: + # The render step draws every block and item in the Maven JVM, so give it room for the renderer's context. + MAVEN_OPTS: -Xmx4g + steps: - name: Checkout code uses: actions/checkout@v7 with: fetch-depth: 0 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v6.0.1 with: java-version: '25' distribution: 'temurin' cache: maven - - name: Set up .NET SDK - if: inputs.pipeline == 'full' || inputs.pipeline == 'render-only' - uses: actions/setup-dotnet@v6 - with: - dotnet-version: '10.0.x' - - name: Build Tooling Module run: mvn -pl tooling -am compile -DskipTests -q @@ -187,7 +187,7 @@ jobs: ## Asset Changes $DIFF_TABLE" \ - --base master || echo "PR already exists" + --base "${{ github.ref_name }}" || echo "PR already exists" - name: Commit Directly if: steps.changes.outputs.has_changes == 'true' && inputs.create_pr == false @@ -196,7 +196,7 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add generator/src/main/resources/minecraft/assets/ git commit -m "Update spritesheet assets" - git push origin master + git push origin HEAD:"${{ github.ref_name }}" - name: Upload Artifacts if: steps.changes.outputs.has_changes == 'true' diff --git a/README.md b/README.md index 06dfd84..ba0e0e1 100644 --- a/README.md +++ b/README.md @@ -664,31 +664,47 @@ MinecraftImageGenerator/ The project includes a GitHub Actions workflow for generating and updating Minecraft item spritesheets and overlays. This is triggered manually via `workflow_dispatch` and supports: - Downloading Minecraft assets for any version -- Rendering items at configurable sizes (requires .NET 10) +- Rendering every block and item icon at a configurable size - Generating sprite atlases with coordinate metadata - Generating item overlays for colored variants (armor, potions, etc.) -### Configurable tooling outputs and atlas pages +The render step draws the icons in-process with +[asset-renderer](https://github.com/minecraft-library/asset-renderer), an Apache-2.0 Java library that reads +the client JAR the download step cached. It needs nothing beyond the JDK, and the tooling pins it to a commit +in `tooling/pom.xml` because the library has no tagged releases. -`MinecraftAssetTool` accepts `--work-dir=`, `--output-dir=` and -`--renderer-dir=`. Set these separately for each Minecraft version when -calling the tooling from another build system. The same paths can be set with -`generator.tooling.workDir`, `generator.tooling.outputDir` and -`generator.tooling.rendererDir` JVM system properties. Existing default paths remain unchanged. +### Configurable tooling outputs and atlas pages -`--size` controls the rendered image size and the default item-atlas tile size. -`--tile-size` overrides just the tile size. `--atlas-width`, `--atlas-height` -and `--padding` control page bounds and transparent gutters. Standalone -`SpritesheetGenerator` supports these atlas options and `--input`. +By default the tooling keeps its scratch files under `tooling/build` and writes +spritesheets and JSON into `generator/src/main/resources/minecraft/assets`. +`MinecraftAssetTool`, `SpritesheetGenerator` and `OverlayGenerator` all accept +`--work-dir=` and `--output-dir=` to move those, which is handy when +another build drives the tooling once per Minecraft version. The same two +settings can be given as the JVM system properties `generator.tooling.workDir` +and `generator.tooling.outputDir`, which is the way to pass them through the +Maven profiles. A flag wins over a property. + +`--size` sets the render size only. Atlas tiles stay at 256px unless you pass +`--tile-size`. `--atlas-width`, `--atlas-height` and `--padding` bound each page +and add a transparent gutter around every tile. All of these are checked before +any download or render starts, so a bad value fails immediately. The overlay +generator reads its definitions from a file bundled with the tooling; pass +`--overlay-definitions=` to use your own instead. ```text -MinecraftAssetTool --download --render --spritesheet --no-clean --version=26.3 +MinecraftAssetTool --download --render --spritesheet --version=26.3 --work-dir=build/26.3/work --output-dir=build/26.3/generated - --size=64 --atlas-width=2048 --atlas-height=2048 --padding=1 + --tile-size=64 --atlas-width=2048 --atlas-height=2048 --padding=1 ``` -The first page keeps its original PNG filename; additional pages use `-1`, -`-2`, etc. Coordinate JSON remains an array with `name`, `x`, `y`, and `size`, -plus `page`, `atlas` (PNG filename), `atlasWidth`, and `atlasHeight` for each entry. -Paging and padding are opt-in. The generator library's bundled single-page -resources retain their existing default layout. +When the tiles do not fit on one page, the first page keeps its file name and +later pages get `-1`, `-2` and so on before the extension. Every coordinates +entry carries `page` and `atlas` (the PNG file name) alongside `name`, `x`, `y` +and `size`. Numbered pages left behind by an earlier, larger run are deleted. +The generator library reads the `atlas` field, so a paged atlas placed in its +resources loads without any other change. + +The tool deletes `tooling/build` at the end of a run, as it always has. A work +directory you chose yourself is kept unless you pass `--clean`, and `--no-clean` +keeps the default one too. Cleanup refuses to run when the work directory +contains the current directory or the output directory. diff --git a/generator/pom.xml b/generator/pom.xml index ddd7f36..a23d754 100644 --- a/generator/pom.xml +++ b/generator/pom.xml @@ -171,6 +171,14 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + + -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar + + io.github.git-commit-id git-commit-id-maven-plugin diff --git a/generator/src/main/java/net/aerh/imagegenerator/spritesheet/Spritesheet.java b/generator/src/main/java/net/aerh/imagegenerator/spritesheet/Spritesheet.java index 6f30ce9..152ade1 100644 --- a/generator/src/main/java/net/aerh/imagegenerator/spritesheet/Spritesheet.java +++ b/generator/src/main/java/net/aerh/imagegenerator/spritesheet/Spritesheet.java @@ -1,86 +1,58 @@ package net.aerh.imagegenerator.spritesheet; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; import net.hypixel.nerdbot.marmalade.functional.Pair; -import javax.imageio.ImageIO; import java.awt.image.BufferedImage; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; -@Slf4j +/** + * Static facade over a single {@link SpritesheetLoader} for the bundled vanilla texture atlas under + * {@code /minecraft/assets}. + *

+ * The loader is created when this class is first used and decodes atlas pages lazily, so the first call that touches + * a page pays for decoding it. Use {@link SpritesheetLoader} directly to load an atlas from another resource path. + */ @UtilityClass public class Spritesheet { - private static final Map IMAGE_MAP = new HashMap<>(); - private static final String ATLAS_PATH = "/minecraft/assets/spritesheets/minecraft_texture_atlas.png"; - private static final String COORDINATES_PATH = "/minecraft/assets/json/atlas_coordinates.json"; - private static final Gson GSON = new Gson(); - private static BufferedImage textureAtlas; - - static { - try (InputStream atlasStream = Spritesheet.class.getResourceAsStream(ATLAS_PATH)) { - if (atlasStream == null) { - throw new IOException("Texture atlas image not found: " + ATLAS_PATH); - } - // Load the texture atlas image - log.info("Loading texture atlas image"); - textureAtlas = ImageIO.read(atlasStream); - log.info("Loaded texture atlas image (size: {}x{})", textureAtlas.getWidth(), textureAtlas.getHeight()); - } catch (IOException exception) { - log.error("Failed to load texture atlas image", exception); - } - - try (InputStream coordinatesStream = Spritesheet.class.getResourceAsStream(COORDINATES_PATH)) { - if (coordinatesStream == null) { - throw new IOException("Texture atlas coordinates file not found: " + COORDINATES_PATH); - } - log.info("Loading texture atlas coordinates from JSON file"); - - // Load the texture atlas coordinates - JsonArray jsonCoordinates = GSON.fromJson(new InputStreamReader(coordinatesStream), JsonArray.class); - - for (JsonElement jsonElement : jsonCoordinates) { - JsonObject itemData = jsonElement.getAsJsonObject(); - String name = itemData.get("name").getAsString(); - int x = itemData.get("x").getAsInt(); - int y = itemData.get("y").getAsInt(); - int size = itemData.get("size").getAsInt(); - BufferedImage image = textureAtlas.getSubimage(x, y, size, size); - - log.debug("Loaded texture: {} at ({}, {}) with size {}x{}", name, x, y, size, size); - IMAGE_MAP.put(name, image); - } - - log.info("Finished loading texture atlas coordinates from JSON file ({} textures loaded)", IMAGE_MAP.size()); - } catch (IOException exception) { - log.error("Failed to load texture atlas coordinates", exception); - } - } - - + private static final SpritesheetLoader LOADER = new SpritesheetLoader(); + + /** + * Get a texture by name. + * + * @param textureId Texture name as listed in the coordinates file + * + * @return The texture, or null if the name is null or not listed + * + * @see SpritesheetLoader#getTexture(String) + */ public static BufferedImage getTexture(String textureId) { - return IMAGE_MAP.get(textureId); + return LOADER.getTexture(textureId); } + /** + * Find every texture whose name contains {@code textureId}. The match is case-sensitive. + * + * @param textureId Substring to look for in texture names + * + * @return Matching textures paired with their names; empty if nothing matches + * + * @see SpritesheetLoader#searchForTexture(String) + */ public static List> searchForTexture(String textureId) { - return IMAGE_MAP.entrySet().stream() - .filter(entry -> entry.getKey().contains(textureId)) - .map(entry -> Pair.of(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()); + return LOADER.searchForTexture(textureId); } + /** + * Get every texture keyed by name. This decodes every atlas page. + * + * @return An unmodifiable map of texture name to texture + * + * @see SpritesheetLoader#getImageMap() + */ public static Map getImageMap() { - return IMAGE_MAP; + return LOADER.getImageMap(); } } diff --git a/generator/src/main/java/net/aerh/imagegenerator/spritesheet/SpritesheetLoader.java b/generator/src/main/java/net/aerh/imagegenerator/spritesheet/SpritesheetLoader.java new file mode 100644 index 0000000..0cb2f67 --- /dev/null +++ b/generator/src/main/java/net/aerh/imagegenerator/spritesheet/SpritesheetLoader.java @@ -0,0 +1,277 @@ +package net.aerh.imagegenerator.spritesheet; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import lombok.extern.slf4j.Slf4j; +import net.aerh.imagegenerator.exception.GeneratorException; +import net.hypixel.nerdbot.marmalade.functional.Pair; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.awt.image.RasterFormatException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Loads item textures from one or more texture atlas pages. + *

+ * The coordinates file {@code /json/atlas_coordinates.json} is parsed on first use. Each entry names the atlas + * page it lives on through its {@code atlas} field; entries without one belong to {@code minecraft_texture_atlas.png}. + * Atlas pages under {@code /spritesheets/} are decoded lazily, the first time a texture on that page is + * requested, and are then kept in memory. Instances are safe to share between threads. + */ +@Slf4j +public class SpritesheetLoader { + + private static final String DEFAULT_RESOURCE_BASE_PATH = "/minecraft/assets"; + private static final String COORDINATES_PATH = "/json/atlas_coordinates.json"; + private static final String SPRITESHEETS_PATH = "/spritesheets/"; + /** Atlas file used by coordinate entries that predate paging and therefore carry no {@code atlas} field. */ + private static final String DEFAULT_ATLAS_FILE_NAME = "minecraft_texture_atlas.png"; + + /** + * One entry of the coordinates file: where a named texture sits on which atlas page. + */ + private record AtlasEntry(String name, int x, int y, int size, String atlasFileName) { + } + + private final String resourceBasePath; + private final Gson gson = new Gson(); + /** Decoded atlas pages keyed by atlas file name. */ + private final Map pages = new ConcurrentHashMap<>(); + /** Sliced textures keyed by texture name. */ + private final Map textures = new ConcurrentHashMap<>(); + /** Parsed coordinates keyed by texture name, in file order. Null until {@link #load()} has run. */ + private volatile Map entries; + + public SpritesheetLoader() { + this(DEFAULT_RESOURCE_BASE_PATH); + } + + /** + * Constructor to specify a custom resource base path. + * + * @param resourceBasePath Base path for resources, for example {@code /minecraft/assets} + */ + public SpritesheetLoader(String resourceBasePath) { + this.resourceBasePath = resourceBasePath; + } + + /** + * Parse the coordinates file. Safe to call more than once and from several threads; only the first call does + * any work. Atlas pages are not decoded here. + * + * @throws GeneratorException if the coordinates file is missing, unreadable or malformed + */ + public synchronized void load() { + if (entries != null) { + return; + } + + String path = resourceBasePath + COORDINATES_PATH; + log.debug("Loading texture atlas coordinates from {}", path); + JsonElement root; + + try (InputStream stream = openResource(path, "Texture atlas coordinates file")) { + root = gson.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), JsonElement.class); + } catch (IOException | JsonParseException e) { + throw new GeneratorException("Failed to read texture atlas coordinates file: " + path, e); + } + + Map parsed = parseEntries(root, path); + entries = Collections.unmodifiableMap(parsed); + log.info("Loaded {} texture atlas coordinates from {}", parsed.size(), path); + } + + /** + * Get a texture by name, decoding its atlas page if this is the first texture requested from that page. Repeated + * calls for the same name return the same instance. + * + * @param name Texture name as listed in the coordinates file + * + * @return The texture, or null if the name is null or not listed + * + * @throws GeneratorException if the coordinates file or the atlas page cannot be read, or the entry lies outside + * its page + */ + public BufferedImage getTexture(String name) { + if (name == null) { + return null; + } + + AtlasEntry entry = entries().get(name); + if (entry == null) { + return null; + } + + // Decode the page before touching the textures map, so a slow decode only blocks callers that need + // this page rather than everyone whose name happens to share a bin. + BufferedImage page = pages.computeIfAbsent(entry.atlasFileName(), this::decodePage); + return textures.computeIfAbsent(name, key -> slice(page, entry)); + } + + /** + * Find every texture whose name contains {@code query}. The match is case-sensitive. Only the atlas pages that + * hold a match are decoded. + * + * @param query Substring to look for in texture names + * + * @return Matching textures paired with their names, in coordinates-file order; empty if nothing matches + * + * @throws GeneratorException if the coordinates file or an atlas page cannot be read + */ + public List> searchForTexture(String query) { + Objects.requireNonNull(query, "query"); + return entries().keySet().stream() + .filter(name -> name.contains(query)) + .map(name -> Pair.of(name, getTexture(name))) + .toList(); + } + + /** + * Get every texture keyed by name. This decodes every atlas page, so prefer {@link #getTexture(String)} when only + * some textures are needed. + * + * @return An unmodifiable map of texture name to texture, in coordinates-file order + * + * @throws GeneratorException if the coordinates file or an atlas page cannot be read + */ + public Map getImageMap() { + Map result = new LinkedHashMap<>(); + for (String name : entries().keySet()) { + result.put(name, getTexture(name)); + } + return Collections.unmodifiableMap(result); + } + + /** + * Protected method for resource loading (can be overridden for testing). + * + * @param path Resource path + * + * @return InputStream or null if not found + */ + protected InputStream getResource(String path) { + return getClass().getResourceAsStream(path); + } + + private Map entries() { + Map loaded = entries; + if (loaded == null) { + load(); + loaded = entries; + } + return loaded; + } + + private Map parseEntries(JsonElement root, String path) { + if (root == null || !root.isJsonArray()) { + throw new GeneratorException("Texture atlas coordinates file " + path + " must contain a JSON array"); + } + + Map parsed = new LinkedHashMap<>(); + JsonArray array = root.getAsJsonArray(); + + for (int index = 0; index < array.size(); index++) { + AtlasEntry entry = parseEntry(array.get(index), index, path); + AtlasEntry previous = parsed.put(entry.name(), entry); + if (previous != null) { + log.warn("Duplicate texture name '{}' in {}; keeping the last entry", entry.name(), path); + } + } + + return parsed; + } + + private AtlasEntry parseEntry(JsonElement element, int index, String path) { + if (!element.isJsonObject()) { + throw new GeneratorException(entryDescription(index, path) + " is not a JSON object"); + } + + JsonObject object = element.getAsJsonObject(); + JsonElement atlas = object.get("atlas"); + + try { + return new AtlasEntry( + requiredField(object, "name", index, path).getAsString(), + requiredField(object, "x", index, path).getAsInt(), + requiredField(object, "y", index, path).getAsInt(), + requiredField(object, "size", index, path).getAsInt(), + atlas == null || atlas.isJsonNull() ? DEFAULT_ATLAS_FILE_NAME : atlas.getAsString() + ); + } catch (IllegalStateException | NumberFormatException | UnsupportedOperationException e) { + throw new GeneratorException(entryDescription(index, path) + " has a field of the wrong type", e); + } + } + + private static JsonElement requiredField(JsonObject object, String field, int index, String path) { + JsonElement value = object.get(field); + if (value == null || value.isJsonNull()) { + throw new GeneratorException(entryDescription(index, path) + " is missing field '" + field + "'"); + } + return value; + } + + private static String entryDescription(int index, String path) { + return "Texture atlas coordinates file " + path + " entry " + index; + } + + private BufferedImage slice(BufferedImage page, AtlasEntry entry) { + try { + return page.getSubimage(entry.x(), entry.y(), entry.size(), entry.size()); + } catch (RasterFormatException e) { + throw new GeneratorException("Texture '" + entry.name() + "' at (" + entry.x() + ", " + entry.y() + + ") size " + entry.size() + " lies outside texture atlas page " + pagePath(entry.atlasFileName()) + + " (" + page.getWidth() + "x" + page.getHeight() + ")", e); + } + } + + private BufferedImage decodePage(String atlasFileName) { + String path = pagePath(atlasFileName); + log.debug("Decoding texture atlas page {}", path); + + try (InputStream stream = openResource(path, "Texture atlas page")) { + BufferedImage page = ImageIO.read(stream); + if (page == null) { + throw new GeneratorException("Texture atlas page is not a readable image: " + path); + } + log.info("Decoded texture atlas page {} ({}x{})", path, page.getWidth(), page.getHeight()); + return page; + } catch (IOException e) { + throw new GeneratorException("Failed to decode texture atlas page: " + path, e); + } + } + + private String pagePath(String atlasFileName) { + return resourceBasePath + SPRITESHEETS_PATH + atlasFileName; + } + + /** + * Open a resource, failing loudly when it does not exist. + * + * @param path Resource path + * @param description What the resource is, used in the error message + * + * @return The open stream; the caller closes it + * + * @throws GeneratorException if the resource does not exist + */ + private InputStream openResource(String path, String description) { + InputStream stream = getResource(path); + if (stream == null) { + throw new GeneratorException(description + " not found: " + path); + } + return stream; + } +} diff --git a/generator/src/main/resources/minecraft/assets/json/atlas_coordinates.json b/generator/src/main/resources/minecraft/assets/json/atlas_coordinates.json index 4dd3ecc..f48eadf 100644 --- a/generator/src/main/resources/minecraft/assets/json/atlas_coordinates.json +++ b/generator/src/main/resources/minecraft/assets/json/atlas_coordinates.json @@ -1,11060 +1,13258 @@ [ { - "name": "acacia_boat", + "name": "abandoned_camp_map", "x": 0, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_button", + "name": "acacia_boat", "x": 256, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_chest_boat", + "name": "acacia_button", "x": 512, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_door", + "name": "acacia_chest_boat", "x": 768, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_fence", + "name": "acacia_door", "x": 1024, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_fence_gate", + "name": "acacia_fence", "x": 1280, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_hanging_sign", + "name": "acacia_fence_gate", "x": 1536, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_leaves", + "name": "acacia_hanging_sign", "x": 1792, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_log", + "name": "acacia_leaves", "x": 2048, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_planks", + "name": "acacia_log", "x": 2304, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_pressure_plate", + "name": "acacia_planks", "x": 2560, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_sapling", + "name": "acacia_pressure_plate", "x": 2816, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_shelf", + "name": "acacia_sapling", "x": 3072, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_sign", + "name": "acacia_shelf", "x": 3328, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_slab", + "name": "acacia_sign", "x": 3584, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_stairs", + "name": "acacia_slab", "x": 3840, "y": 0, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_trapdoor", + "name": "acacia_stairs", "x": 0, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "acacia_wall_sign", + "name": "acacia_trapdoor", "x": 256, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { "name": "acacia_wood", "x": 512, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { "name": "activator_rail", "x": 768, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { "name": "allay_spawn_egg", "x": 1024, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { "name": "allium", "x": 1280, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { "name": "amethyst_block", "x": 1536, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "amethyst_bud", + "name": "amethyst_cluster", "x": 1792, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "amethyst_cluster", + "name": "amethyst_shard", "x": 2048, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "amethyst_shard", + "name": "ancient_debris", "x": 2304, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ancient_debris", + "name": "andesite", "x": 2560, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "andesite", + "name": "andesite_slab", "x": 2816, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "andesite_slab", + "name": "andesite_stairs", "x": 3072, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "andesite_stairs", + "name": "andesite_wall", "x": 3328, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "andesite_wall", + "name": "angler_pottery_sherd", "x": 3584, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "angler_pottery_sherd", + "name": "anvil", "x": 3840, "y": 256, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "anvil", + "name": "apple", "x": 0, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "apple", + "name": "archer_pottery_sherd", "x": 256, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "archer_pottery_sherd", + "name": "armadillo_scute", "x": 512, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "armadillo_scute", + "name": "armadillo_spawn_egg", "x": 768, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "armadillo_spawn_egg", + "name": "armor_stand", "x": 1024, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "armor_stand", + "name": "arms_up_pottery_sherd", "x": 1280, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "arms_up_pottery_sherd", + "name": "arrow", "x": 1536, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "arrow", + "name": "axolotl_bucket", "x": 1792, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "attached_melon_stem", + "name": "axolotl_spawn_egg", "x": 2048, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "attached_pumpkin_stem", + "name": "azalea", "x": 2304, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "axolotl_bucket", + "name": "azalea_leaves", "x": 2560, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "axolotl_spawn_egg", + "name": "azure_bluet", "x": 2816, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "azalea", + "name": "baked_potato", "x": 3072, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "azalea_leaves", + "name": "bamboo", "x": 3328, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "azure_bluet", + "name": "bamboo_block", "x": 3584, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "baked_potato", + "name": "bamboo_button", "x": 3840, "y": 512, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo", + "name": "bamboo_chest_raft", "x": 0, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_block", + "name": "bamboo_door", "x": 256, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_button", + "name": "bamboo_fence", "x": 512, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_chest_raft", + "name": "bamboo_fence_gate", "x": 768, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_door", + "name": "bamboo_hanging_sign", "x": 1024, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_fence", + "name": "bamboo_mosaic", "x": 1280, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_fence_gate", + "name": "bamboo_mosaic_slab", "x": 1536, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_hanging_sign", + "name": "bamboo_mosaic_stairs", "x": 1792, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_mosaic", + "name": "bamboo_planks", "x": 2048, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_mosaic_slab", + "name": "bamboo_pressure_plate", "x": 2304, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_mosaic_stairs", + "name": "bamboo_raft", "x": 2560, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_planks", + "name": "bamboo_shelf", "x": 2816, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_pressure_plate", + "name": "bamboo_sign", "x": 3072, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_raft", + "name": "bamboo_slab", "x": 3328, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_sapling", + "name": "bamboo_stairs", "x": 3584, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_shelf", + "name": "bamboo_trapdoor", "x": 3840, "y": 768, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_sign", + "name": "barrel", "x": 0, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_slab", + "name": "barrier", "x": 256, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_stairs", + "name": "basalt", "x": 512, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bamboo_trapdoor", + "name": "bat_spawn_egg", "x": 768, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "barrel", + "name": "beacon", "x": 1024, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "barrier", + "name": "bedrock", "x": 1280, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "basalt", + "name": "bee_nest", "x": 1536, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bat_spawn_egg", + "name": "bee_spawn_egg", "x": 1792, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beacon", + "name": "beef", "x": 2048, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bedrock", + "name": "beehive", "x": 2304, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bee_nest", + "name": "beetroot", "x": 2560, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bee_spawn_egg", + "name": "beetroot_seeds", "x": 2816, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beef", + "name": "beetroot_soup", "x": 3072, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beehive", + "name": "bell", "x": 3328, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beetroot", + "name": "big_dripleaf", "x": 3584, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beetroot_seeds", + "name": "birch_boat", "x": 3840, "y": 1024, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beetroot_soup", + "name": "birch_button", "x": 0, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "beetroots", + "name": "birch_chest_boat", "x": 256, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bell", + "name": "birch_door", "x": 512, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "big_dripleaf", + "name": "birch_fence", "x": 768, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "big_dripleaf_stem", + "name": "birch_fence_gate", "x": 1024, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_boat", + "name": "birch_hanging_sign", "x": 1280, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_button", + "name": "birch_leaves", "x": 1536, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_chest_boat", + "name": "birch_log", "x": 1792, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_door", + "name": "birch_planks", "x": 2048, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_fence", + "name": "birch_pressure_plate", "x": 2304, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_fence_gate", + "name": "birch_sapling", "x": 2560, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_hanging_sign", + "name": "birch_shelf", "x": 2816, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_leaves", + "name": "birch_sign", "x": 3072, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_log", + "name": "birch_slab", "x": 3328, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_planks", + "name": "birch_stairs", "x": 3584, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_pressure_plate", + "name": "birch_trapdoor", "x": 3840, "y": 1280, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_sapling", + "name": "birch_wood", "x": 0, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_shelf", + "name": "black_banner", "x": 256, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_sign", + "name": "black_bed", "x": 512, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_slab", + "name": "black_bundle", "x": 768, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_stairs", + "name": "black_candle", "x": 1024, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_trapdoor", + "name": "black_carpet", "x": 1280, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_wall_sign", + "name": "black_concrete", "x": 1536, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "birch_wood", + "name": "black_concrete_powder", "x": 1792, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_banner", + "name": "black_concrete_slab", "x": 2048, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_bed", + "name": "black_concrete_stairs", "x": 2304, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_bundle", + "name": "black_cushion", "x": 2560, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_bundle_open_back", + "name": "black_dye", "x": 2816, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_bundle_open_front", + "name": "black_glazed_terracotta", "x": 3072, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_candle", + "name": "black_harness", "x": 3328, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_candle_cake", + "name": "black_shulker_box", "x": 3584, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_carpet", + "name": "black_stained_glass", "x": 3840, "y": 1536, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_concrete", + "name": "black_stained_glass_pane", "x": 0, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_concrete_powder", + "name": "black_terracotta", "x": 256, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_dye", + "name": "black_wool", "x": 512, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_glazed_terracotta", + "name": "black_wool_slab", "x": 768, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_harness", + "name": "black_wool_stairs", "x": 1024, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_shulker_box", + "name": "blackstone", "x": 1280, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_stained_glass", + "name": "blackstone_slab", "x": 1536, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_stained_glass_pane", + "name": "blackstone_stairs", "x": 1792, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_terracotta", + "name": "blackstone_wall", "x": 2048, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_wall_banner", + "name": "blade_pottery_sherd", "x": 2304, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "black_wool", + "name": "blast_furnace", "x": 2560, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blackstone", + "name": "blaze_powder", "x": 2816, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blackstone_slab", + "name": "blaze_rod", "x": 3072, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blackstone_stairs", + "name": "blaze_spawn_egg", "x": 3328, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blackstone_wall", + "name": "blue_banner", "x": 3584, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blade_pottery_sherd", + "name": "blue_bed", "x": 3840, "y": 1792, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blast_furnace", + "name": "blue_bundle", "x": 0, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blaze_powder", + "name": "blue_candle", "x": 256, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blaze_rod", + "name": "blue_carpet", "x": 512, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blaze_spawn_egg", + "name": "blue_concrete", "x": 768, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_banner", + "name": "blue_concrete_powder", "x": 1024, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_bed", + "name": "blue_concrete_slab", "x": 1280, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_bundle", + "name": "blue_concrete_stairs", "x": 1536, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_bundle_open_back", + "name": "blue_cushion", "x": 1792, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_bundle_open_front", + "name": "blue_dye", "x": 2048, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_candle", + "name": "blue_egg", "x": 2304, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_candle_cake", + "name": "blue_glazed_terracotta", "x": 2560, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_carpet", + "name": "blue_harness", "x": 2816, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_concrete", + "name": "blue_ice", "x": 3072, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_concrete_powder", + "name": "blue_orchid", "x": 3328, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_dye", + "name": "blue_shulker_box", "x": 3584, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_egg", + "name": "blue_stained_glass", "x": 3840, "y": 2048, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_glazed_terracotta", + "name": "blue_stained_glass_pane", "x": 0, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_harness", + "name": "blue_terracotta", "x": 256, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_ice", + "name": "blue_wool", "x": 512, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_orchid", + "name": "blue_wool_slab", "x": 768, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_shulker_box", + "name": "blue_wool_stairs", "x": 1024, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_stained_glass", + "name": "bogged_spawn_egg", "x": 1280, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_stained_glass_pane", + "name": "bolt_armor_trim_smithing_template", "x": 1536, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_terracotta", + "name": "bone", "x": 1792, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_wall_banner", + "name": "bone_block", "x": 2048, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "blue_wool", + "name": "bone_meal", "x": 2304, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bogged_spawn_egg", + "name": "book", "x": 2560, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bolt_armor_trim_smithing_template", + "name": "bookshelf", "x": 2816, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bone", + "name": "bordure_indented_banner_pattern", "x": 3072, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bone_block", + "name": "bow", "x": 3328, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bone_meal", + "name": "bowl", "x": 3584, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "book", + "name": "brain_coral", "x": 3840, "y": 2304, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bookshelf", + "name": "brain_coral_block", "x": 0, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bordure_indented_banner_pattern", + "name": "brain_coral_fan", "x": 256, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bow", + "name": "bread", "x": 512, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bow_pulling_0", + "name": "breeze_rod", "x": 768, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bow_pulling_1", + "name": "breeze_spawn_egg", "x": 1024, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bow_pulling_2", + "name": "brewer_pottery_sherd", "x": 1280, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bowl", + "name": "brewing_stand", "x": 1536, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brain_coral", + "name": "brick", "x": 1792, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brain_coral_block", + "name": "brick_slab", "x": 2048, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brain_coral_fan", + "name": "brick_stairs", "x": 2304, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brain_coral_wall_fan", + "name": "brick_wall", "x": 2560, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bread", + "name": "bricks", "x": 2816, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "breeze_rod", + "name": "brown_banner", "x": 3072, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "breeze_spawn_egg", + "name": "brown_bed", "x": 3328, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brewer_pottery_sherd", + "name": "brown_bundle", "x": 3584, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brewing_stand", + "name": "brown_candle", "x": 3840, "y": 2560, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brick", + "name": "brown_carpet", "x": 0, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brick_slab", + "name": "brown_concrete", "x": 256, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brick_stairs", + "name": "brown_concrete_powder", "x": 512, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brick_wall", + "name": "brown_concrete_slab", "x": 768, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bricks", + "name": "brown_concrete_stairs", "x": 1024, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_banner", + "name": "brown_cushion", "x": 1280, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_bed", + "name": "brown_dye", "x": 1536, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_bundle", + "name": "brown_egg", "x": 1792, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_bundle_open_back", + "name": "brown_glazed_terracotta", "x": 2048, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_bundle_open_front", + "name": "brown_harness", "x": 2304, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_candle", + "name": "brown_mushroom", "x": 2560, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_candle_cake", + "name": "brown_mushroom_block", "x": 2816, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_carpet", + "name": "brown_shulker_box", "x": 3072, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_concrete", + "name": "brown_stained_glass", "x": 3328, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_concrete_powder", + "name": "brown_stained_glass_pane", "x": 3584, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_dye", + "name": "brown_terracotta", "x": 3840, "y": 2816, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_egg", + "name": "brown_wool", "x": 0, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_glazed_terracotta", + "name": "brown_wool_slab", "x": 256, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_harness", + "name": "brown_wool_stairs", "x": 512, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_mushroom", + "name": "brush", "x": 768, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_mushroom_block", + "name": "bubble_coral", "x": 1024, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_shulker_box", + "name": "bubble_coral_block", "x": 1280, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_stained_glass", + "name": "bubble_coral_fan", "x": 1536, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_stained_glass_pane", + "name": "bucket", "x": 1792, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_terracotta", + "name": "budding_amethyst", "x": 2048, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_wall_banner", + "name": "bundle", "x": 2304, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brown_wool", + "name": "buried_ancient_city_map", "x": 2560, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brush", + "name": "buried_mineshaft_map", "x": 2816, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brush_brushing_0", + "name": "buried_treasure_map", "x": 3072, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brush_brushing_1", + "name": "buried_trial_chambers_map", "x": 3328, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "brush_brushing_2", + "name": "burn_pottery_sherd", "x": 3584, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bubble_coral", + "name": "bush", "x": 3840, "y": 3072, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bubble_coral_block", + "name": "cactus", "x": 0, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bubble_coral_fan", + "name": "cactus_flower", "x": 256, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bubble_coral_wall_fan", + "name": "cake", "x": 512, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bucket", + "name": "calcite", "x": 768, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "budding_amethyst", + "name": "calibrated_sculk_sensor", "x": 1024, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bundle", + "name": "camel_husk_spawn_egg", "x": 1280, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bundle_open_back", + "name": "camel_spawn_egg", "x": 1536, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bundle_open_front", + "name": "campfire", "x": 1792, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "burn_pottery_sherd", + "name": "candle", "x": 2048, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "bush", + "name": "carrot", "x": 2304, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cactus", + "name": "carrot_on_a_stick", "x": 2560, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cactus_flower", + "name": "cartography_table", "x": 2816, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cake", + "name": "carved_pumpkin", "x": 3072, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "calcite", + "name": "cat_spawn_egg", "x": 3328, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "calibrated_sculk_sensor", + "name": "cauldron", "x": 3584, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "camel_husk_spawn_egg", + "name": "cave_spider_spawn_egg", "x": 3840, "y": 3328, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "camel_spawn_egg", + "name": "chain_command_block", "x": 0, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "campfire", + "name": "chainmail_boots", "x": 256, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "candle", + "name": "chainmail_chestplate", "x": 512, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "candle_cake", + "name": "chainmail_helmet", "x": 768, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "carrot", + "name": "chainmail_leggings", "x": 1024, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "carrot_on_a_stick", + "name": "charcoal", "x": 1280, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "carrots", + "name": "cherry_boat", "x": 1536, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cartography_table", + "name": "cherry_button", "x": 1792, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "carved_pumpkin", + "name": "cherry_chest_boat", "x": 2048, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cat_spawn_egg", + "name": "cherry_door", "x": 2304, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cauldron", + "name": "cherry_fence", "x": 2560, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cave_spider_spawn_egg", + "name": "cherry_fence_gate", "x": 2816, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cave_vines", + "name": "cherry_hanging_sign", "x": 3072, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cave_vines_plant", + "name": "cherry_leaves", "x": 3328, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chain_command_block", + "name": "cherry_log", "x": 3584, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chainmail_boots", + "name": "cherry_planks", "x": 3840, "y": 3584, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chainmail_chestplate", + "name": "cherry_pressure_plate", "x": 0, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chainmail_helmet", + "name": "cherry_sapling", "x": 256, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chainmail_leggings", + "name": "cherry_shelf", "x": 512, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "charcoal", + "name": "cherry_sign", "x": 768, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_boat", + "name": "cherry_slab", "x": 1024, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_button", + "name": "cherry_stairs", "x": 1280, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_chest_boat", + "name": "cherry_trapdoor", "x": 1536, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_door", + "name": "cherry_wood", "x": 1792, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_fence", + "name": "chest", "x": 2048, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_fence_gate", + "name": "chest_minecart", "x": 2304, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_hanging_sign", + "name": "chicken", "x": 2560, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_leaves", + "name": "chicken_spawn_egg", "x": 2816, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_log", + "name": "chipped_anvil", "x": 3072, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_planks", + "name": "chiseled_bookshelf", "x": 3328, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_pressure_plate", + "name": "chiseled_cinnabar", "x": 3584, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_sapling", + "name": "chiseled_copper", "x": 3840, "y": 3840, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_shelf", + "name": "chiseled_deepslate", "x": 0, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_sign", + "name": "chiseled_nether_bricks", "x": 256, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_slab", + "name": "chiseled_polished_blackstone", "x": 512, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_stairs", + "name": "chiseled_quartz_block", "x": 768, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_trapdoor", + "name": "chiseled_red_sandstone", "x": 1024, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cherry_wood", + "name": "chiseled_resin_bricks", "x": 1280, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chest", + "name": "chiseled_sandstone", "x": 1536, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chest_minecart", + "name": "chiseled_stone_bricks", "x": 1792, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chicken", + "name": "chiseled_sulfur", "x": 2048, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chicken_spawn_egg", + "name": "chiseled_tuff", "x": 2304, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chipped_anvil", + "name": "chiseled_tuff_bricks", "x": 2560, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_bookshelf", + "name": "chorus_flower", "x": 2816, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_copper", + "name": "chorus_fruit", "x": 3072, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_deepslate", + "name": "chorus_plant", "x": 3328, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_nether_bricks", + "name": "cinnabar", "x": 3584, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_polished_blackstone", + "name": "cinnabar_brick_slab", "x": 3840, "y": 4096, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_quartz_block", + "name": "cinnabar_brick_stairs", "x": 0, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_red_sandstone", + "name": "cinnabar_brick_wall", "x": 256, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_resin_bricks", + "name": "cinnabar_bricks", "x": 512, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_sandstone", + "name": "cinnabar_slab", "x": 768, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_stone_bricks", + "name": "cinnabar_stairs", "x": 1024, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_tuff", + "name": "cinnabar_wall", "x": 1280, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chiseled_tuff_bricks", + "name": "clay", "x": 1536, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chorus_flower", + "name": "clay_ball", "x": 1792, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chorus_fruit", + "name": "clock", "x": 2048, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "chorus_plant", + "name": "closed_eyeblossom", "x": 2304, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clay", + "name": "coal", "x": 2560, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clay_ball", + "name": "coal_block", "x": 2816, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock", + "name": "coal_ore", "x": 3072, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_00", + "name": "coarse_dirt", "x": 3328, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_01", + "name": "coast_armor_trim_smithing_template", "x": 3584, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_02", + "name": "cobbled_deepslate", "x": 3840, "y": 4352, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_03", + "name": "cobbled_deepslate_slab", "x": 0, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_04", + "name": "cobbled_deepslate_stairs", "x": 256, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_05", + "name": "cobbled_deepslate_wall", "x": 512, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_06", + "name": "cobblestone", "x": 768, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_07", + "name": "cobblestone_slab", "x": 1024, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_08", + "name": "cobblestone_stairs", "x": 1280, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_09", + "name": "cobblestone_wall", "x": 1536, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_10", + "name": "cobweb", "x": 1792, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_11", + "name": "cocoa_beans", "x": 2048, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_12", + "name": "cod", "x": 2304, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_13", + "name": "cod_bucket", "x": 2560, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_14", + "name": "cod_spawn_egg", "x": 2816, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_15", + "name": "command_block", "x": 3072, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_16", + "name": "command_block_minecart", "x": 3328, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_17", + "name": "comparator", "x": 3584, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_18", + "name": "compass", "x": 3840, "y": 4608, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_19", + "name": "composter", "x": 0, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_20", + "name": "conduit", "x": 256, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_21", + "name": "cooked_beef", "x": 512, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_22", + "name": "cooked_chicken", "x": 768, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_23", + "name": "cooked_cod", "x": 1024, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_24", + "name": "cooked_mutton", "x": 1280, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_25", + "name": "cooked_porkchop", "x": 1536, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_26", + "name": "cooked_rabbit", "x": 1792, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_27", + "name": "cooked_salmon", "x": 2048, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_28", + "name": "cookie", "x": 2304, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_29", + "name": "copper_axe", "x": 2560, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_30", + "name": "copper_bars", "x": 2816, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_31", + "name": "copper_block", "x": 3072, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_32", + "name": "copper_boots", "x": 3328, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_33", + "name": "copper_bulb", "x": 3584, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_34", + "name": "copper_chain", "x": 3840, "y": 4864, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_35", + "name": "copper_chest", "x": 0, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_36", + "name": "copper_chestplate", "x": 256, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_37", + "name": "copper_door", "x": 512, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_38", + "name": "copper_golem_spawn_egg", "x": 768, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_39", + "name": "copper_golem_statue", "x": 1024, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_40", + "name": "copper_grate", "x": 1280, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_41", + "name": "copper_helmet", "x": 1536, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_42", + "name": "copper_hoe", "x": 1792, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_43", + "name": "copper_horse_armor", "x": 2048, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_44", + "name": "copper_ingot", "x": 2304, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_45", + "name": "copper_lantern", "x": 2560, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_46", + "name": "copper_leggings", "x": 2816, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_47", + "name": "copper_nautilus_armor", "x": 3072, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_48", + "name": "copper_nugget", "x": 3328, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_49", + "name": "copper_ore", "x": 3584, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_50", + "name": "copper_pickaxe", "x": 3840, "y": 5120, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_51", + "name": "copper_shovel", "x": 0, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_52", + "name": "copper_spear", "x": 256, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_53", + "name": "copper_sword", "x": 512, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_54", + "name": "copper_torch", "x": 768, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_55", + "name": "copper_trapdoor", "x": 1024, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_56", + "name": "cornflower", "x": 1280, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_57", + "name": "cow_spawn_egg", "x": 1536, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_58", + "name": "cracked_deepslate_bricks", "x": 1792, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_59", + "name": "cracked_deepslate_tiles", "x": 2048, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_60", + "name": "cracked_nether_bricks", "x": 2304, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_61", + "name": "cracked_polished_blackstone_bricks", "x": 2560, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_62", + "name": "cracked_stone_bricks", "x": 2816, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "clock_63", + "name": "crafter", "x": 3072, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "closed_eyeblossom", + "name": "crafting_table", "x": 3328, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "coal", + "name": "creaking_heart", "x": 3584, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "coal_block", + "name": "creaking_spawn_egg", "x": 3840, "y": 5376, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "coal_ore", + "name": "creeper_banner_pattern", "x": 0, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "coarse_dirt", + "name": "creeper_head", "x": 256, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "coast_armor_trim_smithing_template", + "name": "creeper_spawn_egg", "x": 512, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobbled_deepslate", + "name": "crimson_button", "x": 768, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobbled_deepslate_slab", + "name": "crimson_door", "x": 1024, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobbled_deepslate_stairs", + "name": "crimson_fence", "x": 1280, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobbled_deepslate_wall", + "name": "crimson_fence_gate", "x": 1536, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobblestone", + "name": "crimson_fungus", "x": 1792, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobblestone_slab", + "name": "crimson_hanging_sign", "x": 2048, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobblestone_stairs", + "name": "crimson_hyphae", "x": 2304, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobblestone_wall", + "name": "crimson_nylium", "x": 2560, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cobweb", + "name": "crimson_planks", "x": 2816, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cocoa", + "name": "crimson_pressure_plate", "x": 3072, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cocoa_beans", + "name": "crimson_roots", "x": 3328, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cod", + "name": "crimson_shelf", "x": 3584, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cod_bucket", + "name": "crimson_sign", "x": 3840, "y": 5632, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cod_spawn_egg", + "name": "crimson_slab", "x": 0, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "command_block", + "name": "crimson_stairs", "x": 256, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "command_block_minecart", + "name": "crimson_stem", "x": 512, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "comparator", + "name": "crimson_trapdoor", "x": 768, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass", + "name": "crossbow", "x": 1024, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_00", + "name": "crying_obsidian", "x": 1280, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_01", + "name": "cut_copper", "x": 1536, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_02", + "name": "cut_copper_slab", "x": 1792, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_03", + "name": "cut_copper_stairs", "x": 2048, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_04", + "name": "cut_red_sandstone", "x": 2304, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_05", + "name": "cut_red_sandstone_slab", "x": 2560, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_06", + "name": "cut_sandstone", "x": 2816, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_07", + "name": "cut_sandstone_slab", "x": 3072, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_08", + "name": "cyan_banner", "x": 3328, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_09", + "name": "cyan_bed", "x": 3584, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_10", + "name": "cyan_bundle", "x": 3840, "y": 5888, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_11", + "name": "cyan_candle", "x": 0, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_12", + "name": "cyan_carpet", "x": 256, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_13", + "name": "cyan_concrete", "x": 512, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_14", + "name": "cyan_concrete_powder", "x": 768, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_15", + "name": "cyan_concrete_slab", "x": 1024, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_16", + "name": "cyan_concrete_stairs", "x": 1280, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_17", + "name": "cyan_cushion", "x": 1536, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_18", + "name": "cyan_dye", "x": 1792, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_19", + "name": "cyan_glazed_terracotta", "x": 2048, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_20", + "name": "cyan_harness", "x": 2304, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_21", + "name": "cyan_shulker_box", "x": 2560, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_22", + "name": "cyan_stained_glass", "x": 2816, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_23", + "name": "cyan_stained_glass_pane", "x": 3072, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_24", + "name": "cyan_terracotta", "x": 3328, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_25", + "name": "cyan_wool", "x": 3584, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_26", + "name": "cyan_wool_slab", "x": 3840, "y": 6144, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_27", + "name": "cyan_wool_stairs", "x": 0, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_28", + "name": "damaged_anvil", "x": 256, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_29", + "name": "dandelion", "x": 512, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_30", + "name": "danger_pottery_sherd", "x": 768, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "compass_31", + "name": "dark_oak_boat", "x": 1024, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "composter", + "name": "dark_oak_button", "x": 1280, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "conduit", + "name": "dark_oak_chest_boat", "x": 1536, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_beef", + "name": "dark_oak_door", "x": 1792, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_chicken", + "name": "dark_oak_fence", "x": 2048, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_cod", + "name": "dark_oak_fence_gate", "x": 2304, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_mutton", + "name": "dark_oak_hanging_sign", "x": 2560, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_porkchop", + "name": "dark_oak_leaves", "x": 2816, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_rabbit", + "name": "dark_oak_log", "x": 3072, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cooked_salmon", + "name": "dark_oak_planks", "x": 3328, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cookie", + "name": "dark_oak_pressure_plate", "x": 3584, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_axe", + "name": "dark_oak_sapling", "x": 3840, "y": 6400, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_bars", + "name": "dark_oak_shelf", "x": 0, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_block", + "name": "dark_oak_sign", "x": 256, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_boots", + "name": "dark_oak_slab", "x": 512, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_bulb", + "name": "dark_oak_stairs", "x": 768, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_chain", + "name": "dark_oak_trapdoor", "x": 1024, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_chest", + "name": "dark_oak_wood", "x": 1280, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_chestplate", + "name": "dark_prismarine", "x": 1536, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_door", + "name": "dark_prismarine_slab", "x": 1792, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_golem_spawn_egg", + "name": "dark_prismarine_stairs", "x": 2048, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_golem_statue", + "name": "daylight_detector", "x": 2304, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_grate", + "name": "dead_brain_coral", "x": 2560, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_helmet", + "name": "dead_brain_coral_block", "x": 2816, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_hoe", + "name": "dead_brain_coral_fan", "x": 3072, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_horse_armor", + "name": "dead_bubble_coral", "x": 3328, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_ingot", + "name": "dead_bubble_coral_block", "x": 3584, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_lantern", + "name": "dead_bubble_coral_fan", "x": 3840, "y": 6656, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_leggings", + "name": "dead_bush", "x": 0, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_nautilus_armor", + "name": "dead_fire_coral", "x": 256, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_nugget", + "name": "dead_fire_coral_block", "x": 512, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_ore", + "name": "dead_fire_coral_fan", "x": 768, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_pickaxe", + "name": "dead_horn_coral", "x": 1024, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_shovel", + "name": "dead_horn_coral_block", "x": 1280, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_spear", + "name": "dead_horn_coral_fan", "x": 1536, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_spear_in_hand", + "name": "dead_tube_coral", "x": 1792, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_sword", + "name": "dead_tube_coral_block", "x": 2048, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_torch", + "name": "dead_tube_coral_fan", "x": 2304, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_trapdoor", + "name": "debug_stick", "x": 2560, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "copper_wall_torch", + "name": "decorated_pot", "x": 2816, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cornflower", + "name": "deepslate", "x": 3072, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cow_spawn_egg", + "name": "deepslate_brick_slab", "x": 3328, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cracked_deepslate_bricks", + "name": "deepslate_brick_stairs", "x": 3584, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cracked_deepslate_tiles", + "name": "deepslate_brick_wall", "x": 3840, "y": 6912, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cracked_nether_bricks", + "name": "deepslate_bricks", "x": 0, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cracked_polished_blackstone_bricks", + "name": "deepslate_coal_ore", "x": 256, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cracked_stone_bricks", + "name": "deepslate_copper_ore", "x": 512, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crafter", + "name": "deepslate_diamond_ore", "x": 768, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crafting_table", + "name": "deepslate_emerald_ore", "x": 1024, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "creaking_heart", + "name": "deepslate_gold_ore", "x": 1280, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "creaking_spawn_egg", + "name": "deepslate_iron_ore", "x": 1536, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "creeper_banner_pattern", + "name": "deepslate_lapis_ore", "x": 1792, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "creeper_head", + "name": "deepslate_redstone_ore", "x": 2048, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "creeper_spawn_egg", + "name": "deepslate_tile_slab", "x": 2304, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "creeper_wall_head", + "name": "deepslate_tile_stairs", "x": 2560, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_button", + "name": "deepslate_tile_wall", "x": 2816, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_door", + "name": "deepslate_tiles", "x": 3072, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_fence", + "name": "desert_pyramid_map", "x": 3328, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_fence_gate", + "name": "desert_village_map", "x": 3584, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_fungus", + "name": "detector_rail", "x": 3840, "y": 7168, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_hanging_sign", + "name": "diamond", "x": 0, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_hyphae", + "name": "diamond_axe", "x": 256, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_nylium", + "name": "diamond_block", "x": 512, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_planks", + "name": "diamond_boots", "x": 768, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_pressure_plate", + "name": "diamond_chestplate", "x": 1024, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_roots", + "name": "diamond_helmet", "x": 1280, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_shelf", + "name": "diamond_hoe", "x": 1536, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_sign", + "name": "diamond_horse_armor", "x": 1792, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_slab", + "name": "diamond_leggings", "x": 2048, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_stairs", + "name": "diamond_nautilus_armor", "x": 2304, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_stem", + "name": "diamond_ore", "x": 2560, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_trapdoor", + "name": "diamond_pickaxe", "x": 2816, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crimson_wall_sign", + "name": "diamond_shovel", "x": 3072, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crossbow", + "name": "diamond_spear", "x": 3328, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crossbow_arrow", + "name": "diamond_sword", "x": 3584, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crossbow_firework", + "name": "diorite", "x": 3840, "y": 7424, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crossbow_pulling_0", + "name": "diorite_slab", "x": 0, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crossbow_pulling_1", + "name": "diorite_stairs", "x": 256, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crossbow_pulling_2", + "name": "diorite_wall", "x": 512, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "crying_obsidian", + "name": "dirt", "x": 768, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_copper", + "name": "dirt_path", "x": 1024, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_copper_slab", + "name": "disc_fragment_5", "x": 1280, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_copper_stairs", + "name": "dispenser", "x": 1536, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_red_sandstone", + "name": "dolphin_spawn_egg", "x": 1792, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_red_sandstone_slab", + "name": "donkey_spawn_egg", "x": 2048, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_sandstone", + "name": "dragon_breath", "x": 2304, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cut_sandstone_slab", + "name": "dragon_egg", "x": 2560, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_banner", + "name": "dragon_head", "x": 2816, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_bed", + "name": "dried_ghast", "x": 3072, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_bundle", + "name": "dried_kelp", "x": 3328, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_bundle_open_back", + "name": "dried_kelp_block", "x": 3584, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_bundle_open_front", + "name": "dripstone_block", "x": 3840, "y": 7680, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_candle", + "name": "dropper", "x": 0, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_candle_cake", + "name": "drowned_spawn_egg", "x": 256, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_carpet", + "name": "dune_armor_trim_smithing_template", "x": 512, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_concrete", + "name": "echo_shard", "x": 768, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_concrete_powder", + "name": "egg", "x": 1024, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_dye", + "name": "elder_guardian_spawn_egg", "x": 1280, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_glazed_terracotta", + "name": "elytra", "x": 1536, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_harness", + "name": "emerald", "x": 1792, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_shulker_box", + "name": "emerald_block", "x": 2048, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_stained_glass", + "name": "emerald_ore", "x": 2304, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_stained_glass_pane", + "name": "enchanted_book", "x": 2560, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_terracotta", + "name": "enchanted_golden_apple", "x": 2816, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_wall_banner", + "name": "enchanting_table", "x": 3072, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "cyan_wool", + "name": "end_crystal", "x": 3328, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "damaged_anvil", + "name": "end_portal_frame", "x": 3584, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dandelion", + "name": "end_rod", "x": 3840, "y": 7936, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "danger_pottery_sherd", + "name": "end_stone", "x": 0, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_boat", + "name": "end_stone_brick_slab", "x": 256, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_button", + "name": "end_stone_brick_stairs", "x": 512, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_chest_boat", + "name": "end_stone_brick_wall", "x": 768, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_door", + "name": "end_stone_bricks", "x": 1024, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_fence", + "name": "ender_chest", "x": 1280, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_fence_gate", + "name": "ender_dragon_spawn_egg", "x": 1536, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_hanging_sign", + "name": "ender_eye", "x": 1792, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_leaves", + "name": "ender_pearl", "x": 2048, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_log", + "name": "enderman_spawn_egg", "x": 2304, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_planks", + "name": "endermite_spawn_egg", "x": 2560, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_pressure_plate", + "name": "evoker_spawn_egg", "x": 2816, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_sapling", + "name": "experience_bottle", "x": 3072, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_shelf", + "name": "explorer_pottery_sherd", "x": 3328, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_sign", + "name": "exposed_chiseled_copper", "x": 3584, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_slab", + "name": "exposed_copper", "x": 3840, "y": 8192, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_stairs", + "name": "exposed_copper_bars", "x": 0, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_trapdoor", + "name": "exposed_copper_bulb", "x": 256, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_wall_sign", + "name": "exposed_copper_chain", "x": 512, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_oak_wood", + "name": "exposed_copper_chest", "x": 768, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_prismarine", + "name": "exposed_copper_door", "x": 1024, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_prismarine_slab", + "name": "exposed_copper_golem_statue", "x": 1280, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dark_prismarine_stairs", + "name": "exposed_copper_grate", "x": 1536, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "daylight_detector", + "name": "exposed_copper_lantern", "x": 1792, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_brain_coral", + "name": "exposed_copper_trapdoor", "x": 2048, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_brain_coral_block", + "name": "exposed_cut_copper", "x": 2304, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_brain_coral_fan", + "name": "exposed_cut_copper_slab", "x": 2560, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_brain_coral_wall_fan", + "name": "exposed_cut_copper_stairs", "x": 2816, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_bubble_coral", + "name": "exposed_lightning_rod", "x": 3072, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_bubble_coral_block", + "name": "eye_armor_trim_smithing_template", "x": 3328, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_bubble_coral_fan", + "name": "farmland", "x": 3584, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_bubble_coral_wall_fan", + "name": "feather", "x": 3840, "y": 8448, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_bush", + "name": "fermented_spider_eye", "x": 0, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_fire_coral", + "name": "fern", "x": 256, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_fire_coral_block", + "name": "field_masoned_banner_pattern", "x": 512, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_fire_coral_fan", + "name": "filled_map", "x": 768, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_fire_coral_wall_fan", + "name": "fire_charge", "x": 1024, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_horn_coral", + "name": "fire_coral", "x": 1280, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_horn_coral_block", + "name": "fire_coral_block", "x": 1536, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_horn_coral_fan", + "name": "fire_coral_fan", "x": 1792, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_horn_coral_wall_fan", + "name": "firefly_bush", "x": 2048, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_tube_coral", + "name": "firework_rocket", "x": 2304, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_tube_coral_block", + "name": "firework_star", "x": 2560, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_tube_coral_fan", + "name": "fishing_rod", "x": 2816, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dead_tube_coral_wall_fan", + "name": "fletching_table", "x": 3072, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "debug_stick", + "name": "flint", "x": 3328, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "decorated_pot", + "name": "flint_and_steel", "x": 3584, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate", + "name": "flow_armor_trim_smithing_template", "x": 3840, "y": 8704, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_brick_slab", + "name": "flow_banner_pattern", "x": 0, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_brick_stairs", + "name": "flow_pottery_sherd", "x": 256, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_brick_wall", + "name": "flower_banner_pattern", "x": 512, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_bricks", + "name": "flower_pot", "x": 768, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_coal_ore", + "name": "flowering_azalea", "x": 1024, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_copper_ore", + "name": "flowering_azalea_leaves", "x": 1280, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_diamond_ore", + "name": "fox_spawn_egg", "x": 1536, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_emerald_ore", + "name": "friend_pottery_sherd", "x": 1792, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_gold_ore", + "name": "frog_spawn_egg", "x": 2048, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_iron_ore", + "name": "frogspawn", "x": 2304, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_lapis_ore", + "name": "furnace", "x": 2560, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_redstone_ore", + "name": "furnace_minecart", "x": 2816, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_tile_slab", + "name": "ghast_spawn_egg", "x": 3072, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_tile_stairs", + "name": "ghast_tear", "x": 3328, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_tile_wall", + "name": "gilded_blackstone", "x": 3584, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "deepslate_tiles", + "name": "glass", "x": 3840, "y": 8960, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "detector_rail", + "name": "glass_bottle", "x": 0, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond", + "name": "glass_pane", "x": 256, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_axe", + "name": "glistering_melon_slice", "x": 512, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_block", + "name": "globe_banner_pattern", "x": 768, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_boots", + "name": "glow_berries", "x": 1024, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_chestplate", + "name": "glow_ink_sac", "x": 1280, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_helmet", + "name": "glow_item_frame", "x": 1536, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_hoe", + "name": "glow_lichen", "x": 1792, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_horse_armor", + "name": "glow_squid_spawn_egg", "x": 2048, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_leggings", + "name": "glowstone", "x": 2304, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_nautilus_armor", + "name": "glowstone_dust", "x": 2560, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_ore", + "name": "goat_horn", "x": 2816, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_pickaxe", + "name": "goat_spawn_egg", "x": 3072, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_shovel", + "name": "gold_block", "x": 3328, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_spear", + "name": "gold_ingot", "x": 3584, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_spear_in_hand", + "name": "gold_nugget", "x": 3840, "y": 9216, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diamond_sword", + "name": "gold_ore", "x": 0, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diorite", + "name": "golden_apple", "x": 256, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diorite_slab", + "name": "golden_axe", "x": 512, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diorite_stairs", + "name": "golden_boots", "x": 768, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "diorite_wall", + "name": "golden_carrot", "x": 1024, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dirt", + "name": "golden_chestplate", "x": 1280, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dirt_path", + "name": "golden_dandelion", "x": 1536, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "disc_fragment_5", + "name": "golden_helmet", "x": 1792, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dispenser", + "name": "golden_hoe", "x": 2048, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dolphin_spawn_egg", + "name": "golden_horse_armor", "x": 2304, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "donkey_spawn_egg", + "name": "golden_leggings", "x": 2560, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dragon_breath", + "name": "golden_nautilus_armor", "x": 2816, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dragon_egg", + "name": "golden_pickaxe", "x": 3072, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dragon_head", + "name": "golden_shovel", "x": 3328, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dragon_wall_head", + "name": "golden_spear", "x": 3584, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dried_ghast", + "name": "golden_sword", "x": 3840, "y": 9472, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dried_kelp", + "name": "granite", "x": 0, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dried_kelp_block", + "name": "granite_slab", "x": 256, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dripstone_block", + "name": "granite_stairs", "x": 512, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dropper", + "name": "granite_wall", "x": 768, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "drowned_spawn_egg", + "name": "grass_block", "x": 1024, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "dune_armor_trim_smithing_template", + "name": "gravel", "x": 1280, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "echo_shard", + "name": "gray_banner", "x": 1536, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "egg", + "name": "gray_bed", "x": 1792, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "elder_guardian_spawn_egg", + "name": "gray_bundle", "x": 2048, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "elytra", + "name": "gray_candle", "x": 2304, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "elytra_broken", + "name": "gray_carpet", "x": 2560, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "emerald", + "name": "gray_concrete", "x": 2816, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "emerald_block", + "name": "gray_concrete_powder", "x": 3072, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "emerald_ore", + "name": "gray_concrete_slab", "x": 3328, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "enchanted_book", + "name": "gray_concrete_stairs", "x": 3584, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "enchanted_golden_apple", + "name": "gray_cushion", "x": 3840, "y": 9728, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "enchanting_table", + "name": "gray_dye", "x": 0, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_crystal", + "name": "gray_glazed_terracotta", "x": 256, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_gateway", + "name": "gray_harness", "x": 512, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_portal", + "name": "gray_shulker_box", "x": 768, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_portal_frame", + "name": "gray_stained_glass", "x": 1024, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_rod", + "name": "gray_stained_glass_pane", "x": 1280, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_stone", + "name": "gray_terracotta", "x": 1536, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_stone_brick_slab", + "name": "gray_wool", "x": 1792, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_stone_brick_stairs", + "name": "gray_wool_slab", "x": 2048, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_stone_brick_wall", + "name": "gray_wool_stairs", "x": 2304, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "end_stone_bricks", + "name": "green_banner", "x": 2560, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ender_chest", + "name": "green_bed", "x": 2816, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ender_dragon_spawn_egg", + "name": "green_bundle", "x": 3072, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ender_eye", + "name": "green_candle", "x": 3328, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ender_pearl", + "name": "green_carpet", "x": 3584, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "enderman_spawn_egg", + "name": "green_concrete", "x": 3840, "y": 9984, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "endermite_spawn_egg", + "name": "green_concrete_powder", "x": 0, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "evoker_spawn_egg", + "name": "green_concrete_slab", "x": 256, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "experience_bottle", + "name": "green_concrete_stairs", "x": 512, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "explorer_pottery_sherd", + "name": "green_cushion", "x": 768, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_chiseled_copper", + "name": "green_dye", "x": 1024, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper", + "name": "green_glazed_terracotta", "x": 1280, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_bars", + "name": "green_harness", "x": 1536, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_bulb", + "name": "green_shulker_box", "x": 1792, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_chain", + "name": "green_stained_glass", "x": 2048, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_chest", + "name": "green_stained_glass_pane", "x": 2304, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_door", + "name": "green_terracotta", "x": 2560, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_golem_statue", + "name": "green_wool", "x": 2816, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_grate", + "name": "green_wool_slab", "x": 3072, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_lantern", + "name": "green_wool_stairs", "x": 3328, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_copper_trapdoor", + "name": "grindstone", "x": 3584, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_cut_copper", + "name": "guardian_spawn_egg", "x": 3840, "y": 10240, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_cut_copper_slab", + "name": "gunpowder", "x": 0, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_cut_copper_stairs", + "name": "guster_banner_pattern", "x": 256, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "exposed_lightning_rod", + "name": "guster_pottery_sherd", "x": 512, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "eye_armor_trim_smithing_template", + "name": "hanging_roots", "x": 768, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "farmland", + "name": "happy_ghast_spawn_egg", "x": 1024, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "feather", + "name": "hay_block", "x": 1280, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fermented_spider_eye", + "name": "heart_of_the_sea", "x": 1536, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fern", + "name": "heart_pottery_sherd", "x": 1792, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "field_masoned_banner_pattern", + "name": "heartbreak_pottery_sherd", "x": 2048, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "filled_map", + "name": "heavy_core", "x": 2304, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fire", + "name": "heavy_weighted_pressure_plate", "x": 2560, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fire_charge", + "name": "hoglin_spawn_egg", "x": 2816, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fire_coral", + "name": "honey_block", "x": 3072, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fire_coral_block", + "name": "honey_bottle", "x": 3328, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fire_coral_fan", + "name": "honeycomb", "x": 3584, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fire_coral_wall_fan", + "name": "honeycomb_block", "x": 3840, "y": 10496, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "firefly_bush", + "name": "hopper", "x": 0, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "firework_rocket", + "name": "hopper_minecart", "x": 256, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "firework_star", + "name": "horn_coral", "x": 512, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fishing_rod", + "name": "horn_coral_block", "x": 768, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fishing_rod_cast", + "name": "horn_coral_fan", "x": 1024, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fletching_table", + "name": "horse_spawn_egg", "x": 1280, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flint", + "name": "host_armor_trim_smithing_template", "x": 1536, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flint_and_steel", + "name": "howl_pottery_sherd", "x": 1792, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flow_armor_trim_smithing_template", + "name": "husk_spawn_egg", "x": 2048, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flow_banner_pattern", + "name": "ice", "x": 2304, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flow_pottery_sherd", + "name": "infested_chiseled_stone_bricks", "x": 2560, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flower_banner_pattern", + "name": "infested_cobblestone", "x": 2816, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flower_pot", + "name": "infested_cracked_stone_bricks", "x": 3072, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flowering_azalea", + "name": "infested_deepslate", "x": 3328, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "flowering_azalea_leaves", + "name": "infested_mossy_stone_bricks", "x": 3584, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "fox_spawn_egg", + "name": "infested_stone", "x": 3840, "y": 10752, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "friend_pottery_sherd", + "name": "infested_stone_bricks", "x": 0, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "frog_spawn_egg", + "name": "ink_sac", "x": 256, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "frogspawn", + "name": "iron_axe", "x": 512, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "frosted_ice", + "name": "iron_bars", "x": 768, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "furnace", + "name": "iron_block", "x": 1024, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "furnace_minecart", + "name": "iron_boots", "x": 1280, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ghast_spawn_egg", + "name": "iron_chain", "x": 1536, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ghast_tear", + "name": "iron_chestplate", "x": 1792, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gilded_blackstone", + "name": "iron_door", "x": 2048, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glass", + "name": "iron_golem_spawn_egg", "x": 2304, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glass_bottle", + "name": "iron_helmet", "x": 2560, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glass_pane", + "name": "iron_hoe", "x": 2816, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glistering_melon_slice", + "name": "iron_horse_armor", "x": 3072, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "globe_banner_pattern", + "name": "iron_ingot", "x": 3328, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glow_berries", + "name": "iron_leggings", "x": 3584, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glow_ink_sac", + "name": "iron_nautilus_armor", "x": 3840, "y": 11008, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glow_item_frame", + "name": "iron_nugget", "x": 0, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glow_lichen", + "name": "iron_ore", "x": 256, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glow_squid_spawn_egg", + "name": "iron_pickaxe", "x": 512, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glowstone", + "name": "iron_shovel", "x": 768, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "glowstone_dust", + "name": "iron_spear", "x": 1024, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "goat_horn", + "name": "iron_sword", "x": 1280, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "goat_spawn_egg", + "name": "iron_trapdoor", "x": 1536, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gold_block", + "name": "item_frame", "x": 1792, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gold_ingot", + "name": "jack_o_lantern", "x": 2048, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gold_nugget", + "name": "jigsaw", "x": 2304, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gold_ore", + "name": "jukebox", "x": 2560, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_apple", + "name": "jungle_boat", "x": 2816, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_axe", + "name": "jungle_button", "x": 3072, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_boots", + "name": "jungle_chest_boat", "x": 3328, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_carrot", + "name": "jungle_door", "x": 3584, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_chestplate", + "name": "jungle_fence", "x": 3840, "y": 11264, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_helmet", + "name": "jungle_fence_gate", "x": 0, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_hoe", + "name": "jungle_hanging_sign", "x": 256, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_horse_armor", + "name": "jungle_leaves", "x": 512, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_leggings", + "name": "jungle_log", "x": 768, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_nautilus_armor", + "name": "jungle_planks", "x": 1024, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_pickaxe", + "name": "jungle_pressure_plate", "x": 1280, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_shovel", + "name": "jungle_pyramid_map", "x": 1536, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_spear", + "name": "jungle_sapling", "x": 1792, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_spear_in_hand", + "name": "jungle_shelf", "x": 2048, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "golden_sword", + "name": "jungle_sign", "x": 2304, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "granite", + "name": "jungle_slab", "x": 2560, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "granite_slab", + "name": "jungle_stairs", "x": 2816, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "granite_stairs", + "name": "jungle_trapdoor", "x": 3072, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "granite_wall", + "name": "jungle_wood", "x": 3328, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "grass_block", + "name": "kelp", "x": 3584, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gravel", + "name": "knowledge_book", "x": 3840, "y": 11520, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_banner", + "name": "ladder", "x": 0, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_bed", + "name": "lantern", "x": 256, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_bundle", + "name": "lapis_block", "x": 512, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_bundle_open_back", + "name": "lapis_lazuli", "x": 768, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_bundle_open_front", + "name": "lapis_ore", "x": 1024, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_candle", + "name": "large_amethyst_bud", "x": 1280, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_candle_cake", + "name": "large_fern", "x": 1536, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_carpet", + "name": "lava_bucket", "x": 1792, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_concrete", + "name": "lead", "x": 2048, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_concrete_powder", + "name": "leaf_litter", "x": 2304, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_dye", + "name": "leather", "x": 2560, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_glazed_terracotta", + "name": "leather_boots", "x": 2816, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_harness", + "name": "leather_chestplate", "x": 3072, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_shulker_box", + "name": "leather_helmet", "x": 3328, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_stained_glass", + "name": "leather_horse_armor", "x": 3584, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_stained_glass_pane", + "name": "leather_leggings", "x": 3840, "y": 11776, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_terracotta", + "name": "lectern", "x": 0, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_wall_banner", + "name": "lever", "x": 256, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gray_wool", + "name": "light", "x": 512, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_banner", + "name": "light_blue_banner", "x": 768, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_bed", + "name": "light_blue_bed", "x": 1024, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_bundle", + "name": "light_blue_bundle", "x": 1280, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_bundle_open_back", + "name": "light_blue_candle", "x": 1536, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_bundle_open_front", + "name": "light_blue_carpet", "x": 1792, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_candle", + "name": "light_blue_concrete", "x": 2048, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_candle_cake", + "name": "light_blue_concrete_powder", "x": 2304, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_carpet", + "name": "light_blue_concrete_slab", "x": 2560, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_concrete", + "name": "light_blue_concrete_stairs", "x": 2816, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_concrete_powder", + "name": "light_blue_cushion", "x": 3072, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_dye", + "name": "light_blue_dye", "x": 3328, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_glazed_terracotta", + "name": "light_blue_glazed_terracotta", "x": 3584, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_harness", + "name": "light_blue_harness", "x": 3840, "y": 12032, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_shulker_box", + "name": "light_blue_shulker_box", "x": 0, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_stained_glass", + "name": "light_blue_stained_glass", "x": 256, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_stained_glass_pane", + "name": "light_blue_stained_glass_pane", "x": 512, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_terracotta", + "name": "light_blue_terracotta", "x": 768, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_wall_banner", + "name": "light_blue_wool", "x": 1024, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "green_wool", + "name": "light_blue_wool_slab", "x": 1280, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "grindstone", + "name": "light_blue_wool_stairs", "x": 1536, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "guardian_spawn_egg", + "name": "light_gray_banner", "x": 1792, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "gunpowder", + "name": "light_gray_bed", "x": 2048, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "guster_banner_pattern", + "name": "light_gray_bundle", "x": 2304, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "guster_pottery_sherd", + "name": "light_gray_candle", "x": 2560, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "hanging_roots", + "name": "light_gray_carpet", "x": 2816, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "happy_ghast_spawn_egg", + "name": "light_gray_concrete", "x": 3072, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "hay_block", + "name": "light_gray_concrete_powder", "x": 3328, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "heart_of_the_sea", + "name": "light_gray_concrete_slab", "x": 3584, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "heart_pottery_sherd", + "name": "light_gray_concrete_stairs", "x": 3840, "y": 12288, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "heartbreak_pottery_sherd", + "name": "light_gray_cushion", "x": 0, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "heavy_core", + "name": "light_gray_dye", "x": 256, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "heavy_weighted_pressure_plate", + "name": "light_gray_glazed_terracotta", "x": 512, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "hoglin_spawn_egg", + "name": "light_gray_harness", "x": 768, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "honey_block", + "name": "light_gray_shulker_box", "x": 1024, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "honey_bottle", + "name": "light_gray_stained_glass", "x": 1280, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "honeycomb", + "name": "light_gray_stained_glass_pane", "x": 1536, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "honeycomb_block", + "name": "light_gray_terracotta", "x": 1792, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "hopper", + "name": "light_gray_wool", "x": 2048, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "hopper_minecart", + "name": "light_gray_wool_slab", "x": 2304, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "horn_coral", + "name": "light_gray_wool_stairs", "x": 2560, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "horn_coral_block", + "name": "light_weighted_pressure_plate", "x": 2816, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "horn_coral_fan", + "name": "lightning_rod", "x": 3072, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "horn_coral_wall_fan", + "name": "lilac", "x": 3328, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "horse_spawn_egg", + "name": "lily_of_the_valley", "x": 3584, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "host_armor_trim_smithing_template", + "name": "lily_pad", "x": 3840, "y": 12544, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "howl_pottery_sherd", + "name": "lime_banner", "x": 0, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "husk_spawn_egg", + "name": "lime_bed", "x": 256, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ice", + "name": "lime_bundle", "x": 512, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_chiseled_stone_bricks", + "name": "lime_candle", "x": 768, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_cobblestone", + "name": "lime_carpet", "x": 1024, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_cracked_stone_bricks", + "name": "lime_concrete", "x": 1280, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_deepslate", + "name": "lime_concrete_powder", "x": 1536, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_mossy_stone_bricks", + "name": "lime_concrete_slab", "x": 1792, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_stone", + "name": "lime_concrete_stairs", "x": 2048, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "infested_stone_bricks", + "name": "lime_cushion", "x": 2304, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ink_sac", + "name": "lime_dye", "x": 2560, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_axe", + "name": "lime_glazed_terracotta", "x": 2816, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_bars", + "name": "lime_harness", "x": 3072, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_block", + "name": "lime_shulker_box", "x": 3328, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_boots", + "name": "lime_stained_glass", "x": 3584, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_chain", + "name": "lime_stained_glass_pane", "x": 3840, "y": 12800, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_chestplate", + "name": "lime_terracotta", "x": 0, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_door", + "name": "lime_wool", "x": 256, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_golem_spawn_egg", + "name": "lime_wool_slab", "x": 512, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_helmet", + "name": "lime_wool_stairs", "x": 768, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_hoe", + "name": "lingering_potion", "x": 1024, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_horse_armor", + "name": "llama_spawn_egg", "x": 1280, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_ingot", + "name": "lodestone", "x": 1536, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_leggings", + "name": "loom", "x": 1792, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_nautilus_armor", + "name": "mace", "x": 2048, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_nugget", + "name": "magenta_banner", "x": 2304, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_ore", + "name": "magenta_bed", "x": 2560, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_pickaxe", + "name": "magenta_bundle", "x": 2816, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_shovel", + "name": "magenta_candle", "x": 3072, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_spear", + "name": "magenta_carpet", "x": 3328, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_spear_in_hand", + "name": "magenta_concrete", "x": 3584, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_sword", + "name": "magenta_concrete_powder", "x": 3840, "y": 13056, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "iron_trapdoor", + "name": "magenta_concrete_slab", "x": 0, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "item_frame", + "name": "magenta_concrete_stairs", "x": 256, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jack_o_lantern", + "name": "magenta_cushion", "x": 512, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jigsaw", + "name": "magenta_dye", "x": 768, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jukebox", + "name": "magenta_glazed_terracotta", "x": 1024, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_boat", + "name": "magenta_harness", "x": 1280, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_button", + "name": "magenta_shulker_box", "x": 1536, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_chest_boat", + "name": "magenta_stained_glass", "x": 1792, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_door", + "name": "magenta_stained_glass_pane", "x": 2048, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_fence", + "name": "magenta_terracotta", "x": 2304, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_fence_gate", + "name": "magenta_wool", "x": 2560, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_hanging_sign", + "name": "magenta_wool_slab", "x": 2816, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_leaves", + "name": "magenta_wool_stairs", "x": 3072, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_log", + "name": "magma_block", "x": 3328, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_planks", + "name": "magma_cream", "x": 3584, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_pressure_plate", + "name": "magma_cube_spawn_egg", "x": 3840, "y": 13312, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_sapling", + "name": "mangrove_boat", "x": 0, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_shelf", + "name": "mangrove_button", "x": 256, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_sign", + "name": "mangrove_chest_boat", "x": 512, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_slab", + "name": "mangrove_door", "x": 768, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_stairs", + "name": "mangrove_fence", "x": 1024, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_trapdoor", + "name": "mangrove_fence_gate", "x": 1280, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_wall_sign", + "name": "mangrove_hanging_sign", "x": 1536, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "jungle_wood", + "name": "mangrove_leaves", "x": 1792, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "kelp", + "name": "mangrove_log", "x": 2048, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "kelp_plant", + "name": "mangrove_planks", "x": 2304, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "knowledge_book", + "name": "mangrove_pressure_plate", "x": 2560, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ladder", + "name": "mangrove_propagule", "x": 2816, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lantern", + "name": "mangrove_roots", "x": 3072, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lapis_block", + "name": "mangrove_shelf", "x": 3328, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lapis_lazuli", + "name": "mangrove_sign", "x": 3584, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lapis_ore", + "name": "mangrove_slab", "x": 3840, "y": 13568, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "large_amethyst_bud", + "name": "mangrove_stairs", "x": 0, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "large_fern", + "name": "mangrove_trapdoor", "x": 256, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lava_bucket", + "name": "mangrove_wood", "x": 512, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lava_cauldron", + "name": "map", "x": 768, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lead", + "name": "medium_amethyst_bud", "x": 1024, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leaf_litter", + "name": "melon", "x": 1280, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leather", + "name": "melon_seeds", "x": 1536, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leather_boots", + "name": "melon_slice", "x": 1792, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leather_chestplate", + "name": "milk_bucket", "x": 2048, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leather_helmet", + "name": "minecart", "x": 2304, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leather_horse_armor", + "name": "miner_pottery_sherd", "x": 2560, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "leather_leggings", + "name": "mojang_banner_pattern", "x": 2816, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lectern", + "name": "mooshroom_spawn_egg", "x": 3072, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lever", + "name": "moss_block", "x": 3328, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light", + "name": "moss_carpet", "x": 3584, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_00", + "name": "mossy_cobblestone", "x": 3840, "y": 13824, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_01", + "name": "mossy_cobblestone_slab", "x": 0, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_02", + "name": "mossy_cobblestone_stairs", "x": 256, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_03", + "name": "mossy_cobblestone_wall", "x": 512, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_04", + "name": "mossy_stone_brick_slab", "x": 768, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_05", + "name": "mossy_stone_brick_stairs", "x": 1024, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_06", + "name": "mossy_stone_brick_wall", "x": 1280, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_07", + "name": "mossy_stone_bricks", "x": 1536, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_08", + "name": "mourner_pottery_sherd", "x": 1792, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_09", + "name": "mud", "x": 2048, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_10", + "name": "mud_brick_slab", "x": 2304, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_11", + "name": "mud_brick_stairs", "x": 2560, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_12", + "name": "mud_brick_wall", "x": 2816, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_13", + "name": "mud_bricks", "x": 3072, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_14", + "name": "muddy_mangrove_roots", "x": 3328, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_15", + "name": "mule_spawn_egg", "x": 3584, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_banner", + "name": "mushroom_stem", "x": 3840, "y": 14080, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_bed", + "name": "mushroom_stew", "x": 0, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_bundle", + "name": "music_disc_11", "x": 256, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_bundle_open_back", + "name": "music_disc_13", "x": 512, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_bundle_open_front", + "name": "music_disc_5", "x": 768, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_candle", + "name": "music_disc_blocks", "x": 1024, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_candle_cake", + "name": "music_disc_bounce", "x": 1280, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_carpet", + "name": "music_disc_cat", "x": 1536, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_concrete", + "name": "music_disc_chirp", "x": 1792, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_concrete_powder", + "name": "music_disc_creator", "x": 2048, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_dye", + "name": "music_disc_creator_music_box", "x": 2304, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_glazed_terracotta", + "name": "music_disc_far", "x": 2560, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_harness", + "name": "music_disc_lava_chicken", "x": 2816, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_shulker_box", + "name": "music_disc_mall", "x": 3072, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_stained_glass", + "name": "music_disc_mellohi", "x": 3328, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_stained_glass_pane", + "name": "music_disc_otherside", "x": 3584, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_terracotta", + "name": "music_disc_pigstep", "x": 3840, "y": 14336, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_wall_banner", + "name": "music_disc_precipice", "x": 0, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_blue_wool", + "name": "music_disc_relic", "x": 256, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_banner", + "name": "music_disc_stal", "x": 512, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_bed", + "name": "music_disc_strad", "x": 768, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_bundle", + "name": "music_disc_tears", "x": 1024, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_bundle_open_back", + "name": "music_disc_wait", "x": 1280, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_bundle_open_front", + "name": "music_disc_ward", "x": 1536, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_candle", + "name": "mutton", "x": 1792, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_candle_cake", + "name": "mycelium", "x": 2048, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_carpet", + "name": "name_tag", "x": 2304, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_concrete", + "name": "nautilus_shell", "x": 2560, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_concrete_powder", + "name": "nautilus_spawn_egg", "x": 2816, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_dye", + "name": "nether_brick", "x": 3072, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_glazed_terracotta", + "name": "nether_brick_fence", "x": 3328, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_harness", + "name": "nether_brick_slab", "x": 3584, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_shulker_box", + "name": "nether_brick_stairs", "x": 3840, "y": 14592, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_stained_glass", + "name": "nether_brick_wall", "x": 0, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_stained_glass_pane", + "name": "nether_bricks", "x": 256, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_terracotta", + "name": "nether_gold_ore", "x": 512, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_wall_banner", + "name": "nether_quartz_ore", "x": 768, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_gray_wool", + "name": "nether_sprouts", "x": 1024, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "light_weighted_pressure_plate", + "name": "nether_star", "x": 1280, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lightning_rod", + "name": "nether_wart", "x": 1536, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lilac", + "name": "nether_wart_block", "x": 1792, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lily_of_the_valley", + "name": "netherite_axe", "x": 2048, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lily_pad", + "name": "netherite_block", "x": 2304, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_banner", + "name": "netherite_boots", "x": 2560, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_bed", + "name": "netherite_chestplate", "x": 2816, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_bundle", + "name": "netherite_helmet", "x": 3072, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_bundle_open_back", + "name": "netherite_hoe", "x": 3328, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_bundle_open_front", + "name": "netherite_horse_armor", "x": 3584, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_candle", + "name": "netherite_ingot", "x": 3840, "y": 14848, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_candle_cake", + "name": "netherite_leggings", "x": 0, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_carpet", + "name": "netherite_nautilus_armor", "x": 256, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_concrete", + "name": "netherite_pickaxe", "x": 512, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_concrete_powder", + "name": "netherite_scrap", "x": 768, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_dye", + "name": "netherite_shovel", "x": 1024, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_glazed_terracotta", + "name": "netherite_spear", "x": 1280, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_harness", + "name": "netherite_sword", "x": 1536, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_shulker_box", + "name": "netherite_upgrade_smithing_template", "x": 1792, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_stained_glass", + "name": "netherrack", "x": 2048, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_stained_glass_pane", + "name": "note_block", "x": 2304, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_terracotta", + "name": "oak_boat", "x": 2560, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_wall_banner", + "name": "oak_button", "x": 2816, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lime_wool", + "name": "oak_chest_boat", "x": 3072, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lingering_potion", + "name": "oak_door", "x": 3328, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "llama_spawn_egg", + "name": "oak_fence", "x": 3584, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "lodestone", + "name": "oak_fence_gate", "x": 3840, "y": 15104, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "loom", + "name": "oak_hanging_sign", "x": 0, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mace", + "name": "oak_leaves", "x": 256, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_banner", + "name": "oak_log", "x": 512, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_bed", + "name": "oak_planks", "x": 768, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_bundle", + "name": "oak_pressure_plate", "x": 1024, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_bundle_open_back", + "name": "oak_sapling", "x": 1280, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_bundle_open_front", + "name": "oak_shelf", "x": 1536, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_candle", + "name": "oak_sign", "x": 1792, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_candle_cake", + "name": "oak_slab", "x": 2048, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_carpet", + "name": "oak_stairs", "x": 2304, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_concrete", + "name": "oak_trapdoor", "x": 2560, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_concrete_powder", + "name": "oak_wood", "x": 2816, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_dye", + "name": "observer", "x": 3072, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_glazed_terracotta", + "name": "obsidian", "x": 3328, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_harness", + "name": "ocean_monument_map", "x": 3584, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_shulker_box", + "name": "ocelot_spawn_egg", "x": 3840, "y": 15360, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_stained_glass", + "name": "ochre_froglight", "x": 0, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_stained_glass_pane", + "name": "ominous_bottle", "x": 256, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_terracotta", + "name": "ominous_trial_key", "x": 512, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_wall_banner", + "name": "open_eyeblossom", "x": 768, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magenta_wool", + "name": "orange_banner", "x": 1024, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magma_block", + "name": "orange_bed", "x": 1280, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magma_cream", + "name": "orange_bundle", "x": 1536, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "magma_cube_spawn_egg", + "name": "orange_candle", "x": 1792, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_boat", + "name": "orange_carpet", "x": 2048, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_button", + "name": "orange_concrete", "x": 2304, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_chest_boat", + "name": "orange_concrete_powder", "x": 2560, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_door", + "name": "orange_concrete_slab", "x": 2816, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_fence", + "name": "orange_concrete_stairs", "x": 3072, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_fence_gate", + "name": "orange_cushion", "x": 3328, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_hanging_sign", + "name": "orange_dye", "x": 3584, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_leaves", + "name": "orange_glazed_terracotta", "x": 3840, "y": 15616, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_log", + "name": "orange_harness", "x": 0, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_planks", + "name": "orange_poplar_leaves", "x": 256, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_pressure_plate", + "name": "orange_shulker_box", "x": 512, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_propagule", + "name": "orange_stained_glass", "x": 768, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_roots", + "name": "orange_stained_glass_pane", "x": 1024, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_shelf", + "name": "orange_terracotta", "x": 1280, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_sign", + "name": "orange_tulip", "x": 1536, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_slab", + "name": "orange_wool", "x": 1792, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_stairs", + "name": "orange_wool_slab", "x": 2048, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_trapdoor", + "name": "orange_wool_stairs", "x": 2304, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_wall_sign", + "name": "oxeye_daisy", "x": 2560, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mangrove_wood", + "name": "oxidized_chiseled_copper", "x": 2816, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "map", + "name": "oxidized_copper", "x": 3072, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "medium_amethyst_bud", + "name": "oxidized_copper_bars", "x": 3328, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "melon", + "name": "oxidized_copper_bulb", "x": 3584, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "melon_seeds", + "name": "oxidized_copper_chain", "x": 3840, "y": 15872, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "melon_slice", + "name": "oxidized_copper_chest", "x": 0, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "melon_stem", + "name": "oxidized_copper_door", "x": 256, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "milk_bucket", + "name": "oxidized_copper_golem_statue", "x": 512, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "minecart", + "name": "oxidized_copper_grate", "x": 768, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "miner_pottery_sherd", + "name": "oxidized_copper_lantern", "x": 1024, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mojang_banner_pattern", + "name": "oxidized_copper_trapdoor", "x": 1280, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mooshroom_spawn_egg", + "name": "oxidized_cut_copper", "x": 1536, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "moss_block", + "name": "oxidized_cut_copper_slab", "x": 1792, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "moss_carpet", + "name": "oxidized_cut_copper_stairs", "x": 2048, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_cobblestone", + "name": "oxidized_lightning_rod", "x": 2304, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_cobblestone_slab", + "name": "packed_ice", "x": 2560, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_cobblestone_stairs", + "name": "packed_mud", "x": 2816, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_cobblestone_wall", + "name": "painting", "x": 3072, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_stone_brick_slab", + "name": "pale_hanging_moss", "x": 3328, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_stone_brick_stairs", + "name": "pale_moss_block", "x": 3584, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_stone_brick_wall", + "name": "pale_moss_carpet", "x": 3840, "y": 16128, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mossy_stone_bricks", + "name": "pale_oak_boat", "x": 0, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mourner_pottery_sherd", + "name": "pale_oak_button", "x": 256, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mud", + "name": "pale_oak_chest_boat", "x": 512, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mud_brick_slab", + "name": "pale_oak_door", "x": 768, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mud_brick_stairs", + "name": "pale_oak_fence", "x": 1024, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mud_brick_wall", + "name": "pale_oak_fence_gate", "x": 1280, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mud_bricks", + "name": "pale_oak_hanging_sign", "x": 1536, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "muddy_mangrove_roots", + "name": "pale_oak_leaves", "x": 1792, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mule_spawn_egg", + "name": "pale_oak_log", "x": 2048, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mushroom_stem", + "name": "pale_oak_planks", "x": 2304, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mushroom_stew", + "name": "pale_oak_pressure_plate", "x": 2560, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_11", + "name": "pale_oak_sapling", "x": 2816, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_13", + "name": "pale_oak_shelf", "x": 3072, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_5", + "name": "pale_oak_sign", "x": 3328, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_blocks", + "name": "pale_oak_slab", "x": 3584, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_cat", + "name": "pale_oak_stairs", "x": 3840, "y": 16384, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_chirp", + "name": "pale_oak_trapdoor", "x": 0, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_creator", + "name": "pale_oak_wood", "x": 256, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_creator_music_box", + "name": "panda_spawn_egg", "x": 512, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_far", + "name": "paper", "x": 768, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_lava_chicken", + "name": "parched_spawn_egg", "x": 1024, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_mall", + "name": "parrot_spawn_egg", "x": 1280, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_mellohi", + "name": "pearlescent_froglight", "x": 1536, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_otherside", + "name": "peony", "x": 1792, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_pigstep", + "name": "petrified_oak_slab", "x": 2048, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_precipice", + "name": "phantom_membrane", "x": 2304, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_relic", + "name": "phantom_spawn_egg", "x": 2560, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_stal", + "name": "pig_spawn_egg", "x": 2816, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_strad", + "name": "piglin_banner_pattern", "x": 3072, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_tears", + "name": "piglin_brute_spawn_egg", "x": 3328, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_wait", + "name": "piglin_head", "x": 3584, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "music_disc_ward", + "name": "piglin_spawn_egg", "x": 3840, "y": 16640, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mutton", + "name": "pillager_spawn_egg", "x": 0, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "mycelium", + "name": "pink_banner", "x": 256, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "name_tag", + "name": "pink_bed", "x": 512, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nautilus_shell", + "name": "pink_bundle", "x": 768, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nautilus_spawn_egg", + "name": "pink_candle", "x": 1024, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_brick", + "name": "pink_carpet", "x": 1280, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_brick_fence", + "name": "pink_concrete", "x": 1536, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_brick_slab", + "name": "pink_concrete_powder", "x": 1792, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_brick_stairs", + "name": "pink_concrete_slab", "x": 2048, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_brick_wall", + "name": "pink_concrete_stairs", "x": 2304, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_bricks", + "name": "pink_cushion", "x": 2560, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_gold_ore", + "name": "pink_dye", "x": 2816, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_portal", + "name": "pink_glazed_terracotta", "x": 3072, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_quartz_ore", + "name": "pink_harness", "x": 3328, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_sprouts", + "name": "pink_petals", "x": 3584, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_star", + "name": "pink_shulker_box", "x": 3840, "y": 16896, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_wart", + "name": "pink_stained_glass", "x": 0, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "nether_wart_block", + "name": "pink_stained_glass_pane", "x": 256, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_axe", + "name": "pink_terracotta", "x": 512, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_block", + "name": "pink_tulip", "x": 768, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_boots", + "name": "pink_wool", "x": 1024, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_chestplate", + "name": "pink_wool_slab", "x": 1280, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_helmet", + "name": "pink_wool_stairs", "x": 1536, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_hoe", + "name": "piston", "x": 1792, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_horse_armor", + "name": "pitcher_plant", "x": 2048, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_ingot", + "name": "pitcher_pod", "x": 2304, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_leggings", + "name": "plains_village_map", "x": 2560, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_nautilus_armor", + "name": "player_head", "x": 2816, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_pickaxe", + "name": "plenty_pottery_sherd", "x": 3072, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_scrap", + "name": "podzol", "x": 3328, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_shovel", + "name": "pointed_dripstone", "x": 3584, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_spear", + "name": "poisonous_potato", "x": 3840, "y": 17152, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_spear_in_hand", + "name": "polar_bear_spawn_egg", "x": 0, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_sword", + "name": "polished_andesite", "x": 256, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherite_upgrade_smithing_template", + "name": "polished_andesite_slab", "x": 512, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "netherrack", + "name": "polished_andesite_stairs", "x": 768, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "note_block", + "name": "polished_basalt", "x": 1024, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_boat", + "name": "polished_blackstone", "x": 1280, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_button", + "name": "polished_blackstone_brick_slab", "x": 1536, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_chest_boat", + "name": "polished_blackstone_brick_stairs", "x": 1792, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_door", + "name": "polished_blackstone_brick_wall", "x": 2048, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_fence", + "name": "polished_blackstone_bricks", "x": 2304, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_fence_gate", + "name": "polished_blackstone_button", "x": 2560, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_hanging_sign", + "name": "polished_blackstone_pressure_plate", "x": 2816, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_leaves", + "name": "polished_blackstone_slab", "x": 3072, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_log", + "name": "polished_blackstone_stairs", "x": 3328, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_planks", + "name": "polished_blackstone_wall", "x": 3584, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_pressure_plate", + "name": "polished_cinnabar", "x": 3840, "y": 17408, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_sapling", + "name": "polished_cinnabar_slab", "x": 0, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_shelf", + "name": "polished_cinnabar_stairs", "x": 256, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_sign", + "name": "polished_cinnabar_wall", "x": 512, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_slab", + "name": "polished_deepslate", "x": 768, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_stairs", + "name": "polished_deepslate_slab", "x": 1024, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_trapdoor", + "name": "polished_deepslate_stairs", "x": 1280, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_wall_sign", + "name": "polished_deepslate_wall", "x": 1536, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oak_wood", + "name": "polished_diorite", "x": 1792, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "observer", + "name": "polished_diorite_slab", "x": 2048, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "obsidian", + "name": "polished_diorite_stairs", "x": 2304, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ocelot_spawn_egg", + "name": "polished_granite", "x": 2560, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ochre_froglight", + "name": "polished_granite_slab", "x": 2816, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ominous_bottle", + "name": "polished_granite_stairs", "x": 3072, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ominous_trial_key", + "name": "polished_sulfur", "x": 3328, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "open_eyeblossom", + "name": "polished_sulfur_slab", "x": 3584, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_banner", + "name": "polished_sulfur_stairs", "x": 3840, "y": 17664, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_bed", + "name": "polished_sulfur_wall", "x": 0, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_bundle", + "name": "polished_tuff", "x": 256, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_bundle_open_back", + "name": "polished_tuff_slab", "x": 512, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_bundle_open_front", + "name": "polished_tuff_stairs", "x": 768, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_candle", + "name": "polished_tuff_wall", "x": 1024, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_candle_cake", + "name": "poplar_boat", "x": 1280, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_carpet", + "name": "poplar_button", "x": 1536, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_concrete", + "name": "poplar_chest_boat", "x": 1792, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_concrete_powder", + "name": "poplar_door", "x": 2048, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_dye", + "name": "poplar_fence", "x": 2304, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_glazed_terracotta", + "name": "poplar_fence_gate", "x": 2560, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_harness", + "name": "poplar_hanging_sign", "x": 2816, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_shulker_box", + "name": "poplar_log", "x": 3072, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_stained_glass", + "name": "poplar_planks", "x": 3328, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_stained_glass_pane", + "name": "poplar_pressure_plate", "x": 3584, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_terracotta", + "name": "poplar_sapling", "x": 3840, "y": 17920, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_tulip", + "name": "poplar_shelf", "x": 0, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_wall_banner", + "name": "poplar_sign", "x": 256, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "orange_wool", + "name": "poplar_slab", "x": 512, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxeye_daisy", + "name": "poplar_stairs", "x": 768, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_chiseled_copper", + "name": "poplar_trapdoor", "x": 1024, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper", + "name": "poplar_wood", "x": 1280, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_bars", + "name": "popped_chorus_fruit", "x": 1536, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_bulb", + "name": "poppy", "x": 1792, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_chain", + "name": "porkchop", "x": 2048, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_chest", + "name": "potato", "x": 2304, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_door", + "name": "potent_sulfur", "x": 2560, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_golem_statue", + "name": "potion", "x": 2816, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_grate", + "name": "powder_snow_bucket", "x": 3072, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_lantern", + "name": "powered_rail", "x": 3328, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_copper_trapdoor", + "name": "prismarine", "x": 3584, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_cut_copper", + "name": "prismarine_brick_slab", "x": 3840, "y": 18176, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_cut_copper_slab", + "name": "prismarine_brick_stairs", "x": 0, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_cut_copper_stairs", + "name": "prismarine_bricks", "x": 256, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "oxidized_lightning_rod", + "name": "prismarine_crystals", "x": 512, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "packed_ice", + "name": "prismarine_shard", "x": 768, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "packed_mud", + "name": "prismarine_slab", "x": 1024, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "painting", + "name": "prismarine_stairs", "x": 1280, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_hanging_moss", + "name": "prismarine_wall", "x": 1536, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_moss_block", + "name": "prize_pottery_sherd", "x": 1792, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_moss_carpet", + "name": "pufferfish", "x": 2048, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_boat", + "name": "pufferfish_bucket", "x": 2304, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_button", + "name": "pufferfish_spawn_egg", "x": 2560, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_chest_boat", + "name": "pumpkin", "x": 2816, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_door", + "name": "pumpkin_pie", "x": 3072, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_fence", + "name": "pumpkin_seeds", "x": 3328, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_fence_gate", + "name": "purple_banner", "x": 3584, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_hanging_sign", + "name": "purple_bed", "x": 3840, "y": 18432, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_leaves", + "name": "purple_bundle", "x": 0, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_log", + "name": "purple_candle", "x": 256, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_planks", + "name": "purple_carpet", "x": 512, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_pressure_plate", + "name": "purple_concrete", "x": 768, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_sapling", + "name": "purple_concrete_powder", "x": 1024, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_shelf", + "name": "purple_concrete_slab", "x": 1280, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_sign", + "name": "purple_concrete_stairs", "x": 1536, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_slab", + "name": "purple_cushion", "x": 1792, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_stairs", + "name": "purple_dye", "x": 2048, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_trapdoor", + "name": "purple_glazed_terracotta", "x": 2304, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_wall_sign", + "name": "purple_harness", "x": 2560, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pale_oak_wood", + "name": "purple_shulker_box", "x": 2816, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "panda_spawn_egg", + "name": "purple_stained_glass", "x": 3072, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "paper", + "name": "purple_stained_glass_pane", "x": 3328, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "parched_spawn_egg", + "name": "purple_terracotta", "x": 3584, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "parrot_spawn_egg", + "name": "purple_wool", "x": 3840, "y": 18688, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pearlescent_froglight", + "name": "purple_wool_slab", "x": 0, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "peony", + "name": "purple_wool_stairs", "x": 256, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "petrified_oak_slab", + "name": "purpur_block", "x": 512, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "phantom_membrane", + "name": "purpur_pillar", "x": 768, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "phantom_spawn_egg", + "name": "purpur_slab", "x": 1024, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pig_spawn_egg", + "name": "purpur_stairs", "x": 1280, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piglin_banner_pattern", + "name": "quartz", "x": 1536, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piglin_brute_spawn_egg", + "name": "quartz_block", "x": 1792, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piglin_head", + "name": "quartz_bricks", "x": 2048, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piglin_spawn_egg", + "name": "quartz_pillar", "x": 2304, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piglin_wall_head", + "name": "quartz_slab", "x": 2560, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pillager_spawn_egg", + "name": "quartz_stairs", "x": 2816, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_banner", + "name": "rabbit", "x": 3072, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_bed", + "name": "rabbit_foot", "x": 3328, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_bundle", + "name": "rabbit_hide", "x": 3584, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_bundle_open_back", + "name": "rabbit_spawn_egg", "x": 3840, "y": 18944, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_bundle_open_front", + "name": "rabbit_stew", "x": 0, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_candle", + "name": "rail", "x": 256, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_candle_cake", + "name": "raiser_armor_trim_smithing_template", "x": 512, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_carpet", + "name": "ravager_spawn_egg", "x": 768, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_concrete", + "name": "raw_copper", "x": 1024, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_concrete_powder", + "name": "raw_copper_block", "x": 1280, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_dye", + "name": "raw_gold", "x": 1536, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_glazed_terracotta", + "name": "raw_gold_block", "x": 1792, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_harness", + "name": "raw_iron", "x": 2048, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_petals", + "name": "raw_iron_block", "x": 2304, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_shulker_box", + "name": "recovery_compass", "x": 2560, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_stained_glass", + "name": "red_banner", "x": 2816, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_stained_glass_pane", + "name": "red_bed", "x": 3072, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_terracotta", + "name": "red_bundle", "x": 3328, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_tulip", + "name": "red_candle", "x": 3584, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_wall_banner", + "name": "red_carpet", "x": 3840, "y": 19200, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pink_wool", + "name": "red_concrete", "x": 0, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piston", + "name": "red_concrete_powder", "x": 256, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "piston_head", + "name": "red_concrete_slab", "x": 512, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pitcher_crop", + "name": "red_concrete_stairs", "x": 768, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pitcher_plant", + "name": "red_cushion", "x": 1024, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pitcher_pod", + "name": "red_dye", "x": 1280, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "player_head", + "name": "red_glazed_terracotta", "x": 1536, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "player_wall_head", + "name": "red_harness", "x": 1792, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "plenty_pottery_sherd", + "name": "red_mushroom", "x": 2048, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "podzol", + "name": "red_mushroom_block", "x": 2304, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pointed_dripstone", + "name": "red_nether_brick_slab", "x": 2560, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "poisonous_potato", + "name": "red_nether_brick_stairs", "x": 2816, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polar_bear_spawn_egg", + "name": "red_nether_brick_wall", "x": 3072, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_andesite", + "name": "red_nether_bricks", "x": 3328, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_andesite_slab", + "name": "red_poplar_leaves", "x": 3584, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_andesite_stairs", + "name": "red_sand", "x": 3840, "y": 19456, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_basalt", + "name": "red_sandstone", "x": 0, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone", + "name": "red_sandstone_slab", "x": 256, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_brick_slab", + "name": "red_sandstone_stairs", "x": 512, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_brick_stairs", + "name": "red_sandstone_wall", "x": 768, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_brick_wall", + "name": "red_shrub", "x": 1024, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_bricks", + "name": "red_shulker_box", "x": 1280, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_button", + "name": "red_stained_glass", "x": 1536, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_pressure_plate", + "name": "red_stained_glass_pane", "x": 1792, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_slab", + "name": "red_terracotta", "x": 2048, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_stairs", + "name": "red_tulip", "x": 2304, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_blackstone_wall", + "name": "red_wool", "x": 2560, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_deepslate", + "name": "red_wool_slab", "x": 2816, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_deepslate_slab", + "name": "red_wool_stairs", "x": 3072, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_deepslate_stairs", + "name": "redstone", "x": 3328, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_deepslate_wall", + "name": "redstone_block", "x": 3584, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_diorite", + "name": "redstone_lamp", "x": 3840, "y": 19712, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_diorite_slab", + "name": "redstone_ore", "x": 0, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_diorite_stairs", + "name": "redstone_torch", "x": 256, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_granite", + "name": "reinforced_deepslate", "x": 512, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_granite_slab", + "name": "repeater", "x": 768, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_granite_stairs", + "name": "repeating_command_block", "x": 1024, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_tuff", + "name": "resin_block", "x": 1280, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_tuff_slab", + "name": "resin_brick", "x": 1536, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_tuff_stairs", + "name": "resin_brick_slab", "x": 1792, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "polished_tuff_wall", + "name": "resin_brick_stairs", "x": 2048, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "popped_chorus_fruit", + "name": "resin_brick_wall", "x": 2304, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "poppy", + "name": "resin_bricks", "x": 2560, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "porkchop", + "name": "resin_clump", "x": 2816, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potato", + "name": "respawn_anchor", "x": 3072, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potatoes", + "name": "rib_armor_trim_smithing_template", "x": 3328, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potion", + "name": "rooted_dirt", "x": 3584, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_acacia_sapling", + "name": "rose_bush", "x": 3840, "y": 19968, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_allium", + "name": "rotten_flesh", "x": 0, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_azalea_bush", + "name": "saddle", "x": 256, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_azure_bluet", + "name": "salmon", "x": 512, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_bamboo", + "name": "salmon_bucket", "x": 768, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_birch_sapling", + "name": "salmon_spawn_egg", "x": 1024, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_blue_orchid", + "name": "sand", "x": 1280, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_brown_mushroom", + "name": "sandstone", "x": 1536, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_cactus", + "name": "sandstone_slab", "x": 1792, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_cherry_sapling", + "name": "sandstone_stairs", "x": 2048, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_closed_eyeblossom", + "name": "sandstone_wall", "x": 2304, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_cornflower", + "name": "savanna_village_map", "x": 2560, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_crimson_fungus", + "name": "scaffolding", "x": 2816, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_crimson_roots", + "name": "scrape_pottery_sherd", "x": 3072, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_dandelion", + "name": "sculk", "x": 3328, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_dark_oak_sapling", + "name": "sculk_catalyst", "x": 3584, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_dead_bush", + "name": "sculk_sensor", "x": 3840, "y": 20224, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_fern", + "name": "sculk_shrieker", "x": 0, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_flowering_azalea_bush", + "name": "sculk_vein", "x": 256, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_jungle_sapling", + "name": "sea_lantern", "x": 512, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_lily_of_the_valley", + "name": "sea_pickle", "x": 768, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_mangrove_propagule", + "name": "seagrass", "x": 1024, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_oak_sapling", + "name": "sentry_armor_trim_smithing_template", "x": 1280, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_open_eyeblossom", + "name": "shaper_armor_trim_smithing_template", "x": 1536, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_orange_tulip", + "name": "sheaf_pottery_sherd", "x": 1792, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_oxeye_daisy", + "name": "shears", "x": 2048, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_pale_oak_sapling", + "name": "sheep_spawn_egg", "x": 2304, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_pink_tulip", + "name": "shelf_mushroom", "x": 2560, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_poppy", + "name": "shelter_pottery_sherd", "x": 2816, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_red_mushroom", + "name": "shield", "x": 3072, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_red_tulip", + "name": "short_dry_grass", "x": 3328, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_spruce_sapling", + "name": "short_grass", "x": 3584, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_torchflower", + "name": "shroomlight", "x": 3840, "y": 20480, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_warped_fungus", + "name": "shulker_box", "x": 0, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_warped_roots", + "name": "shulker_shell", "x": 256, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_white_tulip", + "name": "shulker_spawn_egg", "x": 512, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "potted_wither_rose", + "name": "silence_armor_trim_smithing_template", "x": 768, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "powder_snow", + "name": "silverfish_spawn_egg", "x": 1024, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "powder_snow_bucket", + "name": "skeleton_horse_spawn_egg", "x": 1280, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "powder_snow_cauldron", + "name": "skeleton_skull", "x": 1536, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "powered_rail", + "name": "skeleton_spawn_egg", "x": 1792, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine", + "name": "skull_banner_pattern", "x": 2048, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_brick_slab", + "name": "skull_pottery_sherd", "x": 2304, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_brick_stairs", + "name": "slime_ball", "x": 2560, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_bricks", + "name": "slime_block", "x": 2816, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_crystals", + "name": "slime_spawn_egg", "x": 3072, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_shard", + "name": "small_amethyst_bud", "x": 3328, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_slab", + "name": "small_dripleaf", "x": 3584, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_stairs", + "name": "smithing_table", "x": 3840, "y": 20736, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prismarine_wall", + "name": "smoker", "x": 0, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "prize_pottery_sherd", + "name": "smooth_basalt", "x": 256, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pufferfish", + "name": "smooth_quartz", "x": 512, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pufferfish_bucket", + "name": "smooth_quartz_slab", "x": 768, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pufferfish_spawn_egg", + "name": "smooth_quartz_stairs", "x": 1024, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pumpkin", + "name": "smooth_red_sandstone", "x": 1280, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pumpkin_pie", + "name": "smooth_red_sandstone_slab", "x": 1536, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pumpkin_seeds", + "name": "smooth_red_sandstone_stairs", "x": 1792, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "pumpkin_stem", + "name": "smooth_sandstone", "x": 2048, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_banner", + "name": "smooth_sandstone_slab", "x": 2304, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_bed", + "name": "smooth_sandstone_stairs", "x": 2560, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_bundle", + "name": "smooth_stone", "x": 2816, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_bundle_open_back", + "name": "smooth_stone_slab", "x": 3072, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_bundle_open_front", + "name": "sniffer_egg", "x": 3328, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_candle", + "name": "sniffer_spawn_egg", "x": 3584, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_candle_cake", + "name": "snort_pottery_sherd", "x": 3840, "y": 20992, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_carpet", + "name": "snout_armor_trim_smithing_template", "x": 0, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_concrete", + "name": "snow", "x": 256, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_concrete_powder", + "name": "snow_block", "x": 512, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_dye", + "name": "snow_golem_spawn_egg", "x": 768, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_glazed_terracotta", + "name": "snowball", "x": 1024, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_harness", + "name": "snowy_village_map", "x": 1280, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_shulker_box", + "name": "soul_campfire", "x": 1536, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_stained_glass", + "name": "soul_lantern", "x": 1792, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_stained_glass_pane", + "name": "soul_sand", "x": 2048, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_terracotta", + "name": "soul_soil", "x": 2304, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_wall_banner", + "name": "soul_torch", "x": 2560, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purple_wool", + "name": "spawner", "x": 2816, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purpur_block", + "name": "spectral_arrow", "x": 3072, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purpur_pillar", + "name": "spider_eye", "x": 3328, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purpur_slab", + "name": "spider_spawn_egg", "x": 3584, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "purpur_stairs", + "name": "spire_armor_trim_smithing_template", "x": 3840, "y": 21248, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "quartz", + "name": "splash_potion", "x": 0, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "quartz_block", + "name": "sponge", "x": 256, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "quartz_bricks", + "name": "spore_blossom", "x": 512, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "quartz_pillar", + "name": "spruce_boat", "x": 768, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "quartz_slab", + "name": "spruce_button", "x": 1024, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "quartz_stairs", + "name": "spruce_chest_boat", "x": 1280, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rabbit", + "name": "spruce_door", "x": 1536, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rabbit_foot", + "name": "spruce_fence", "x": 1792, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rabbit_hide", + "name": "spruce_fence_gate", "x": 2048, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rabbit_spawn_egg", + "name": "spruce_hanging_sign", "x": 2304, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rabbit_stew", + "name": "spruce_leaves", "x": 2560, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rail", + "name": "spruce_log", "x": 2816, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raiser_armor_trim_smithing_template", + "name": "spruce_planks", "x": 3072, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "ravager_spawn_egg", + "name": "spruce_pressure_plate", "x": 3328, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raw_copper", + "name": "spruce_sapling", "x": 3584, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raw_copper_block", + "name": "spruce_shelf", "x": 3840, "y": 21504, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raw_gold", + "name": "spruce_sign", "x": 0, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raw_gold_block", + "name": "spruce_slab", "x": 256, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raw_iron", + "name": "spruce_stairs", "x": 512, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "raw_iron_block", + "name": "spruce_trapdoor", "x": 768, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass", + "name": "spruce_wood", "x": 1024, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_00", + "name": "spyglass", "x": 1280, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_01", + "name": "squid_spawn_egg", "x": 1536, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_02", + "name": "stick", "x": 1792, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_03", + "name": "sticky_piston", "x": 2048, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_04", + "name": "stone", "x": 2304, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_05", + "name": "stone_axe", "x": 2560, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_06", + "name": "stone_brick_slab", "x": 2816, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_07", + "name": "stone_brick_stairs", "x": 3072, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_08", + "name": "stone_brick_wall", "x": 3328, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_09", + "name": "stone_bricks", "x": 3584, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_10", + "name": "stone_button", "x": 3840, "y": 21760, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_11", + "name": "stone_hoe", "x": 0, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_12", + "name": "stone_pickaxe", "x": 256, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_13", + "name": "stone_pressure_plate", "x": 512, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_14", + "name": "stone_shovel", "x": 768, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_15", + "name": "stone_slab", "x": 1024, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_16", + "name": "stone_spear", "x": 1280, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_17", + "name": "stone_stairs", "x": 1536, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_18", + "name": "stone_sword", "x": 1792, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_19", + "name": "stonecutter", "x": 2048, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_20", + "name": "straw_bed", "x": 2304, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_21", + "name": "stray_spawn_egg", "x": 2560, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_22", + "name": "strider_spawn_egg", "x": 2816, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_23", + "name": "string", "x": 3072, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_24", + "name": "stripped_acacia_log", "x": 3328, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_25", + "name": "stripped_acacia_wood", "x": 3584, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_26", + "name": "stripped_bamboo_block", "x": 3840, "y": 22016, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_27", + "name": "stripped_birch_log", "x": 0, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_28", + "name": "stripped_birch_wood", "x": 256, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_29", + "name": "stripped_cherry_log", "x": 512, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_30", + "name": "stripped_cherry_wood", "x": 768, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "recovery_compass_31", + "name": "stripped_crimson_hyphae", "x": 1024, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_banner", + "name": "stripped_crimson_stem", "x": 1280, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_bed", + "name": "stripped_dark_oak_log", "x": 1536, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_bundle", + "name": "stripped_dark_oak_wood", "x": 1792, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_bundle_open_back", + "name": "stripped_jungle_log", "x": 2048, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_bundle_open_front", + "name": "stripped_jungle_wood", "x": 2304, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_candle", + "name": "stripped_mangrove_log", "x": 2560, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_candle_cake", + "name": "stripped_mangrove_wood", "x": 2816, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_carpet", + "name": "stripped_oak_log", "x": 3072, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_concrete", + "name": "stripped_oak_wood", "x": 3328, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_concrete_powder", + "name": "stripped_pale_oak_log", "x": 3584, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_dye", + "name": "stripped_pale_oak_wood", "x": 3840, "y": 22272, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_glazed_terracotta", + "name": "stripped_poplar_log", "x": 0, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_harness", + "name": "stripped_poplar_wood", "x": 256, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_mushroom", + "name": "stripped_spruce_log", "x": 512, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_mushroom_block", + "name": "stripped_spruce_wood", "x": 768, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_nether_brick_slab", + "name": "stripped_warped_hyphae", "x": 1024, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_nether_brick_stairs", + "name": "stripped_warped_stem", "x": 1280, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_nether_brick_wall", + "name": "structure_block", "x": 1536, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_nether_bricks", + "name": "structure_void", "x": 1792, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_sand", + "name": "sugar", "x": 2048, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_sandstone", + "name": "sugar_cane", "x": 2304, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_sandstone_slab", + "name": "sulfur", "x": 2560, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_sandstone_stairs", + "name": "sulfur_brick_slab", "x": 2816, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_sandstone_wall", + "name": "sulfur_brick_stairs", "x": 3072, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_shulker_box", + "name": "sulfur_brick_wall", "x": 3328, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_stained_glass", + "name": "sulfur_bricks", "x": 3584, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_stained_glass_pane", + "name": "sulfur_cube_bucket", "x": 3840, "y": 22528, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_terracotta", + "name": "sulfur_cube_spawn_egg", "x": 0, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_tulip", + "name": "sulfur_slab", "x": 256, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_wall_banner", + "name": "sulfur_spike", "x": 512, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "red_wool", + "name": "sulfur_stairs", "x": 768, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone", + "name": "sulfur_wall", "x": 1024, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone_block", + "name": "sunflower", "x": 1280, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone_lamp", + "name": "suspicious_gravel", "x": 1536, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone_ore", + "name": "suspicious_sand", "x": 1792, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone_torch", + "name": "suspicious_stew", "x": 2048, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone_wall_torch", + "name": "swamp_hut_map", "x": 2304, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "redstone_wire", + "name": "sweet_berries", "x": 2560, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "reinforced_deepslate", + "name": "tadpole_bucket", "x": 2816, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "repeater", + "name": "tadpole_spawn_egg", "x": 3072, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "repeating_command_block", + "name": "taiga_village_map", "x": 3328, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_block", + "name": "tall_dry_grass", "x": 3584, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_brick", + "name": "tall_grass", "x": 3840, "y": 22784, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_brick_slab", + "name": "target", "x": 0, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_brick_stairs", + "name": "terracotta", "x": 256, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_brick_wall", + "name": "test_block", "x": 512, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_bricks", + "name": "test_instance_block", "x": 768, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "resin_clump", + "name": "tide_armor_trim_smithing_template", "x": 1024, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "respawn_anchor", + "name": "tinted_glass", "x": 1280, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rib_armor_trim_smithing_template", + "name": "tipped_arrow", "x": 1536, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rooted_dirt", + "name": "tnt", "x": 1792, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rose_bush", + "name": "tnt_minecart", "x": 2048, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "rotten_flesh", + "name": "torch", "x": 2304, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "saddle", + "name": "torchflower", "x": 2560, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "salmon", + "name": "torchflower_seeds", "x": 2816, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "salmon_bucket", + "name": "totem_of_undying", "x": 3072, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "salmon_spawn_egg", + "name": "trader_llama_spawn_egg", "x": 3328, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sand", + "name": "trapped_chest", "x": 3584, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sandstone", + "name": "trial_key", "x": 3840, "y": 23040, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sandstone_slab", + "name": "trial_spawner", "x": 0, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sandstone_stairs", + "name": "trident", "x": 256, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sandstone_wall", + "name": "tripwire_hook", "x": 512, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "scaffolding", + "name": "tropical_fish", "x": 768, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "scrape_pottery_sherd", + "name": "tropical_fish_bucket", "x": 1024, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sculk", + "name": "tropical_fish_spawn_egg", "x": 1280, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sculk_catalyst", + "name": "tube_coral", "x": 1536, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sculk_sensor", + "name": "tube_coral_block", "x": 1792, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sculk_shrieker", + "name": "tube_coral_fan", "x": 2048, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sculk_vein", + "name": "tuff", "x": 2304, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sea_lantern", + "name": "tuff_brick_slab", "x": 2560, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sea_pickle", + "name": "tuff_brick_stairs", "x": 2816, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "seagrass", + "name": "tuff_brick_wall", "x": 3072, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sentry_armor_trim_smithing_template", + "name": "tuff_bricks", "x": 3328, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shaper_armor_trim_smithing_template", + "name": "tuff_slab", "x": 3584, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sheaf_pottery_sherd", + "name": "tuff_stairs", "x": 3840, "y": 23296, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shears", + "name": "tuff_wall", "x": 0, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sheep_spawn_egg", + "name": "turtle_egg", "x": 256, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shelter_pottery_sherd", + "name": "turtle_helmet", "x": 512, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shield", + "name": "turtle_scute", "x": 768, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shield_blocking", + "name": "turtle_spawn_egg", "x": 1024, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "short_dry_grass", + "name": "twisting_vines", "x": 1280, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "short_grass", + "name": "vault", "x": 1536, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shroomlight", + "name": "verdant_froglight", "x": 1792, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shulker_box", + "name": "vex_armor_trim_smithing_template", "x": 2048, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shulker_shell", + "name": "vex_spawn_egg", "x": 2304, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "shulker_spawn_egg", + "name": "villager_spawn_egg", "x": 2560, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "silence_armor_trim_smithing_template", + "name": "vindicator_spawn_egg", "x": 2816, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "silverfish_spawn_egg", + "name": "vine", "x": 3072, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "skeleton_horse_spawn_egg", + "name": "wandering_trader_spawn_egg", "x": 3328, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "skeleton_skull", + "name": "ward_armor_trim_smithing_template", "x": 3584, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "skeleton_spawn_egg", + "name": "warden_spawn_egg", "x": 3840, "y": 23552, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "skeleton_wall_skull", + "name": "warm_ocean_ruins_map", "x": 0, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "skull_banner_pattern", + "name": "warped_button", "x": 256, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "skull_pottery_sherd", + "name": "warped_door", "x": 512, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "slime_ball", + "name": "warped_fence", "x": 768, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "slime_block", + "name": "warped_fence_gate", "x": 1024, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "slime_spawn_egg", + "name": "warped_fungus", "x": 1280, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "small_amethyst_bud", + "name": "warped_fungus_on_a_stick", "x": 1536, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "small_dripleaf", + "name": "warped_hanging_sign", "x": 1792, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smithing_table", + "name": "warped_hyphae", "x": 2048, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smoker", + "name": "warped_nylium", "x": 2304, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_basalt", + "name": "warped_planks", "x": 2560, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_quartz", + "name": "warped_pressure_plate", "x": 2816, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_quartz_slab", + "name": "warped_roots", "x": 3072, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_quartz_stairs", + "name": "warped_shelf", "x": 3328, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_red_sandstone", + "name": "warped_sign", "x": 3584, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_red_sandstone_slab", + "name": "warped_slab", "x": 3840, "y": 23808, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_red_sandstone_stairs", + "name": "warped_stairs", "x": 0, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_sandstone", + "name": "warped_stem", "x": 256, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_sandstone_slab", + "name": "warped_trapdoor", "x": 512, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_sandstone_stairs", + "name": "warped_wart_block", "x": 768, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_stone", + "name": "water_bucket", "x": 1024, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "smooth_stone_slab", + "name": "waxed_chiseled_copper", "x": 1280, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sniffer_egg", + "name": "waxed_copper_bars", "x": 1536, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sniffer_spawn_egg", + "name": "waxed_copper_block", "x": 1792, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "snort_pottery_sherd", + "name": "waxed_copper_bulb", "x": 2048, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "snout_armor_trim_smithing_template", + "name": "waxed_copper_chain", "x": 2304, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "snow", + "name": "waxed_copper_chest", "x": 2560, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "snow_block", + "name": "waxed_copper_door", "x": 2816, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "snow_golem_spawn_egg", + "name": "waxed_copper_golem_statue", "x": 3072, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "snowball", + "name": "waxed_copper_grate", "x": 3328, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_campfire", + "name": "waxed_copper_lantern", "x": 3584, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_fire", + "name": "waxed_copper_trapdoor", "x": 3840, "y": 24064, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_lantern", + "name": "waxed_cut_copper", "x": 0, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_sand", + "name": "waxed_cut_copper_slab", "x": 256, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_soil", + "name": "waxed_cut_copper_stairs", "x": 512, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_torch", + "name": "waxed_exposed_chiseled_copper", "x": 768, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "soul_wall_torch", + "name": "waxed_exposed_copper", "x": 1024, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spawner", + "name": "waxed_exposed_copper_bars", "x": 1280, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spear_in_hand", + "name": "waxed_exposed_copper_bulb", "x": 1536, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spectral_arrow", + "name": "waxed_exposed_copper_chain", "x": 1792, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spider_eye", + "name": "waxed_exposed_copper_chest", "x": 2048, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spider_spawn_egg", + "name": "waxed_exposed_copper_door", "x": 2304, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spire_armor_trim_smithing_template", + "name": "waxed_exposed_copper_golem_statue", "x": 2560, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "splash_potion", + "name": "waxed_exposed_copper_grate", "x": 2816, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sponge", + "name": "waxed_exposed_copper_lantern", "x": 3072, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spore_blossom", + "name": "waxed_exposed_copper_trapdoor", "x": 3328, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_boat", + "name": "waxed_exposed_cut_copper", "x": 3584, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_button", + "name": "waxed_exposed_cut_copper_slab", "x": 3840, "y": 24320, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_chest_boat", + "name": "waxed_exposed_cut_copper_stairs", "x": 0, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_door", + "name": "waxed_exposed_lightning_rod", "x": 256, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_fence", + "name": "waxed_lightning_rod", "x": 512, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_fence_gate", + "name": "waxed_oxidized_chiseled_copper", "x": 768, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_hanging_sign", + "name": "waxed_oxidized_copper", "x": 1024, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_leaves", + "name": "waxed_oxidized_copper_bars", "x": 1280, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_log", + "name": "waxed_oxidized_copper_bulb", "x": 1536, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_planks", + "name": "waxed_oxidized_copper_chain", "x": 1792, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_pressure_plate", + "name": "waxed_oxidized_copper_chest", "x": 2048, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_sapling", + "name": "waxed_oxidized_copper_door", "x": 2304, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_shelf", + "name": "waxed_oxidized_copper_golem_statue", "x": 2560, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_sign", + "name": "waxed_oxidized_copper_grate", "x": 2816, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_slab", + "name": "waxed_oxidized_copper_lantern", "x": 3072, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_stairs", + "name": "waxed_oxidized_copper_trapdoor", "x": 3328, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_trapdoor", + "name": "waxed_oxidized_cut_copper", "x": 3584, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_wall_sign", + "name": "waxed_oxidized_cut_copper_slab", "x": 3840, "y": 24576, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spruce_wood", + "name": "waxed_oxidized_cut_copper_stairs", "x": 0, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spyglass", + "name": "waxed_oxidized_lightning_rod", "x": 256, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "spyglass_in_hand", + "name": "waxed_weathered_chiseled_copper", "x": 512, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "squid_spawn_egg", + "name": "waxed_weathered_copper", "x": 768, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stick", + "name": "waxed_weathered_copper_bars", "x": 1024, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sticky_piston", + "name": "waxed_weathered_copper_bulb", "x": 1280, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone", + "name": "waxed_weathered_copper_chain", "x": 1536, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_axe", + "name": "waxed_weathered_copper_chest", "x": 1792, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_brick_slab", + "name": "waxed_weathered_copper_door", "x": 2048, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_brick_stairs", + "name": "waxed_weathered_copper_golem_statue", "x": 2304, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_brick_wall", + "name": "waxed_weathered_copper_grate", "x": 2560, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_bricks", + "name": "waxed_weathered_copper_lantern", "x": 2816, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_button", + "name": "waxed_weathered_copper_trapdoor", "x": 3072, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_hoe", + "name": "waxed_weathered_cut_copper", "x": 3328, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_pickaxe", + "name": "waxed_weathered_cut_copper_slab", "x": 3584, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_pressure_plate", + "name": "waxed_weathered_cut_copper_stairs", "x": 3840, "y": 24832, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_shovel", + "name": "waxed_weathered_lightning_rod", "x": 0, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_slab", + "name": "wayfinder_armor_trim_smithing_template", "x": 256, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_spear", + "name": "weathered_chiseled_copper", "x": 512, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_spear_in_hand", + "name": "weathered_copper", "x": 768, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_stairs", + "name": "weathered_copper_bars", "x": 1024, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stone_sword", + "name": "weathered_copper_bulb", "x": 1280, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stonecutter", + "name": "weathered_copper_chain", "x": 1536, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stray_spawn_egg", + "name": "weathered_copper_chest", "x": 1792, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "strider_spawn_egg", + "name": "weathered_copper_door", "x": 2048, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "string", + "name": "weathered_copper_golem_statue", "x": 2304, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_acacia_log", + "name": "weathered_copper_grate", "x": 2560, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_acacia_wood", + "name": "weathered_copper_lantern", "x": 2816, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_bamboo_block", + "name": "weathered_copper_trapdoor", "x": 3072, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_birch_log", + "name": "weathered_cut_copper", "x": 3328, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_birch_wood", + "name": "weathered_cut_copper_slab", "x": 3584, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_cherry_log", + "name": "weathered_cut_copper_stairs", "x": 3840, "y": 25088, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_cherry_wood", + "name": "weathered_lightning_rod", "x": 0, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_crimson_hyphae", + "name": "weeping_vines", "x": 256, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_crimson_stem", + "name": "wet_sponge", "x": 512, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_dark_oak_log", + "name": "wheat", "x": 768, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_dark_oak_wood", + "name": "wheat_seeds", "x": 1024, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_jungle_log", + "name": "white_banner", "x": 1280, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_jungle_wood", + "name": "white_bed", "x": 1536, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_mangrove_log", + "name": "white_bundle", "x": 1792, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_mangrove_wood", + "name": "white_candle", "x": 2048, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_oak_log", + "name": "white_carpet", "x": 2304, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_oak_wood", + "name": "white_concrete", "x": 2560, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_pale_oak_log", + "name": "white_concrete_powder", "x": 2816, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_pale_oak_wood", + "name": "white_concrete_slab", "x": 3072, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_spruce_log", + "name": "white_concrete_stairs", "x": 3328, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_spruce_wood", + "name": "white_cushion", "x": 3584, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_warped_hyphae", + "name": "white_dye", "x": 3840, "y": 25344, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "stripped_warped_stem", + "name": "white_glazed_terracotta", "x": 0, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "structure_block", + "name": "white_harness", "x": 256, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "structure_void", + "name": "white_shulker_box", "x": 512, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sugar", + "name": "white_stained_glass", "x": 768, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sugar_cane", + "name": "white_stained_glass_pane", "x": 1024, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sunflower", + "name": "white_terracotta", "x": 1280, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "suspicious_gravel", + "name": "white_tulip", "x": 1536, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "suspicious_sand", + "name": "white_wool", "x": 1792, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "suspicious_stew", + "name": "white_wool_slab", "x": 2048, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sweet_berries", + "name": "white_wool_stairs", "x": 2304, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "sweet_berry_bush", + "name": "wild_armor_trim_smithing_template", "x": 2560, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tadpole_bucket", + "name": "wildflowers", "x": 2816, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tadpole_spawn_egg", + "name": "wind_charge", "x": 3072, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tall_dry_grass", + "name": "witch_spawn_egg", "x": 3328, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tall_grass", + "name": "wither_rose", "x": 3584, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tall_seagrass", + "name": "wither_skeleton_skull", "x": 3840, "y": 25600, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "target", + "name": "wither_skeleton_spawn_egg", "x": 0, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "terracotta", + "name": "wither_spawn_egg", "x": 256, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "test_block", + "name": "wolf_armor", "x": 512, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "test_instance_block", + "name": "wolf_spawn_egg", "x": 768, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tide_armor_trim_smithing_template", + "name": "wooden_axe", "x": 1024, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tinted_glass", + "name": "wooden_hoe", "x": 1280, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tipped_arrow", + "name": "wooden_pickaxe", "x": 1536, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tnt", + "name": "wooden_shovel", "x": 1792, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tnt_minecart", + "name": "wooden_spear", "x": 2048, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tooting_goat_horn", + "name": "wooden_sword", "x": 2304, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "torch", + "name": "woodland_mansion_map", "x": 2560, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "torchflower", + "name": "writable_book", "x": 2816, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "torchflower_crop", + "name": "written_book", "x": 3072, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "torchflower_seeds", + "name": "yellow_banner", "x": 3328, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "totem_of_undying", + "name": "yellow_bed", "x": 3584, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trader_llama_spawn_egg", + "name": "yellow_bundle", "x": 3840, "y": 25856, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trapped_chest", + "name": "yellow_candle", "x": 0, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trial_key", + "name": "yellow_carpet", "x": 256, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trial_spawner", + "name": "yellow_concrete", "x": 512, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trident", + "name": "yellow_concrete_powder", "x": 768, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trident_in_hand", + "name": "yellow_concrete_slab", "x": 1024, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "trident_throwing", + "name": "yellow_concrete_stairs", "x": 1280, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tripwire", + "name": "yellow_cushion", "x": 1536, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tripwire_hook", + "name": "yellow_dye", "x": 1792, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tropical_fish", + "name": "yellow_glazed_terracotta", "x": 2048, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tropical_fish_bucket", + "name": "yellow_harness", "x": 2304, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tropical_fish_spawn_egg", + "name": "yellow_poplar_leaves", "x": 2560, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tube_coral", + "name": "yellow_shulker_box", "x": 2816, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tube_coral_block", + "name": "yellow_stained_glass", "x": 3072, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tube_coral_fan", + "name": "yellow_stained_glass_pane", "x": 3328, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tube_coral_wall_fan", + "name": "yellow_terracotta", "x": 3584, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff", + "name": "yellow_wool", "x": 3840, "y": 26112, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_brick_slab", + "name": "yellow_wool_slab", "x": 0, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_brick_stairs", + "name": "yellow_wool_stairs", "x": 256, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_brick_wall", + "name": "zoglin_spawn_egg", "x": 512, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_bricks", + "name": "zombie_head", "x": 768, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_slab", + "name": "zombie_horse_spawn_egg", "x": 1024, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_stairs", + "name": "zombie_nautilus_spawn_egg", "x": 1280, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "tuff_wall", + "name": "zombie_spawn_egg", "x": 1536, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "turtle_egg", + "name": "zombie_villager_spawn_egg", "x": 1792, "y": 26368, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" }, { - "name": "turtle_helmet", + "name": "zombified_piglin_spawn_egg", "x": 2048, "y": 26368, - "size": 256 - }, - { - "name": "turtle_scute", - "x": 2304, - "y": 26368, - "size": 256 - }, - { - "name": "turtle_spawn_egg", - "x": 2560, - "y": 26368, - "size": 256 - }, - { - "name": "twisting_vines", - "x": 2816, - "y": 26368, - "size": 256 - }, - { - "name": "twisting_vines_plant", - "x": 3072, - "y": 26368, - "size": 256 - }, - { - "name": "vault", - "x": 3328, - "y": 26368, - "size": 256 - }, - { - "name": "verdant_froglight", - "x": 3584, - "y": 26368, - "size": 256 - }, - { - "name": "vex_armor_trim_smithing_template", - "x": 3840, - "y": 26368, - "size": 256 - }, - { - "name": "vex_spawn_egg", - "x": 0, - "y": 26624, - "size": 256 - }, - { - "name": "villager_spawn_egg", - "x": 256, - "y": 26624, - "size": 256 - }, - { - "name": "vindicator_spawn_egg", - "x": 512, - "y": 26624, - "size": 256 - }, - { - "name": "vine", - "x": 768, - "y": 26624, - "size": 256 - }, - { - "name": "wall_torch", - "x": 1024, - "y": 26624, - "size": 256 - }, - { - "name": "wandering_trader_spawn_egg", - "x": 1280, - "y": 26624, - "size": 256 - }, - { - "name": "ward_armor_trim_smithing_template", - "x": 1536, - "y": 26624, - "size": 256 - }, - { - "name": "warden_spawn_egg", - "x": 1792, - "y": 26624, - "size": 256 - }, - { - "name": "warped_button", - "x": 2048, - "y": 26624, - "size": 256 - }, - { - "name": "warped_door", - "x": 2304, - "y": 26624, - "size": 256 - }, - { - "name": "warped_fence", - "x": 2560, - "y": 26624, - "size": 256 - }, - { - "name": "warped_fence_gate", - "x": 2816, - "y": 26624, - "size": 256 - }, - { - "name": "warped_fungus", - "x": 3072, - "y": 26624, - "size": 256 - }, - { - "name": "warped_fungus_on_a_stick", - "x": 3328, - "y": 26624, - "size": 256 - }, - { - "name": "warped_hanging_sign", - "x": 3584, - "y": 26624, - "size": 256 - }, - { - "name": "warped_hyphae", - "x": 3840, - "y": 26624, - "size": 256 - }, - { - "name": "warped_nylium", - "x": 0, - "y": 26880, - "size": 256 - }, - { - "name": "warped_planks", - "x": 256, - "y": 26880, - "size": 256 - }, - { - "name": "warped_pressure_plate", - "x": 512, - "y": 26880, - "size": 256 - }, - { - "name": "warped_roots", - "x": 768, - "y": 26880, - "size": 256 - }, - { - "name": "warped_shelf", - "x": 1024, - "y": 26880, - "size": 256 - }, - { - "name": "warped_sign", - "x": 1280, - "y": 26880, - "size": 256 - }, - { - "name": "warped_slab", - "x": 1536, - "y": 26880, - "size": 256 - }, - { - "name": "warped_stairs", - "x": 1792, - "y": 26880, - "size": 256 - }, - { - "name": "warped_stem", - "x": 2048, - "y": 26880, - "size": 256 - }, - { - "name": "warped_trapdoor", - "x": 2304, - "y": 26880, - "size": 256 - }, - { - "name": "warped_wall_sign", - "x": 2560, - "y": 26880, - "size": 256 - }, - { - "name": "warped_wart_block", - "x": 2816, - "y": 26880, - "size": 256 - }, - { - "name": "water_bucket", - "x": 3072, - "y": 26880, - "size": 256 - }, - { - "name": "water_cauldron", - "x": 3328, - "y": 26880, - "size": 256 - }, - { - "name": "waxed_chiseled_copper", - "x": 3584, - "y": 26880, - "size": 256 - }, - { - "name": "waxed_copper_bars", - "x": 3840, - "y": 26880, - "size": 256 - }, - { - "name": "waxed_copper_block", - "x": 0, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_bulb", - "x": 256, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_chain", - "x": 512, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_chest", - "x": 768, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_door", - "x": 1024, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_golem_statue", - "x": 1280, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_grate", - "x": 1536, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_lantern", - "x": 1792, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_copper_trapdoor", - "x": 2048, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_cut_copper", - "x": 2304, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_cut_copper_slab", - "x": 2560, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_cut_copper_stairs", - "x": 2816, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_exposed_chiseled_copper", - "x": 3072, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_exposed_copper", - "x": 3328, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_exposed_copper_bars", - "x": 3584, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_exposed_copper_bulb", - "x": 3840, - "y": 27136, - "size": 256 - }, - { - "name": "waxed_exposed_copper_chain", - "x": 0, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_copper_chest", - "x": 256, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_copper_door", - "x": 512, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_copper_golem_statue", - "x": 768, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_copper_grate", - "x": 1024, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_copper_lantern", - "x": 1280, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_copper_trapdoor", - "x": 1536, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_cut_copper", - "x": 1792, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_cut_copper_slab", - "x": 2048, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_cut_copper_stairs", - "x": 2304, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_exposed_lightning_rod", - "x": 2560, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_lightning_rod", - "x": 2816, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_oxidized_chiseled_copper", - "x": 3072, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_oxidized_copper", - "x": 3328, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_bars", - "x": 3584, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_bulb", - "x": 3840, - "y": 27392, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_chain", - "x": 0, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_chest", - "x": 256, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_door", - "x": 512, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_golem_statue", - "x": 768, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_grate", - "x": 1024, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_lantern", - "x": 1280, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_copper_trapdoor", - "x": 1536, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_cut_copper", - "x": 1792, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_cut_copper_slab", - "x": 2048, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_cut_copper_stairs", - "x": 2304, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_oxidized_lightning_rod", - "x": 2560, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_weathered_chiseled_copper", - "x": 2816, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_weathered_copper", - "x": 3072, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_weathered_copper_bars", - "x": 3328, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_weathered_copper_bulb", - "x": 3584, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_weathered_copper_chain", - "x": 3840, - "y": 27648, - "size": 256 - }, - { - "name": "waxed_weathered_copper_chest", - "x": 0, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_copper_door", - "x": 256, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_copper_golem_statue", - "x": 512, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_copper_grate", - "x": 768, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_copper_lantern", - "x": 1024, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_copper_trapdoor", - "x": 1280, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_cut_copper", - "x": 1536, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_cut_copper_slab", - "x": 1792, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_cut_copper_stairs", - "x": 2048, - "y": 27904, - "size": 256 - }, - { - "name": "waxed_weathered_lightning_rod", - "x": 2304, - "y": 27904, - "size": 256 - }, - { - "name": "wayfinder_armor_trim_smithing_template", - "x": 2560, - "y": 27904, - "size": 256 - }, - { - "name": "weathered_chiseled_copper", - "x": 2816, - "y": 27904, - "size": 256 - }, - { - "name": "weathered_copper", - "x": 3072, - "y": 27904, - "size": 256 - }, - { - "name": "weathered_copper_bars", - "x": 3328, - "y": 27904, - "size": 256 - }, - { - "name": "weathered_copper_bulb", - "x": 3584, - "y": 27904, - "size": 256 - }, - { - "name": "weathered_copper_chain", - "x": 3840, - "y": 27904, - "size": 256 - }, - { - "name": "weathered_copper_chest", - "x": 0, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_copper_door", - "x": 256, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_copper_golem_statue", - "x": 512, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_copper_grate", - "x": 768, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_copper_lantern", - "x": 1024, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_copper_trapdoor", - "x": 1280, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_cut_copper", - "x": 1536, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_cut_copper_slab", - "x": 1792, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_cut_copper_stairs", - "x": 2048, - "y": 28160, - "size": 256 - }, - { - "name": "weathered_lightning_rod", - "x": 2304, - "y": 28160, - "size": 256 - }, - { - "name": "weeping_vines", - "x": 2560, - "y": 28160, - "size": 256 - }, - { - "name": "weeping_vines_plant", - "x": 2816, - "y": 28160, - "size": 256 - }, - { - "name": "wet_sponge", - "x": 3072, - "y": 28160, - "size": 256 - }, - { - "name": "wheat", - "x": 3328, - "y": 28160, - "size": 256 - }, - { - "name": "wheat_seeds", - "x": 3584, - "y": 28160, - "size": 256 - }, - { - "name": "white_banner", - "x": 3840, - "y": 28160, - "size": 256 - }, - { - "name": "white_bed", - "x": 0, - "y": 28416, - "size": 256 - }, - { - "name": "white_bundle", - "x": 256, - "y": 28416, - "size": 256 - }, - { - "name": "white_bundle_open_back", - "x": 512, - "y": 28416, - "size": 256 - }, - { - "name": "white_bundle_open_front", - "x": 768, - "y": 28416, - "size": 256 - }, - { - "name": "white_candle", - "x": 1024, - "y": 28416, - "size": 256 - }, - { - "name": "white_candle_cake", - "x": 1280, - "y": 28416, - "size": 256 - }, - { - "name": "white_carpet", - "x": 1536, - "y": 28416, - "size": 256 - }, - { - "name": "white_concrete", - "x": 1792, - "y": 28416, - "size": 256 - }, - { - "name": "white_concrete_powder", - "x": 2048, - "y": 28416, - "size": 256 - }, - { - "name": "white_dye", - "x": 2304, - "y": 28416, - "size": 256 - }, - { - "name": "white_glazed_terracotta", - "x": 2560, - "y": 28416, - "size": 256 - }, - { - "name": "white_harness", - "x": 2816, - "y": 28416, - "size": 256 - }, - { - "name": "white_shulker_box", - "x": 3072, - "y": 28416, - "size": 256 - }, - { - "name": "white_stained_glass", - "x": 3328, - "y": 28416, - "size": 256 - }, - { - "name": "white_stained_glass_pane", - "x": 3584, - "y": 28416, - "size": 256 - }, - { - "name": "white_terracotta", - "x": 3840, - "y": 28416, - "size": 256 - }, - { - "name": "white_tulip", - "x": 0, - "y": 28672, - "size": 256 - }, - { - "name": "white_wall_banner", - "x": 256, - "y": 28672, - "size": 256 - }, - { - "name": "white_wool", - "x": 512, - "y": 28672, - "size": 256 - }, - { - "name": "wild_armor_trim_smithing_template", - "x": 768, - "y": 28672, - "size": 256 - }, - { - "name": "wildflowers", - "x": 1024, - "y": 28672, - "size": 256 - }, - { - "name": "wind_charge", - "x": 1280, - "y": 28672, - "size": 256 - }, - { - "name": "witch_spawn_egg", - "x": 1536, - "y": 28672, - "size": 256 - }, - { - "name": "wither_rose", - "x": 1792, - "y": 28672, - "size": 256 - }, - { - "name": "wither_skeleton_skull", - "x": 2048, - "y": 28672, - "size": 256 - }, - { - "name": "wither_skeleton_spawn_egg", - "x": 2304, - "y": 28672, - "size": 256 - }, - { - "name": "wither_skeleton_wall_skull", - "x": 2560, - "y": 28672, - "size": 256 - }, - { - "name": "wither_spawn_egg", - "x": 2816, - "y": 28672, - "size": 256 - }, - { - "name": "wolf_armor", - "x": 3072, - "y": 28672, - "size": 256 - }, - { - "name": "wolf_armor_dyed", - "x": 3328, - "y": 28672, - "size": 256 - }, - { - "name": "wolf_spawn_egg", - "x": 3584, - "y": 28672, - "size": 256 - }, - { - "name": "wooden_axe", - "x": 3840, - "y": 28672, - "size": 256 - }, - { - "name": "wooden_hoe", - "x": 0, - "y": 28928, - "size": 256 - }, - { - "name": "wooden_pickaxe", - "x": 256, - "y": 28928, - "size": 256 - }, - { - "name": "wooden_shovel", - "x": 512, - "y": 28928, - "size": 256 - }, - { - "name": "wooden_spear", - "x": 768, - "y": 28928, - "size": 256 - }, - { - "name": "wooden_spear_in_hand", - "x": 1024, - "y": 28928, - "size": 256 - }, - { - "name": "wooden_sword", - "x": 1280, - "y": 28928, - "size": 256 - }, - { - "name": "writable_book", - "x": 1536, - "y": 28928, - "size": 256 - }, - { - "name": "written_book", - "x": 1792, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_banner", - "x": 2048, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_bed", - "x": 2304, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_bundle", - "x": 2560, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_bundle_open_back", - "x": 2816, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_bundle_open_front", - "x": 3072, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_candle", - "x": 3328, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_candle_cake", - "x": 3584, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_carpet", - "x": 3840, - "y": 28928, - "size": 256 - }, - { - "name": "yellow_concrete", - "x": 0, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_concrete_powder", - "x": 256, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_dye", - "x": 512, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_glazed_terracotta", - "x": 768, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_harness", - "x": 1024, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_shulker_box", - "x": 1280, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_stained_glass", - "x": 1536, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_stained_glass_pane", - "x": 1792, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_terracotta", - "x": 2048, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_wall_banner", - "x": 2304, - "y": 29184, - "size": 256 - }, - { - "name": "yellow_wool", - "x": 2560, - "y": 29184, - "size": 256 - }, - { - "name": "zoglin_spawn_egg", - "x": 2816, - "y": 29184, - "size": 256 - }, - { - "name": "zombie_head", - "x": 3072, - "y": 29184, - "size": 256 - }, - { - "name": "zombie_horse_spawn_egg", - "x": 3328, - "y": 29184, - "size": 256 - }, - { - "name": "zombie_nautilus_spawn_egg", - "x": 3584, - "y": 29184, - "size": 256 - }, - { - "name": "zombie_spawn_egg", - "x": 3840, - "y": 29184, - "size": 256 - }, - { - "name": "zombie_villager_spawn_egg", - "x": 0, - "y": 29440, - "size": 256 - }, - { - "name": "zombie_wall_head", - "x": 256, - "y": 29440, - "size": 256 - }, - { - "name": "zombified_piglin_spawn_egg", - "x": 512, - "y": 29440, - "size": 256 + "size": 256, + "page": 0, + "atlas": "minecraft_texture_atlas.png" } ] \ No newline at end of file diff --git a/generator/src/main/resources/minecraft/assets/json/overlay_colors.json b/generator/src/main/resources/minecraft/assets/json/overlay_colors.json index 0a44190..693a416 100644 --- a/generator/src/main/resources/minecraft/assets/json/overlay_colors.json +++ b/generator/src/main/resources/minecraft/assets/json/overlay_colors.json @@ -60,35 +60,35 @@ { "name": "armor_trim", "map": { - "-2039584": 0, - "-4144960": 1, - "-6250336": 2, - "-8355712": 3, - "-10461088": 4, - "-12566464": 5, - "-14671840": 6, + "-921103": 0, + "-1973791": 1, + "-3092272": 2, + "-4408132": 3, + "-5921371": 4, + "-7763575": 5, + "-10263709": 6, "-16777216": 7 }, "options": { - "iron_darker": [ - -6115149, - -7695727, - -9472394, - -11050141, - -12760759, - -13550789, - -13945804, - -14866392 + "amethyst": [ + -3567629, + -6660922, + -9680470, + -11389305, + -12441738, + -13230998, + -14414765, + -15268293 ], - "gold": [ - -624, - -1255105, - -2182867, - -5150958, - -6273782, - -8375037, - -9360128, - -11066624 + "copper": [ + -1867156, + -4954035, + -6666452, + -8831960, + -9620448, + -10540264, + -11788272, + -12773365 ], "copper_darker": [ -8699595, @@ -100,15 +100,55 @@ -14085108, -15004152 ], - "netherite": [ - -10856614, - -12305861, - -13554127, - -13687001, - -14475746, - -15067626, - -15725556, - -16185593 + "diamond": [ + -3407883, + -9507630, + -13845848, + -14838118, + -15959923, + -16292488, + -16494241, + -16696249 + ], + "diamond_darker": [ + -15354975, + -16081759, + -16481915, + -16616334, + -16360101, + -16166584, + -16562622, + -16498626 + ], + "emerald": [ + -8194387, + -15808684, + -15622090, + -15697116, + -15830494, + -16162021, + -16560109, + -16630514 + ], + "gold": [ + -624, + -1255105, + -2182867, + -5150958, + -6273782, + -8375037, + -9360128, + -11066624 + ], + "gold_darker": [ + -4023254, + -4553945, + -6070508, + -7780596, + -8375037, + -9360128, + -11066624, + -12707069 ], "iron": [ -3812652, @@ -120,35 +160,15 @@ -11050141, -12168879 ], - "resin": [ - -15532, - -614597, - -1014486, - -1281516, - -2400496, - -3716854, - -5948655, - -8377830 - ], - "amethyst": [ - -3567629, - -6660922, - -9680470, - -11389305, - -12441738, - -13230998, - -14414765, - -15268293 - ], - "diamond": [ - -3407883, - -9507630, - -13845848, - -14838118, - -15959923, - -16292488, - -16494241, - -16696249 + "iron_darker": [ + -6115149, + -7695727, + -9472394, + -11050141, + -12760759, + -13550789, + -13945804, + -14866392 ], "lapis": [ -12489065, @@ -160,35 +180,15 @@ -16179643, -16443850 ], - "redstone": [ - -1695736, - -4382712, - -6875641, - -8908543, - -10155263, - -11399930, - -13236221, - -14875390 - ], - "diamond_darker": [ - -15354975, - -16081759, - -16481915, - -16616334, - -16360101, - -16166584, - -16562622, - -16498626 - ], - "quartz": [ - -856083, - -595233, - -1844284, - -4805226, - -7303552, - -10133162, - -12238020, - -14014430 + "netherite": [ + -10856614, + -12305861, + -13554127, + -13687001, + -14475746, + -15067626, + -15725556, + -16185593 ], "netherite_darker": [ -13752279, @@ -200,35 +200,35 @@ -15462641, -16054007 ], - "gold_darker": [ - -4023254, - -4553945, - -6070508, - -7780596, - -8375037, - -9360128, - -11066624, - -12707069 + "quartz": [ + -856083, + -595233, + -1844284, + -4805226, + -7303552, + -10133162, + -12238020, + -14014430 ], - "copper": [ - -1867156, - -4954035, - -6666452, - -8831960, - -9620448, - -10540264, - -11788272, - -12773365 + "redstone": [ + -1695736, + -4382712, + -6875641, + -8908543, + -10155263, + -11399930, + -13236221, + -14875390 ], - "emerald": [ - -8194387, - -15808684, - -15622090, - -15697116, - -15830494, - -16162021, - -16560109, - -16630514 + "resin": [ + -15532, + -614597, + -1014486, + -1281516, + -2400496, + -3716854, + -5948655, + -8377830 ] }, "allowHexColors": false, diff --git a/generator/src/main/resources/minecraft/assets/json/overlay_coordinates.json b/generator/src/main/resources/minecraft/assets/json/overlay_coordinates.json index dba8532..b53e265 100644 --- a/generator/src/main/resources/minecraft/assets/json/overlay_coordinates.json +++ b/generator/src/main/resources/minecraft/assets/json/overlay_coordinates.json @@ -5,7 +5,9 @@ "y": 0, "size": 256, "type": "NORMAL", - "colorOptions": "leather_armor" + "colorOptions": "leather_armor", + "page": 0, + "atlas": "overlays.png" }, { "name": "leather_chestplate_overlay", @@ -13,7 +15,9 @@ "y": 0, "size": 256, "type": "NORMAL", - "colorOptions": "leather_armor" + "colorOptions": "leather_armor", + "page": 0, + "atlas": "overlays.png" }, { "name": "leather_helmet_overlay", @@ -21,7 +25,9 @@ "y": 0, "size": 256, "type": "NORMAL", - "colorOptions": "leather_armor" + "colorOptions": "leather_armor", + "page": 0, + "atlas": "overlays.png" }, { "name": "leather_horse_armor_overlay", @@ -29,7 +35,9 @@ "y": 0, "size": 256, "type": "NORMAL", - "colorOptions": "leather_armor" + "colorOptions": "leather_armor", + "page": 0, + "atlas": "overlays.png" }, { "name": "leather_leggings_overlay", @@ -37,7 +45,9 @@ "y": 256, "size": 256, "type": "NORMAL", - "colorOptions": "leather_armor" + "colorOptions": "leather_armor", + "page": 0, + "atlas": "overlays.png" }, { "name": "wolf_armor_overlay", @@ -45,7 +55,9 @@ "y": 256, "size": 256, "type": "NORMAL", - "colorOptions": "leather_armor" + "colorOptions": "leather_armor", + "page": 0, + "atlas": "overlays.png" }, { "name": "potion_overlay", @@ -54,7 +66,9 @@ "size": 256, "type": "NORMAL", "colorOptions": "potion", - "colorMode": "OVERLAY" + "colorMode": "OVERLAY", + "page": 0, + "atlas": "overlays.png" }, { "name": "tipped_arrow_head", @@ -63,7 +77,9 @@ "size": 256, "type": "NORMAL", "colorOptions": "effect", - "colorMode": "OVERLAY" + "colorMode": "OVERLAY", + "page": 0, + "atlas": "overlays.png" }, { "name": "firework_star_overlay", @@ -72,6 +88,48 @@ "size": 256, "type": "NORMAL", "colorOptions": "firework", - "colorMode": "OVERLAY" + "colorMode": "OVERLAY", + "page": 0, + "atlas": "overlays.png" + }, + { + "name": "boots_trim", + "x": 256, + "y": 512, + "size": 256, + "type": "MAPPED", + "colorOptions": "armor_trim", + "page": 0, + "atlas": "overlays.png" + }, + { + "name": "chestplate_trim", + "x": 512, + "y": 512, + "size": 256, + "type": "MAPPED", + "colorOptions": "armor_trim", + "page": 0, + "atlas": "overlays.png" + }, + { + "name": "helmet_trim", + "x": 768, + "y": 512, + "size": 256, + "type": "MAPPED", + "colorOptions": "armor_trim", + "page": 0, + "atlas": "overlays.png" + }, + { + "name": "leggings_trim", + "x": 0, + "y": 768, + "size": 256, + "type": "MAPPED", + "colorOptions": "armor_trim", + "page": 0, + "atlas": "overlays.png" } ] \ No newline at end of file diff --git a/generator/src/main/resources/minecraft/assets/spritesheets/minecraft_texture_atlas.png b/generator/src/main/resources/minecraft/assets/spritesheets/minecraft_texture_atlas.png index 435d00b..76667e9 100644 Binary files a/generator/src/main/resources/minecraft/assets/spritesheets/minecraft_texture_atlas.png and b/generator/src/main/resources/minecraft/assets/spritesheets/minecraft_texture_atlas.png differ diff --git a/generator/src/main/resources/minecraft/assets/spritesheets/overlays.png b/generator/src/main/resources/minecraft/assets/spritesheets/overlays.png index 8bfbe5d..7cdc878 100644 Binary files a/generator/src/main/resources/minecraft/assets/spritesheets/overlays.png and b/generator/src/main/resources/minecraft/assets/spritesheets/overlays.png differ diff --git a/generator/src/test/java/net/aerh/imagegenerator/spritesheet/SpritesheetLoaderTest.java b/generator/src/test/java/net/aerh/imagegenerator/spritesheet/SpritesheetLoaderTest.java new file mode 100644 index 0000000..744670c --- /dev/null +++ b/generator/src/test/java/net/aerh/imagegenerator/spritesheet/SpritesheetLoaderTest.java @@ -0,0 +1,418 @@ +package net.aerh.imagegenerator.spritesheet; + +import net.aerh.imagegenerator.exception.GeneratorException; +import net.hypixel.nerdbot.marmalade.functional.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SpritesheetLoaderTest { + + private static final String BASE = "/fixture"; + private static final String COORDINATES_PATH = BASE + "/json/atlas_coordinates.json"; + private static final String PAGE_ZERO_FILE = "minecraft_texture_atlas.png"; + private static final String PAGE_ONE_FILE = "minecraft_texture_atlas-1.png"; + private static final String PAGE_ZERO_PATH = BASE + "/spritesheets/" + PAGE_ZERO_FILE; + private static final String PAGE_ONE_PATH = BASE + "/spritesheets/" + PAGE_ONE_FILE; + private static final int TILE_SIZE = 2; + private static final int RED = 0xFFFF0000; + private static final int GREEN = 0xFF00FF00; + private static final int BLUE = 0xFF0000FF; + private static final int YELLOW = 0xFFFFFF00; + + @TempDir + Path root; + + @BeforeEach + void writeFixture() throws IOException { + writeAtlas(PAGE_ZERO_FILE, RED, GREEN); + writeAtlas(PAGE_ONE_FILE, BLUE, YELLOW); + writeCoordinates( + entry("red", 0, 0, 0, PAGE_ZERO_FILE), + entry("green", TILE_SIZE, 0, 0, PAGE_ZERO_FILE), + entry("blue", 0, 0, 1, PAGE_ONE_FILE), + entry("yellow", TILE_SIZE, 0, 1, PAGE_ONE_FILE) + ); + } + + @Test + void textureOnPageZeroReturnsPixelsFromFirstAtlas() { + CountingLoader loader = new CountingLoader(); + + BufferedImage texture = loader.getTexture("green"); + + assertSolidTile(texture, GREEN); + } + + @Test + void textureOnPageZeroDoesNotOpenPageOneAtlas() { + CountingLoader loader = new CountingLoader(); + + loader.getTexture("red"); + + assertEquals(0, loader.opensOf(PAGE_ONE_PATH), "page 1 opens"); + } + + @Test + void textureOnPageOneReturnsPixelsFromSecondAtlas() { + CountingLoader loader = new CountingLoader(); + + BufferedImage texture = loader.getTexture("yellow"); + + assertSolidTile(texture, YELLOW); + assertEquals(1, loader.opensOf(PAGE_ONE_PATH), "page 1 opens"); + } + + @Test + void unknownNameReturnsNull() { + CountingLoader loader = new CountingLoader(); + + assertNull(loader.getTexture("does_not_exist")); + } + + @Test + void nullNameReturnsNull() { + CountingLoader loader = new CountingLoader(); + + assertNull(loader.getTexture(null)); + } + + @Test + void texturesOnTheSamePageOpenItsAtlasOnce() { + CountingLoader loader = new CountingLoader(); + + loader.getTexture("red"); + loader.getTexture("green"); + + assertEquals(1, loader.opensOf(PAGE_ZERO_PATH), "page 0 opens"); + } + + @Test + void repeatedGetTextureReturnsSameInstance() { + CountingLoader loader = new CountingLoader(); + + BufferedImage first = loader.getTexture("red"); + BufferedImage second = loader.getTexture("red"); + + assertSame(first, second); + } + + @Test + void entriesWithoutAtlasFieldLoadFromDefaultAtlas() throws IOException { + writeCoordinates(entryWithoutAtlas("legacy", TILE_SIZE, 0)); + CountingLoader loader = new CountingLoader(); + + BufferedImage texture = loader.getTexture("legacy"); + + assertSolidTile(texture, GREEN); + assertEquals(1, loader.opensOf(PAGE_ZERO_PATH), "page 0 opens"); + } + + @Test + void missingPageAtlasThrowsGeneratorExceptionNamingPath() throws IOException { + writeCoordinates(entry("ghost", 0, 0, 2, "minecraft_texture_atlas-2.png")); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("ghost")); + + assertTrue(exception.getMessage().contains(BASE + "/spritesheets/minecraft_texture_atlas-2.png"), + exception.getMessage()); + } + + @Test + void missingCoordinatesFileThrowsGeneratorExceptionNamingPath() throws IOException { + Files.delete(fixturePath("/json/atlas_coordinates.json")); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("red")); + + assertTrue(exception.getMessage().contains(COORDINATES_PATH), exception.getMessage()); + } + + @Test + void getImageMapContainsEveryNameWithItsTexture() { + CountingLoader loader = new CountingLoader(); + + Map imageMap = loader.getImageMap(); + + assertEquals(Set.of("red", "green", "blue", "yellow"), imageMap.keySet()); + assertSolidTile(imageMap.get("red"), RED); + assertSolidTile(imageMap.get("green"), GREEN); + assertSolidTile(imageMap.get("blue"), BLUE); + assertSolidTile(imageMap.get("yellow"), YELLOW); + } + + @Test + void getImageMapDecodesEveryPageOnce() { + CountingLoader loader = new CountingLoader(); + + loader.getImageMap(); + + assertEquals(1, loader.opensOf(PAGE_ZERO_PATH), "page 0 opens"); + assertEquals(1, loader.opensOf(PAGE_ONE_PATH), "page 1 opens"); + } + + @Test + void getImageMapReturnsSameInstancesAsGetTexture() { + CountingLoader loader = new CountingLoader(); + + BufferedImage fromGetTexture = loader.getTexture("blue"); + Map imageMap = loader.getImageMap(); + + assertSame(fromGetTexture, imageMap.get("blue")); + } + + @Test + void searchForTextureReturnsSubstringMatchesOnly() { + CountingLoader loader = new CountingLoader(); + + List> matches = loader.searchForTexture("re"); + + assertEquals(List.of("red", "green"), matches.stream().map(Pair::first).toList()); + assertSolidTile(matches.get(0).second(), RED); + assertSolidTile(matches.get(1).second(), GREEN); + } + + @Test + void searchForTextureIsCaseSensitive() { + CountingLoader loader = new CountingLoader(); + + assertTrue(loader.searchForTexture("RE").isEmpty()); + } + + @Test + void searchForTextureReturnsEmptyListWhenNothingMatches() { + CountingLoader loader = new CountingLoader(); + + assertTrue(loader.searchForTexture("purple").isEmpty()); + } + + @Test + void searchForTextureOnlyDecodesPagesOfMatches() { + CountingLoader loader = new CountingLoader(); + + loader.searchForTexture("re"); + + assertEquals(1, loader.opensOf(PAGE_ZERO_PATH), "page 0 opens"); + assertEquals(0, loader.opensOf(PAGE_ONE_PATH), "page 1 opens"); + } + + @Test + void searchForTextureRejectsNullQuery() { + CountingLoader loader = new CountingLoader(); + + assertThrows(NullPointerException.class, () -> loader.searchForTexture(null)); + } + + @Test + void coordinatesOutsidePageThrowGeneratorExceptionNamingTextureAndPath() throws IOException { + writeCoordinates(entry("offscreen", 100, 0, 0, PAGE_ZERO_FILE)); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("offscreen")); + + assertTrue(exception.getMessage().contains("offscreen"), exception.getMessage()); + assertTrue(exception.getMessage().contains(PAGE_ZERO_PATH), exception.getMessage()); + } + + @Test + void coordinatesEntryMissingFieldThrowsGeneratorExceptionNamingField() throws IOException { + writeCoordinates("{\"name\":\"nameless\",\"y\":0,\"size\":" + TILE_SIZE + "}"); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("nameless")); + + assertTrue(exception.getMessage().contains("'x'"), exception.getMessage()); + assertTrue(exception.getMessage().contains(COORDINATES_PATH), exception.getMessage()); + } + + @Test + void coordinatesEntryWithNonNumericFieldThrowsGeneratorException() throws IOException { + writeCoordinates("{\"name\":\"odd\",\"x\":\"left\",\"y\":0,\"size\":" + TILE_SIZE + "}"); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("odd")); + + assertTrue(exception.getMessage().contains(COORDINATES_PATH), exception.getMessage()); + } + + @Test + void malformedCoordinatesFileThrowsGeneratorExceptionNamingPath() throws IOException { + Files.writeString(fixturePath("/json/atlas_coordinates.json"), "[{\"name\": "); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("red")); + + assertTrue(exception.getMessage().contains(COORDINATES_PATH), exception.getMessage()); + } + + @Test + void coordinatesFileThatIsNotAnArrayThrowsGeneratorExceptionNamingPath() throws IOException { + Files.writeString(fixturePath("/json/atlas_coordinates.json"), "{\"name\": \"red\"}"); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("red")); + + assertTrue(exception.getMessage().contains(COORDINATES_PATH), exception.getMessage()); + } + + @Test + void pageAtlasThatIsNotAnImageThrowsGeneratorExceptionNamingPath() throws IOException { + Files.write(fixturePath("/spritesheets/" + PAGE_ZERO_FILE), new byte[] {1, 2, 3, 4}); + CountingLoader loader = new CountingLoader(); + + GeneratorException exception = assertThrows(GeneratorException.class, () -> loader.getTexture("red")); + + assertTrue(exception.getMessage().contains(PAGE_ZERO_PATH), exception.getMessage()); + } + + @Test + void duplicateNamesKeepTheLastEntry() throws IOException { + writeCoordinates(entry("dup", 0, 0, 0, PAGE_ZERO_FILE), entry("dup", TILE_SIZE, 0, 0, PAGE_ZERO_FILE)); + CountingLoader loader = new CountingLoader(); + + assertSolidTile(loader.getTexture("dup"), GREEN); + } + + @Test + void coordinatesFileIsReadOnce() { + CountingLoader loader = new CountingLoader(); + + loader.getTexture("red"); + loader.getTexture("blue"); + loader.searchForTexture("re"); + loader.getImageMap(); + + assertEquals(1, loader.opensOf(COORDINATES_PATH), "coordinates opens"); + } + + @Test + void concurrentRequestsOpenEachResourceOnce() throws Exception { + CountingLoader loader = new CountingLoader(); + int threads = 8; + ExecutorService executor = Executors.newFixedThreadPool(threads); + try { + CountDownLatch start = new CountDownLatch(1); + List> futures = new ArrayList<>(); + for (int i = 0; i < threads; i++) { + String name = i % 2 == 0 ? "red" : "blue"; + futures.add(executor.submit(() -> { + start.await(); + return loader.getTexture(name); + })); + } + start.countDown(); + for (Future future : futures) { + assertNotNull(future.get(10, TimeUnit.SECONDS)); + } + } finally { + executor.shutdownNow(); + } + + assertEquals(1, loader.opensOf(COORDINATES_PATH), "coordinates opens"); + assertEquals(1, loader.opensOf(PAGE_ZERO_PATH), "page 0 opens"); + assertEquals(1, loader.opensOf(PAGE_ONE_PATH), "page 1 opens"); + } + + /** + * Writes a 4x2 ARGB PNG made of two 2x2 tiles, the left one filled with {@code leftColour} + * and the right one with {@code rightColour}. + */ + private void writeAtlas(String fileName, int leftColour, int rightColour) throws IOException { + BufferedImage atlas = new BufferedImage(TILE_SIZE * 2, TILE_SIZE, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < TILE_SIZE; y++) { + for (int x = 0; x < TILE_SIZE; x++) { + atlas.setRGB(x, y, leftColour); + atlas.setRGB(x + TILE_SIZE, y, rightColour); + } + } + Path file = fixturePath("/spritesheets/" + fileName); + Files.createDirectories(file.getParent()); + ImageIO.write(atlas, "png", file.toFile()); + } + + private void writeCoordinates(String... entries) throws IOException { + Path file = fixturePath("/json/atlas_coordinates.json"); + Files.createDirectories(file.getParent()); + Files.writeString(file, "[" + String.join(",", entries) + "]"); + } + + private static String entry(String name, int x, int y, int page, String atlas) { + return "{\"name\":\"" + name + "\",\"x\":" + x + ",\"y\":" + y + ",\"size\":" + TILE_SIZE + + ",\"page\":" + page + ",\"atlas\":\"" + atlas + "\"}"; + } + + private static String entryWithoutAtlas(String name, int x, int y) { + return "{\"name\":\"" + name + "\",\"x\":" + x + ",\"y\":" + y + ",\"size\":" + TILE_SIZE + "}"; + } + + private Path fixturePath(String pathUnderBase) { + return root.resolve((BASE + pathUnderBase).substring(1)); + } + + private static void assertSolidTile(BufferedImage image, int argb) { + assertNotNull(image, "texture"); + assertEquals(TILE_SIZE, image.getWidth(), "width"); + assertEquals(TILE_SIZE, image.getHeight(), "height"); + for (int y = 0; y < TILE_SIZE; y++) { + for (int x = 0; x < TILE_SIZE; x++) { + assertEquals(argb, image.getRGB(x, y), "pixel at (" + x + ", " + y + ")"); + } + } + } + + /** + * Serves resources from the temp directory and counts how many times each path was requested. + */ + private final class CountingLoader extends SpritesheetLoader { + + private final Map opens = new ConcurrentHashMap<>(); + + CountingLoader() { + super(BASE); + } + + @Override + protected InputStream getResource(String path) { + opens.merge(path, 1, Integer::sum); + Path file = root.resolve(path.substring(1)); + if (!Files.isRegularFile(file)) { + return null; + } + try { + return Files.newInputStream(file); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + int opensOf(String path) { + return opens.getOrDefault(path, 0); + } + } +} diff --git a/generator/src/test/java/net/aerh/imagegenerator/spritesheet/SpritesheetTest.java b/generator/src/test/java/net/aerh/imagegenerator/spritesheet/SpritesheetTest.java new file mode 100644 index 0000000..f37869c --- /dev/null +++ b/generator/src/test/java/net/aerh/imagegenerator/spritesheet/SpritesheetTest.java @@ -0,0 +1,55 @@ +package net.aerh.imagegenerator.spritesheet; + +import net.hypixel.nerdbot.marmalade.functional.Pair; +import org.junit.jupiter.api.Test; + +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Exercises the static facade against the bundled vanilla atlas. + */ +class SpritesheetTest { + + @Test + void getTextureReturnsBundledTexture() { + BufferedImage stone = Spritesheet.getTexture("stone"); + + assertNotNull(stone, "the vanilla spritesheet has stone"); + assertTrue(stone.getWidth() > 0, "width"); + assertTrue(stone.getHeight() > 0, "height"); + } + + @Test + void getTextureReturnsNullForUnknownName() { + assertNull(Spritesheet.getTexture("definitely_not_a_texture")); + } + + @Test + void repeatedGetTextureReturnsSameInstance() { + assertSame(Spritesheet.getTexture("stone"), Spritesheet.getTexture("stone")); + } + + @Test + void searchForTextureReturnsSubstringMatchesOnly() { + List names = Spritesheet.searchForTexture("stone").stream().map(Pair::first).toList(); + + assertTrue(names.contains("stone"), names.toString()); + assertTrue(names.stream().allMatch(name -> name.contains("stone")), names.toString()); + } + + @Test + void getImageMapContainsEveryBundledTexture() { + Map imageMap = Spritesheet.getImageMap(); + + assertFalse(imageMap.isEmpty()); + assertSame(Spritesheet.getTexture("stone"), imageMap.get("stone")); + } +} diff --git a/generator/src/test/resources/golden/tools/fixture/inventory.png b/generator/src/test/resources/golden/tools/fixture/inventory.png index c44ac30..f3449d5 100644 Binary files a/generator/src/test/resources/golden/tools/fixture/inventory.png and b/generator/src/test/resources/golden/tools/fixture/inventory.png differ diff --git a/generator/src/test/resources/golden/tools/fixture/powerstone.png b/generator/src/test/resources/golden/tools/fixture/powerstone.png index 07e2faa..c9c37d8 100644 Binary files a/generator/src/test/resources/golden/tools/fixture/powerstone.png and b/generator/src/test/resources/golden/tools/fixture/powerstone.png differ diff --git a/generator/src/test/resources/golden/tools/inventory.png b/generator/src/test/resources/golden/tools/inventory.png index 18ad161..f73b6e2 100644 Binary files a/generator/src/test/resources/golden/tools/inventory.png and b/generator/src/test/resources/golden/tools/inventory.png differ diff --git a/generator/src/test/resources/golden/tools/powerstone.png b/generator/src/test/resources/golden/tools/powerstone.png index fb73e45..0ed3802 100644 Binary files a/generator/src/test/resources/golden/tools/powerstone.png and b/generator/src/test/resources/golden/tools/powerstone.png differ diff --git a/generator/src/test/resources/golden/vanilla/oak_planks.png b/generator/src/test/resources/golden/vanilla/oak_planks.png index a6da255..ab0757e 100644 Binary files a/generator/src/test/resources/golden/vanilla/oak_planks.png and b/generator/src/test/resources/golden/vanilla/oak_planks.png differ diff --git a/generator/src/test/resources/golden/vanilla/stone.png b/generator/src/test/resources/golden/vanilla/stone.png index 1ce582c..e843c9d 100644 Binary files a/generator/src/test/resources/golden/vanilla/stone.png and b/generator/src/test/resources/golden/vanilla/stone.png differ diff --git a/pom.xml b/pom.xml index 833bd5a..6504db3 100644 --- a/pom.xml +++ b/pom.xml @@ -81,7 +81,6 @@ 3.6.0 ${surefire.excludedGroups} - -javaagent:${settings.localRepository}/org/mockito/mockito-core/${mockito.version}/mockito-core-${mockito.version}.jar diff --git a/tooling/pom.xml b/tooling/pom.xml index fc3f1a1..0971a31 100644 --- a/tooling/pom.xml +++ b/tooling/pom.xml @@ -25,6 +25,13 @@ Marmalade master-SNAPSHOT + + + com.github.minecraft-library + asset-renderer + addedec7a2 + com.google.code.gson gson @@ -42,18 +49,6 @@ - - - - org.apache.maven.plugins - maven-surefire-plugin - - ${surefire.excludedGroups} - - - - - generate-spritesheet diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/CliArgs.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/CliArgs.java new file mode 100644 index 0000000..88d4db4 --- /dev/null +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/CliArgs.java @@ -0,0 +1,105 @@ +package net.aerh.imagegenerator.tooling; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; + +/** + * Reads {@code --flag=value} and bare {@code --flag} arguments the way every tooling entry point expects them. + * Later occurrences of a flag override earlier ones, and a bad value names the flag in the error. + */ +public final class CliArgs { + + private CliArgs() { + } + + /** + * Returns the text after {@code =} for the last {@code --flag=value} argument, or empty when the flag is absent. + */ + public static Optional value(String[] args, String flag) { + String prefix = flag + "="; + String found = null; + + for (String arg : args) { + if (arg.startsWith(prefix)) { + found = arg.substring(prefix.length()); + } + } + + return Optional.ofNullable(found); + } + + /** + * Parses an integer flag, falling back to {@code defaultValue} when the flag is absent. + * + * @throws IllegalArgumentException when the value is present but is not an integer + */ + public static int intValue(String[] args, String flag, int defaultValue) { + Optional raw = value(args, flag); + if (raw.isEmpty()) { + return defaultValue; + } + + try { + return Integer.parseInt(raw.get().trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(flag + " expects an integer, got '" + raw.get() + "'"); + } + } + + /** + * Parses a path flag. + * + * @throws IllegalArgumentException when the flag is present with a blank value + */ + public static Optional pathValue(String[] args, String flag) { + Optional raw = value(args, flag); + if (raw.isEmpty()) { + return Optional.empty(); + } + + if (raw.get().isBlank()) { + throw new IllegalArgumentException(flag + " must not be empty"); + } + + return Optional.of(Path.of(raw.get().trim())); + } + + /** + * Rejects any argument that is not a bare flag from {@code flags} (like {@code --clean}) or a flag from + * {@code valuedFlags} with a value (like {@code --size=256}), so a typo cannot be silently ignored. + * + * @throws IllegalArgumentException naming the first unknown argument, or a valued flag given without its value + */ + public static void checkKnown(String[] args, Set flags, Set valuedFlags) { + for (String arg : args) { + if (flags.contains(arg)) { + continue; + } + + int equals = arg.indexOf('='); + if (equals > 0 && valuedFlags.contains(arg.substring(0, equals))) { + continue; + } + + if (valuedFlags.contains(arg)) { + throw new IllegalArgumentException(arg + " needs a value, written as " + arg + "="); + } + + throw new IllegalArgumentException("Unknown option: " + arg); + } + } + + /** + * True when {@code flag} appears as a whole argument, so {@code --clean} does not match {@code --cleanup}. + */ + public static boolean hasFlag(String[] args, String flag) { + for (String arg : args) { + if (arg.equals(flag)) { + return true; + } + } + + return false; + } +} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/PathGuard.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/PathGuard.java new file mode 100644 index 0000000..0470943 --- /dev/null +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/PathGuard.java @@ -0,0 +1,39 @@ +package net.aerh.imagegenerator.tooling; + +import java.nio.file.Path; +import java.util.Optional; + +/** + * Containment checks used before the tooling deletes a directory it was pointed at. + */ +public final class PathGuard { + + private PathGuard() { + } + + /** + * True when {@code child} is {@code parent} or lives somewhere beneath it. Both paths are made absolute + * and normalised first, so {@code a/../a} and {@code a} compare equal. + */ + public static boolean contains(Path parent, Path child) { + Path normalisedParent = parent.toAbsolutePath().normalize(); + Path normalisedChild = child.toAbsolutePath().normalize(); + return normalisedChild.startsWith(normalisedParent); + } + + /** + * Explains why {@code target} must not be deleted recursively, or returns empty when deleting it is safe. + * A relative {@code target} is resolved against {@code currentDir}. Deleting the current directory or any + * of its ancestors is refused. + */ + public static Optional deletionRefusal(Path target, Path currentDir) { + Path current = currentDir.toAbsolutePath().normalize(); + Path resolved = current.resolve(target).normalize(); + + if (contains(resolved, current)) { + return Optional.of(resolved + " contains the current directory " + current); + } + + return Optional.empty(); + } +} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingArguments.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingArguments.java deleted file mode 100644 index 201aafb..0000000 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingArguments.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.aerh.imagegenerator.tooling; - -/** Apply directory options before any ToolingConstants paths are initialized. */ -public final class ToolingArguments { - private ToolingArguments() {} - - public static void configureDirectories(String[] args) { - for (String arg : args) { - String property = null; - if (arg.startsWith("--work-dir=")) property = "generator.tooling.workDir"; - else if (arg.startsWith("--output-dir=")) property = "generator.tooling.outputDir"; - else if (arg.startsWith("--renderer-dir=")) property = "generator.tooling.rendererDir"; - if (property != null) { - String value = arg.substring(arg.indexOf('=') + 1); - if (value.isBlank()) throw new IllegalArgumentException("Empty directory option: " + arg); - System.setProperty(property, value); - } - } - } -} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingConstants.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingConstants.java deleted file mode 100644 index 54c320d..0000000 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingConstants.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.aerh.imagegenerator.tooling; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Comparator; -import java.util.stream.Stream; - -/** - * Shared constants and utilities for the tooling module. - */ -public final class ToolingConstants { - - // Build directories - public static final Path BUILD_DIR = Path.of(System.getProperty("generator.tooling.workDir", "tooling/build")); - public static final Path MINECRAFT_ASSETS = BUILD_DIR.resolve("minecraft-assets"); - public static final Path RENDERED_ITEMS = BUILD_DIR.resolve("rendered-items"); - public static final Path JAR_CACHE = BUILD_DIR.resolve("minecraft-jars"); - public static final Path MAPPINGS_CACHE = BUILD_DIR.resolve("minecraft-mappings"); - public static final Path RENDERER_CLONE = Path.of(System.getProperty("generator.tooling.rendererDir", BUILD_DIR.resolve("MinecraftRenderer").toString())); - // Generator output paths - public static final String GENERATOR_RESOURCES = System.getProperty("generator.tooling.outputDir", "generator/src/main/resources/minecraft/assets"); - public static final String GENERATOR_SPRITESHEETS = GENERATOR_RESOURCES + "/spritesheets"; - public static final String GENERATOR_JSON = GENERATOR_RESOURCES + "/json"; - // Debug flag - private static boolean debugEnabled = false; - - private ToolingConstants() { - } - - public static void setDebugEnabled(boolean enabled) { - debugEnabled = enabled; - } - - /** - * Prints a debug message if debug mode is enabled. - */ - public static void printDebug(String message) { - if (debugEnabled) { - System.out.println("[DEBUG] " + message); - } - } - - /** - * Recursively deletes a directory and all its contents. - */ - public static void deleteDirectory(Path path) throws IOException { - if (!Files.exists(path)) { - return; - } - - try (Stream walk = Files.walk(path)) { - walk.sorted(Comparator.reverseOrder()) - .forEach(p -> { - try { - Files.delete(p); - } catch (IOException e) { - System.err.println("Failed to delete: " + p + " - " + e.getMessage()); - } - }); - } - } - - /** - * Gets the path to a cached Minecraft client JAR. - */ - public static Path getJarPath(String version) { - return JAR_CACHE.resolve("minecraft-" + version + "-client.jar"); - } - - /** - * Gets the path to cached Mojang mappings. - */ - public static Path getMappingsPath(String version) { - return MAPPINGS_CACHE.resolve("minecraft-" + version + "-client.txt"); - } -} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingPaths.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingPaths.java new file mode 100644 index 0000000..29e12d2 --- /dev/null +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingPaths.java @@ -0,0 +1,167 @@ +package net.aerh.imagegenerator.tooling; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Where the tooling reads and writes. Built once per entry point from command line flags, then JVM system + * properties, then the repository defaults, and passed to every component that touches the file system. + * + * @param workDir scratch space for extracted assets, rendered icons and download caches + * @param outputDir the generator assets directory that receives spritesheets and JSON + */ +public record ToolingPaths(Path workDir, Path outputDir) { + + public static final String WORK_DIR_FLAG = "--work-dir"; + public static final String OUTPUT_DIR_FLAG = "--output-dir"; + + public static final String WORK_DIR_PROPERTY = "generator.tooling.workDir"; + public static final String OUTPUT_DIR_PROPERTY = "generator.tooling.outputDir"; + + public static final Path DEFAULT_WORK_DIR = Path.of("tooling/build"); + public static final Path DEFAULT_OUTPUT_DIR = Path.of("generator/src/main/resources/minecraft/assets"); + + private static final String JAR_PREFIX = "minecraft-"; + private static final String JAR_SUFFIX = "-client.jar"; + + public ToolingPaths { + Objects.requireNonNull(workDir, "workDir"); + Objects.requireNonNull(outputDir, "outputDir"); + } + + /** + * The layout the repository's own CI uses: everything under {@code tooling/build}, output into the generator module. + */ + public static ToolingPaths defaults() { + return new ToolingPaths(DEFAULT_WORK_DIR, DEFAULT_OUTPUT_DIR); + } + + /** + * Resolves each directory from its flag, then its system property, then the default. + * + * @throws IllegalArgumentException when a flag is present with a blank value + */ + public static ToolingPaths fromArgs(String[] args) { + Path workDir = resolve(args, WORK_DIR_FLAG, WORK_DIR_PROPERTY).orElse(DEFAULT_WORK_DIR); + Path outputDir = resolve(args, OUTPUT_DIR_FLAG, OUTPUT_DIR_PROPERTY).orElse(DEFAULT_OUTPUT_DIR); + return new ToolingPaths(workDir, outputDir); + } + + private static Optional resolve(String[] args, String flag, String property) { + Optional fromFlag = CliArgs.pathValue(args, flag); + if (fromFlag.isPresent()) { + return fromFlag; + } + + String fromProperty = System.getProperty(property); + if (fromProperty == null || fromProperty.isBlank()) { + return Optional.empty(); + } + + return Optional.of(Path.of(fromProperty.trim())); + } + + /** + * True when the work directory is the repository's own {@code tooling/build}, which the tool may clean + * up after itself without being asked. Any spelling of that directory counts. + */ + public boolean isDefaultWorkDir() { + return workDir.toAbsolutePath().normalize().equals(DEFAULT_WORK_DIR.toAbsolutePath().normalize()); + } + + public Path minecraftAssets() { + return workDir.resolve("minecraft-assets"); + } + + public Path renderedItems() { + return workDir.resolve("rendered-items"); + } + + public Path jarCache() { + return workDir.resolve("minecraft-jars"); + } + + public Path mappingsCache() { + return workDir.resolve("minecraft-mappings"); + } + + /** Scratch space the icon renderer library may use beside the JAR it is handed. */ + public Path rendererCache() { + return workDir.resolve("asset-renderer"); + } + + public Path spritesheets() { + return outputDir.resolve("spritesheets"); + } + + public Path json() { + return outputDir.resolve("json"); + } + + public Path jarPath(String version) { + return jarCache().resolve(JAR_PREFIX + version + JAR_SUFFIX); + } + + public Path mappingsPath(String version) { + return mappingsCache().resolve("minecraft-" + version + "-client.txt"); + } + + /** + * The version of the one client JAR in the cache, or empty when none is cached. + * + * @throws IOException if several versions are cached, since picking one silently would be a guess + */ + public Optional cachedClientVersion() throws IOException { + Path cache = jarCache(); + if (!Files.isDirectory(cache)) { + return Optional.empty(); + } + + List versions; + try (Stream files = Files.list(cache)) { + versions = files + .map(file -> file.getFileName().toString()) + .filter(name -> name.startsWith(JAR_PREFIX) && name.endsWith(JAR_SUFFIX)) + .map(name -> name.substring(JAR_PREFIX.length(), name.length() - JAR_SUFFIX.length())) + .sorted() + .toList(); + } + + if (versions.isEmpty()) { + return Optional.empty(); + } + + if (versions.size() > 1) { + throw new IOException("Several client JARs are cached (" + String.join(", ", versions) + + "); pass --version to choose one"); + } + + return Optional.of(versions.get(0)); + } + + /** + * Explains why deleting the work directory would destroy something that is not ours, or returns empty when + * cleanup is safe. Relative paths are resolved against {@code currentDir}. + */ + public Optional cleanupRefusal(Path currentDir) { + Optional currentDirRefusal = PathGuard.deletionRefusal(workDir, currentDir); + if (currentDirRefusal.isPresent()) { + return Optional.of("work dir " + currentDirRefusal.get()); + } + + Path current = currentDir.toAbsolutePath().normalize(); + Path work = current.resolve(workDir).normalize(); + Path output = current.resolve(outputDir).normalize(); + + if (PathGuard.contains(work, output)) { + return Optional.of("output dir " + output + " is inside work dir " + work); + } + + return Optional.empty(); + } +} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingSupport.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingSupport.java new file mode 100644 index 0000000..f3d4ae0 --- /dev/null +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/ToolingSupport.java @@ -0,0 +1,51 @@ +package net.aerh.imagegenerator.tooling; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.stream.Stream; + +/** + * Debug output and file helpers shared by the tooling module. Directories live in {@link ToolingPaths}. + */ +public final class ToolingSupport { + + private static boolean debugEnabled = false; + + private ToolingSupport() { + } + + public static void setDebugEnabled(boolean enabled) { + debugEnabled = enabled; + } + + /** + * Prints a debug message if debug mode is enabled. + */ + public static void printDebug(String message) { + if (debugEnabled) { + System.out.println("[DEBUG] " + message); + } + } + + /** + * Recursively deletes a directory and all its contents. + */ + public static void deleteDirectory(Path path) throws IOException { + if (!Files.exists(path)) { + return; + } + + try (Stream walk = Files.walk(path)) { + walk.sorted(Comparator.reverseOrder()) + .forEach(p -> { + try { + Files.delete(p); + } catch (IOException e) { + System.err.println("Failed to delete: " + p + " - " + e.getMessage()); + } + }); + } + } +} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/AssetDownloader.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/AssetDownloader.java index f9a18d3..762f294 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/AssetDownloader.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/AssetDownloader.java @@ -2,7 +2,8 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import net.aerh.imagegenerator.tooling.ToolingConstants; +import net.aerh.imagegenerator.tooling.ToolingSupport; +import net.aerh.imagegenerator.tooling.ToolingPaths; import net.hypixel.nerdbot.marmalade.http.HttpClient; import java.io.IOException; @@ -28,6 +29,7 @@ public class AssetDownloader { private static final String VERSION_MANIFEST_URL = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; private static final String ASSETS_PREFIX = "assets/minecraft/"; + private final ToolingPaths paths; private final String version; private final Path outputPath; private final boolean keepJar; @@ -37,20 +39,21 @@ public class AssetDownloader { /** * Creates a new AssetDownloader for downloading and extracting Minecraft assets. * - * @param version the Minecraft version to download, or null to use the latest release - * @param outputPath the directory where assets will be extracted - * @param keepJar if true, keep the downloaded JAR file after extraction - * @param clean if true, delete the output directory before extraction + * @param paths where the JAR and mappings caches live and where assets are extracted + * @param version the Minecraft version to download, or null to use the latest release + * @param keepJar if true, keep the downloaded JAR file after extraction + * @param clean if true, delete the output directory before extraction */ - public AssetDownloader(String version, Path outputPath, boolean keepJar, boolean clean) { + public AssetDownloader(ToolingPaths paths, String version, boolean keepJar, boolean clean) { + this.paths = paths; this.version = version; - this.outputPath = outputPath; + this.outputPath = paths.minecraftAssets(); this.keepJar = keepJar; this.clean = clean; } public void setDebug(boolean debug) { - ToolingConstants.setDebugEnabled(debug); + ToolingSupport.setDebugEnabled(debug); } /** @@ -74,7 +77,7 @@ public int download() throws IOException, InterruptedException { System.out.println("Downloading Minecraft " + resolvedVersion + " assets..."); String clientJarUrl = getClientJarUrl(resolvedVersion); - ToolingConstants.printDebug("Client JAR URL: " + clientJarUrl); + ToolingSupport.printDebug("Client JAR URL: " + clientJarUrl); Path jarPath = downloadClientJar(clientJarUrl, resolvedVersion); System.out.println("Downloaded client JAR: " + jarPath); @@ -84,12 +87,13 @@ public int download() throws IOException, InterruptedException { Path mappingsPath = downloadMappings(mappingsUrl, resolvedVersion); System.out.println("Downloaded mappings: " + mappingsPath); } else { - ToolingConstants.printDebug("No mappings available for this version"); + System.out.println("No mappings published for " + resolvedVersion + + " (none exist for 26.1 and later, whose JARs carry real class names)."); } if (clean && Files.exists(outputPath)) { System.out.println("Cleaning output directory: " + outputPath); - ToolingConstants.deleteDirectory(outputPath); + ToolingSupport.deleteDirectory(outputPath); } int count = extractAssets(jarPath); @@ -97,7 +101,7 @@ public int download() throws IOException, InterruptedException { if (!keepJar) { Files.deleteIfExists(jarPath); - ToolingConstants.printDebug("Deleted JAR file"); + ToolingSupport.printDebug("Deleted JAR file"); } return count; @@ -134,7 +138,7 @@ private String getClientJarUrl(String targetVersion) throws IOException, Interru if (versionId.equals(targetVersion)) { String versionUrl = versionEntry.get("url").getAsString(); - ToolingConstants.printDebug("Version manifest URL: " + versionUrl); + ToolingSupport.printDebug("Version manifest URL: " + versionUrl); JsonObject versionMeta = HttpClient.getJson(versionUrl).orElseThrow(); JsonObject client = versionMeta.getAsJsonObject("downloads").getAsJsonObject("client"); @@ -178,9 +182,9 @@ private String getMappingsUrl(String targetVersion) throws IOException, Interrup * Downloads the client mappings file, or returns the cached path if already downloaded. */ private Path downloadMappings(String url, String versionId) throws IOException, InterruptedException { - Files.createDirectories(ToolingConstants.MAPPINGS_CACHE); + Files.createDirectories(paths.mappingsCache()); - Path mappingsPath = ToolingConstants.getMappingsPath(versionId); + Path mappingsPath = paths.mappingsPath(versionId); if (Files.exists(mappingsPath)) { System.out.println("Using cached mappings: " + mappingsPath); @@ -212,9 +216,9 @@ private Path downloadMappings(String url, String versionId) throws IOException, * Downloads the client JAR file with progress reporting, or returns the cached path if already downloaded. */ private Path downloadClientJar(String url, String versionId) throws IOException, InterruptedException { - Files.createDirectories(ToolingConstants.JAR_CACHE); + Files.createDirectories(paths.jarCache()); - Path jarPath = ToolingConstants.getJarPath(versionId); + Path jarPath = paths.jarPath(versionId); if (Files.exists(jarPath)) { System.out.println("Using cached JAR: " + jarPath); @@ -241,7 +245,7 @@ private Path downloadClientJar(String url, String versionId) throws IOException, .orElse(-1); try (InputStream in = response.body()) { - Path tempFile = ToolingConstants.JAR_CACHE.resolve("download-" + System.currentTimeMillis() + ".tmp"); + Path tempFile = paths.jarCache().resolve("download-" + System.currentTimeMillis() + ".tmp"); long totalRead = 0; byte[] buffer = new byte[8_192]; int read; diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractor.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractor.java index 42371a4..d0970ba 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractor.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractor.java @@ -1,6 +1,6 @@ package net.aerh.imagegenerator.tooling.minecraft; -import net.aerh.imagegenerator.tooling.ToolingConstants; +import net.aerh.imagegenerator.tooling.ToolingSupport; import net.aerh.imagegenerator.tooling.minecraft.colorextractor.DyeColorVisitor; import net.aerh.imagegenerator.tooling.minecraft.colorextractor.EffectFieldMapper; import net.aerh.imagegenerator.tooling.minecraft.colorextractor.EffectRegistryVisitor; @@ -20,7 +20,9 @@ import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -30,30 +32,45 @@ */ public class ColorExtractor { + private static final String DYE_COLOR_CLASS = "net.minecraft.world.item.DyeColor"; + private static final String MOB_EFFECTS_CLASS = "net.minecraft.world.effect.MobEffects"; + private static final String MOB_EFFECT_CLASS = "net.minecraft.world.effect.MobEffect"; + private static final String POTIONS_CLASS = "net.minecraft.world.item.alchemy.Potions"; + private static final List NAMED_CLASSES = List.of(DYE_COLOR_CLASS, MOB_EFFECTS_CLASS, MOB_EFFECT_CLASS, POTIONS_CLASS); + private final Path jarPath; + /** Mojang's ProGuard mappings for an obfuscated JAR, or null when the JAR already uses the real class names. */ private final Path mappingsPath; - private final Map classToObfuscated = new HashMap<>(); private boolean debug = false; + /** + * Reads a client JAR that ships unobfuscated, as every version from 26.x on does. + */ + public ColorExtractor(Path jarPath) { + this.jarPath = Objects.requireNonNull(jarPath, "jarPath"); + this.mappingsPath = null; + } + + /** + * Reads an obfuscated client JAR through Mojang's mappings for that version. + */ public ColorExtractor(Path jarPath, Path mappingsPath) { - this.jarPath = jarPath; - this.mappingsPath = mappingsPath; + this.jarPath = Objects.requireNonNull(jarPath, "jarPath"); + this.mappingsPath = Objects.requireNonNull(mappingsPath, "mappingsPath"); } public void setDebug(boolean debug) { this.debug = debug; - ToolingConstants.setDebugEnabled(debug); + ToolingSupport.setDebugEnabled(debug); } public ExtractedColors extract() throws IOException { - System.out.println("Parsing mappings..."); - parseMappings(); - ToolingConstants.printDebug("Found " + classToObfuscated.size() + " class mappings"); + Map classes = resolveClassNames(); ExtractedColors result = new ExtractedColors(); - String dyeColorClass = classToObfuscated.get("net.minecraft.world.item.DyeColor"); + String dyeColorClass = classes.get(DYE_COLOR_CLASS); if (dyeColorClass != null) { System.out.println("Extracting DyeColor values from: " + dyeColorClass); result.dyeColors = extractDyeColors(dyeColorClass); @@ -62,8 +79,8 @@ public ExtractedColors extract() throws IOException { System.err.println("WARNING: Could not find DyeColor class mapping"); } - String mobEffectsClass = classToObfuscated.get("net.minecraft.world.effect.MobEffects"); - String mobEffectClass = classToObfuscated.get("net.minecraft.world.effect.MobEffect"); + String mobEffectsClass = classes.get(MOB_EFFECTS_CLASS); + String mobEffectClass = classes.get(MOB_EFFECT_CLASS); if (mobEffectsClass != null && mobEffectClass != null) { System.out.println("Extracting MobEffect colors from: " + mobEffectsClass); result.effectColors = extractEffectColors(mobEffectsClass); @@ -78,7 +95,7 @@ public ExtractedColors extract() throws IOException { System.err.println("WARNING: Could not find MobEffects class mapping"); } - String potionsClass = classToObfuscated.get("net.minecraft.world.item.alchemy.Potions"); + String potionsClass = classes.get(POTIONS_CLASS); Set effectsWithPotions = new HashSet<>(); if (potionsClass != null && mobEffectsClass != null) { @@ -108,13 +125,52 @@ public ExtractedColors extract() throws IOException { return result; } - private void parseMappings() throws IOException { + /** + * Works out the class names to read from the JAR. With mappings, each named class is translated to its + * obfuscated name and left out when the mappings do not list it. Without mappings the JAR must already use + * the real names, which is checked by looking for the DyeColor class before anything else runs. + * + * @throws IOException if the mappings cannot be read, or no mappings were given and the JAR is obfuscated + */ + Map resolveClassNames() throws IOException { + Map classes = new HashMap<>(); + + if (mappingsPath == null) { + try (ZipFile zip = new ZipFile(jarPath.toFile())) { + if (zip.getEntry(DYE_COLOR_CLASS.replace('.', '/') + ".class") == null) { + throw new IOException("Client JAR " + jarPath.getFileName() + " looks obfuscated (no " + + DYE_COLOR_CLASS + " class) and no mappings were given"); + } + } + + System.out.println("JAR uses real class names; no mappings needed."); + for (String name : NAMED_CLASSES) { + classes.put(name, name); + } + return classes; + } + + System.out.println("Parsing mappings..."); + Map classToObfuscated = parseMappings(); + ToolingSupport.printDebug("Found " + classToObfuscated.size() + " class mappings"); + + for (String name : NAMED_CLASSES) { + String obfuscated = classToObfuscated.get(name); + if (obfuscated != null) { + classes.put(name, obfuscated); + } + } + return classes; + } + + private Map parseMappings() throws IOException { MemoryMappingTree tree = new MemoryMappingTree(); try (BufferedReader reader = Files.newBufferedReader(mappingsPath)) { MappingReader.read(reader, tree); } + Map classToObfuscated = new HashMap<>(); for (MappingTree.ClassMapping classMapping : tree.getClasses()) { String srcName = classMapping.getSrcName(); String dstName = classMapping.getDstName(0); @@ -126,6 +182,7 @@ private void parseMappings() throws IOException { ); } } + return classToObfuscated; } private Map extractDyeColors(String obfuscatedClass) throws IOException { @@ -139,7 +196,7 @@ private Map extractDyeColors(String obfuscatedClass) throw try (InputStream is = zip.getInputStream(entry)) { ClassReader reader = new ClassReader(is); - DyeColorVisitor visitor = new DyeColorVisitor(ToolingConstants::printDebug); + DyeColorVisitor visitor = new DyeColorVisitor(ToolingSupport::printDebug); reader.accept(visitor, 0); return visitor.getColors(); } @@ -157,7 +214,7 @@ private Map extractEffectColors(String mobEffectsClass) throws try (InputStream is = zip.getInputStream(entry)) { ClassReader reader = new ClassReader(is); - EffectRegistryVisitor visitor = new EffectRegistryVisitor(ToolingConstants::printDebug); + EffectRegistryVisitor visitor = new EffectRegistryVisitor(ToolingSupport::printDebug); reader.accept(visitor, 0); return visitor.getColors(); } @@ -175,10 +232,10 @@ private Set findPotionEffects(String potionsClass, String mobEffectsClas if (mobEffectsEntry != null) { try (InputStream is = zip.getInputStream(mobEffectsEntry)) { ClassReader reader = new ClassReader(is); - EffectFieldMapper fieldMapper = new EffectFieldMapper(ToolingConstants::printDebug); + EffectFieldMapper fieldMapper = new EffectFieldMapper(ToolingSupport::printDebug); reader.accept(fieldMapper, 0); fieldToEffectName = fieldMapper.getFieldToName(); - ToolingConstants.printDebug("Mapped " + fieldToEffectName.size() + " MobEffects fields to names"); + ToolingSupport.printDebug("Mapped " + fieldToEffectName.size() + " MobEffects fields to names"); } } @@ -186,7 +243,7 @@ private Set findPotionEffects(String potionsClass, String mobEffectsClas if (potionsEntry != null) { try (InputStream is = zip.getInputStream(potionsEntry)) { ClassReader reader = new ClassReader(is); - PotionVisitor visitor = new PotionVisitor(mobEffectsInternal, fieldToEffectName, ToolingConstants::printDebug); + PotionVisitor visitor = new PotionVisitor(mobEffectsInternal, fieldToEffectName, ToolingSupport::printDebug); reader.accept(visitor, 0); return visitor.getEffectsWithPotions(); } @@ -216,7 +273,7 @@ private Map scanEffectSubclasses(String mobEffectClass) throws if (visitor.getEffectName() != null && visitor.getColor() != 0) { colors.put(visitor.getEffectName(), visitor.getColor()); - ToolingConstants.printDebug("Found effect: " + visitor.getEffectName() + " -> " + visitor.getColor()); + ToolingSupport.printDebug("Found effect: " + visitor.getEffectName() + " -> " + visitor.getColor()); } } } catch (Exception ignored) { diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ItemIconRenderer.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ItemIconRenderer.java new file mode 100644 index 0000000..5a2702b --- /dev/null +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ItemIconRenderer.java @@ -0,0 +1,217 @@ +package net.aerh.imagegenerator.tooling.minecraft; + +import lib.minecraft.renderer.AtlasRenderer; +import lib.minecraft.renderer.call.request.AtlasOptions; +import lib.minecraft.renderer.call.result.AtlasResult; +import lib.minecraft.renderer.content.client.ClientAcquisition; +import lib.minecraft.renderer.content.client.ClientAssets; +import lib.minecraft.renderer.content.client.ClientOptions; +import lib.minecraft.renderer.content.index.RendererContext; +import net.aerh.imagegenerator.tooling.ToolingPaths; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipFile; + +/** + * Renders every item of a client version as the icon its inventory slot shows, one PNG per id, with the + * asset-renderer library running in this JVM. The library reads the client JAR the download step cached, so + * there is nothing to clone, build or license. + *

+ * The item list is the game's own registry: since 26.1 the client ships one definition file per item under + * {@code assets/minecraft/items/}, which covers block items such as stone and chests as well as plain items. + * The work goes through the library's {@code AtlasRenderer}, which already knows which ids are drawn as flat + * sprites, as isometric blocks, or as block entities. Ids are rendered in batches so one pass holds a small + * sheet rather than every icon at once; each sheet is then cut into the individual files the spritesheet + * step packs. + */ +public final class ItemIconRenderer { + + /** Ids per atlas pass; at 256px tiles a pass holds a few dozen megabytes of pixels. */ + static final int BATCH_SIZE = 128; + + private static final int COLUMNS = 16; + private static final String VANILLA_NAMESPACE = "minecraft:"; + private static final String ITEM_DEFINITIONS_DIR = "assets/minecraft/items/"; + private static final Pattern ITEM_DEFINITION = Pattern.compile(Pattern.quote(ITEM_DEFINITIONS_DIR) + "([^/]+)\\.json"); + + /** + * What a render produced: how many icons were written, and every item the library could not draw with + * the reason it gave, or no reason when it simply produced no tile for the id. + */ + public record Report(int written, List skipped) { + } + + private final ToolingPaths paths; + + public ItemIconRenderer(ToolingPaths paths) { + this.paths = paths; + } + + /** + * Renders every item of {@code version} into {@code outputDir}, creating it if needed. + * + * @param outputDir where the PNG files go, one per id, named without the {@code minecraft:} namespace + * @param version the client version whose JAR is in the work directory's cache + * @param renderSize the icon size in pixels + * + * @throws IOException if the JAR is not cached, holds no item definitions, or an icon cannot be written + */ + public Report renderAll(Path outputDir, String version, int renderSize) throws IOException { + if (renderSize <= 0) { + throw new IllegalArgumentException("Render size must be positive, got " + renderSize); + } + + Path jar = paths.jarPath(version); + if (!Files.isRegularFile(jar)) { + throw new IOException("Client JAR for " + version + " is not cached at " + jar + "; run --download first"); + } + + List ids = itemIdsFromJar(jar); + System.out.println("Loading client " + version + " from " + jar); + ClientOptions options = ClientOptions.builder() + .version(version) + .cacheRoot(paths.rendererCache().toFile()) + .build(); + RendererContext context = RendererContext.load(new ClientAssets(options, ClientAcquisition.readVanillaPack(jar))); + System.out.println("Rendering " + ids.size() + " items at " + renderSize + "px..."); + + Files.createDirectories(outputDir); + AtlasRenderer renderer = new AtlasRenderer(context); + Set drawn = new HashSet<>(); + List skipped = new ArrayList<>(); + int written = 0; + int done = 0; + + for (List batch : batches(ids, BATCH_SIZE)) { + Set wanted = new HashSet<>(batch); + AtlasResult result = renderer.render(AtlasOptions.builder() + .filter(wanted::contains) + .tileSize(renderSize) + .columns(COLUMNS) + .progressLogging(false) + .build()); + + written += writeTiles(result.image().toBufferedImage(), result.sidecar().tiles(), outputDir); + for (AtlasResult.Tile tile : result.sidecar().tiles()) { + drawn.add(tile.id()); + } + for (AtlasResult.Skipped row : result.sidecar().skipped()) { + drawn.add(row.id()); + skipped.add(row.id() + " (" + row.reason() + ")"); + } + + done += batch.size(); + System.out.println(" rendered " + done + "/" + ids.size()); + } + + for (String id : ids) { + if (!drawn.contains(id)) { + skipped.add(id + " (the renderer produced no tile for it)"); + } + } + + for (String row : skipped) { + System.err.println(" skipped " + row); + } + System.out.println("Wrote " + written + " icons to " + outputDir + + (skipped.isEmpty() ? "" : " (" + skipped.size() + " skipped)")); + return new Report(written, List.copyOf(skipped)); + } + + /** + * The item ids a client JAR registers, read from its one-definition-per-item directory, namespaced and + * sorted. Model files, which also exist for variants such as armour trims, are not items and are not read. + * + * @throws IOException if the JAR cannot be read or has no item definitions, which means a client older + * than 26.1 + */ + static List itemIdsFromJar(Path jar) throws IOException { + Set ids = new LinkedHashSet<>(); + + try (ZipFile zip = new ZipFile(jar.toFile())) { + zip.stream().forEach(entry -> { + Matcher matcher = ITEM_DEFINITION.matcher(entry.getName()); + if (matcher.matches()) { + ids.add(VANILLA_NAMESPACE + matcher.group(1)); + } + }); + } + + if (ids.isEmpty()) { + throw new IOException("No item definitions under " + ITEM_DEFINITIONS_DIR + " in " + jar + + "; the icon renderer needs a 26.1 or newer client"); + } + + return ids.stream().sorted().toList(); + } + + /** + * The file an id is written to: vanilla ids drop their namespace, so {@code minecraft:stone} becomes + * {@code stone.png} as the previous renderer named them; any other namespace is kept with its colon + * replaced, since a colon is not a usable file name character. + */ + static String iconFileName(String id) { + String name = id.startsWith(VANILLA_NAMESPACE) ? id.substring(VANILLA_NAMESPACE.length()) : id.replace(':', '_'); + return name + ".png"; + } + + static List> batches(List ids, int size) { + if (size <= 0) { + throw new IllegalArgumentException("Batch size must be positive, got " + size); + } + + List> batches = new ArrayList<>(); + for (int start = 0; start < ids.size(); start += size) { + batches.add(List.copyOf(ids.subList(start, Math.min(ids.size(), start + size)))); + } + return batches; + } + + /** + * Cuts each tile out of a rendered sheet into its own PNG. A tile whose source is {@code EMPTY} drew + * nothing (air, for instance) and is not written. + * + * @return how many files were written + * + * @throws IOException if a tile lies outside the sheet or a file cannot be written + */ + static int writeTiles(BufferedImage sheet, List tiles, Path outputDir) throws IOException { + int written = 0; + + for (AtlasResult.Tile tile : tiles) { + if (tile.source() == AtlasResult.Tile.Source.EMPTY) { + continue; + } + + if (tile.x() < 0 || tile.y() < 0 + || tile.x() + tile.width() > sheet.getWidth() || tile.y() + tile.height() > sheet.getHeight()) { + throw new IOException("Tile for " + tile.id() + " at (" + tile.x() + ", " + tile.y() + ") " + + tile.width() + "x" + tile.height() + " lies outside the " + + sheet.getWidth() + "x" + sheet.getHeight() + " sheet"); + } + + BufferedImage icon = new BufferedImage(tile.width(), tile.height(), BufferedImage.TYPE_INT_ARGB); + int[] pixels = sheet.getRGB(tile.x(), tile.y(), tile.width(), tile.height(), null, 0, tile.width()); + icon.setRGB(0, 0, tile.width(), tile.height(), pixels, 0, tile.width()); + + Path file = outputDir.resolve(iconFileName(tile.id())); + if (!ImageIO.write(icon, "png", file.toFile())) { + throw new IOException("No PNG writer is available for " + file); + } + written++; + } + + return written; + } +} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ItemRenderer.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ItemRenderer.java deleted file mode 100644 index 433a153..0000000 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/ItemRenderer.java +++ /dev/null @@ -1,337 +0,0 @@ -package net.aerh.imagegenerator.tooling.minecraft; - -import net.aerh.imagegenerator.tooling.ToolingConstants; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.concurrent.TimeUnit; - -/** - * Utility class for rendering Minecraft items and blocks using the MinecraftRenderer .NET library. - *

- * Handles cloning, building, and running the .NET renderer to generate images. - * Used by {@link MinecraftAssetTool} for the render step. - *

- * Prerequisites: .NET SDK 9.0+ and Git. - * - * @see MinecraftAssetTool - * @see MinecraftRenderer on GitHub - */ -public class ItemRenderer { - - private static final String REPO_URL = "https://github.com/ptlthg/MinecraftRenderer.git"; - - private final Path repoPath; - private boolean debug; - - public ItemRenderer() { - this(ToolingConstants.RENDERER_CLONE); - } - - public ItemRenderer(Path repoPath) { - this.repoPath = repoPath; - } - - public void setDebug(boolean debug) { - this.debug = debug; - ToolingConstants.setDebugEnabled(debug); - } - - /** - * Sets up the MinecraftRenderer by cloning and building if necessary. - * - * @throws IOException if setup fails - * @throws InterruptedException if the process is interrupted - */ - public void setup() throws IOException, InterruptedException { - setup(false, false); - } - - /** - * Sets up the MinecraftRenderer by cloning and building. - * - * @param forceReclone if true, delete and re-clone the repository - * @param forceRebuild if true, force a clean rebuild - * - * @throws IOException if setup fails - * @throws InterruptedException if the process is interrupted - */ - public void setup(boolean forceReclone, boolean forceRebuild) throws IOException, InterruptedException { - checkPrerequisites(); - - if (forceReclone && Files.exists(repoPath)) { - System.out.println("Force re-clone requested, deleting existing repository..."); - ToolingConstants.deleteDirectory(repoPath); - } - - if (!isCloned()) { - cloneRepository(); - } else { - System.out.println("Repository already cloned at: " + repoPath); - pullLatest(); - } - - if (forceRebuild || !isBuilt()) { - buildProject(); - } else { - System.out.println("Project already built."); - } - } - - /** - * Checks if required tools are available. - */ - private void checkPrerequisites() throws IOException, InterruptedException { - if (!isCommandAvailable("git", "--version")) { - throw new IllegalStateException("Git is not installed or not in PATH. Please install Git."); - } - - if (!isCommandAvailable("dotnet", "--version")) { - throw new IllegalStateException( - ".NET SDK is not installed or not in PATH. " + - "Please install .NET SDK 10.0+ from https://dotnet.microsoft.com/download" - ); - } - - ToolingConstants.printDebug("Prerequisites check passed."); - } - - private boolean isCommandAvailable(String... command) { - try { - ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); - Process process = pb.start(); - boolean finished = process.waitFor(10, TimeUnit.SECONDS); - if (finished) { - String output = new String(process.getInputStream().readAllBytes()); - ToolingConstants.printDebug(command[0] + " version: " + output.trim().split("\n")[0]); - } - return finished && process.exitValue() == 0; - } catch (Exception e) { - return false; - } - } - - /** - * Checks if the repository is already cloned. - */ - public boolean isCloned() { - return Files.exists(repoPath.resolve(".git")); - } - - /** - * Checks if the project has been built. - */ - public boolean isBuilt() { - return Files.exists(repoPath.resolve("CreateAtlases/bin")); - } - - /** - * Clones the MinecraftRenderer repository. - */ - private void cloneRepository() throws IOException, InterruptedException { - System.out.println("Cloning MinecraftRenderer repository..."); - Files.createDirectories(repoPath.getParent()); - - ProcessBuilder pb = new ProcessBuilder( - "git", "clone", "--depth", "1", REPO_URL, repoPath.toString() - ); - runProcess(pb, "Failed to clone repository"); - - System.out.println("Repository cloned successfully."); - } - - /** - * Pulls the latest changes from the repository. - */ - private void pullLatest() throws IOException, InterruptedException { - System.out.println("Pulling latest changes..."); - - ProcessBuilder pb = new ProcessBuilder("git", "pull", "--ff-only"); - pb.directory(repoPath.toFile()); - - try { - runProcess(pb, "Failed to pull latest changes"); - } catch (IOException e) { - System.out.println("Warning: Could not pull latest changes: " + e.getMessage()); - } - } - - /** - * Builds the .NET project. - */ - private void buildProject() throws IOException, InterruptedException { - System.out.println("Building MinecraftRenderer..."); - - ProcessBuilder restorePb = new ProcessBuilder("dotnet", "restore"); - restorePb.directory(repoPath.toFile()); - runProcess(restorePb, "Failed to restore .NET dependencies"); - - ProcessBuilder buildPb = new ProcessBuilder( - "dotnet", "build", - "--configuration", "Release", - "--no-restore" - ); - buildPb.directory(repoPath.toFile()); - runProcess(buildPb, "Failed to build project"); - - System.out.println("Build completed successfully."); - } - - /** - * Renders all items to individual PNG files. - * - * @param outputPath the directory to write rendered images to - * - * @throws IOException if rendering fails - * @throws InterruptedException if the process is interrupted - */ - public void renderAllItems(Path outputPath) throws IOException, InterruptedException { - renderAllItems(outputPath, null, 256); - } - - /** - * Renders all items to individual PNG files. - * - * @param outputPath the directory to write rendered images to - * @param minecraftVersion the Minecraft version for assets (null for default) - * @param size the render size in pixels - * - * @throws IOException if rendering fails - * @throws InterruptedException if the process is interrupted - */ - public void renderAllItems(Path outputPath, String minecraftVersion, int size) throws IOException, InterruptedException { - if (!isBuilt()) { - throw new IllegalStateException("Project not built. Call setup() first."); - } - - Path assetsPath = ensureMinecraftAssets(minecraftVersion); - Files.createDirectories(outputPath); - - Path batchDir = createBatchRenderScript(assetsPath, outputPath, size); - Path batchProject = batchDir.resolve("BatchRenderer.csproj").toAbsolutePath(); - - try { - System.out.println("Building batch renderer..."); - System.out.println("Project path: " + batchProject); - - ProcessBuilder buildPb = new ProcessBuilder( - "dotnet", "build", - batchProject.toString(), - "--configuration", "Release" - ); - buildPb.directory(repoPath.toFile()); - runProcess(buildPb, "Failed to build batch renderer"); - - System.out.println("Rendering items to: " + outputPath.toAbsolutePath()); - System.out.println("Using assets from: " + assetsPath.toAbsolutePath()); - - ProcessBuilder runPb = new ProcessBuilder( - "dotnet", "run", - "--project", batchProject.toString(), - "--configuration", "Release", - "--no-build" - ); - runPb.directory(repoPath.toFile()); - runProcessWithOutput(runPb); - } finally { - ToolingConstants.deleteDirectory(batchDir); - } - } - - /** - * Creates a C# project that batch renders all items to individual PNGs. - * Reads template files from resources and substitutes placeholders. - */ - private Path createBatchRenderScript(Path assetsPath, Path outputPath, int size) throws IOException { - Path batchDir = repoPath.resolve("BatchRenderer_" + System.currentTimeMillis()); - Files.createDirectories(batchDir); - - try (InputStream csprojStream = getClass().getResourceAsStream("/renderer/BatchRenderer.csproj")) { - if (csprojStream == null) { - throw new IOException("Could not find BatchRenderer.csproj in resources"); - } - Files.copy(csprojStream, batchDir.resolve("BatchRenderer.csproj")); - } - - String programTemplate; - try (InputStream programStream = getClass().getResourceAsStream("/renderer/Program.cs")) { - if (programStream == null) { - throw new IOException("Could not find Program.cs in resources"); - } - programTemplate = new String(programStream.readAllBytes()); - } - - String program = programTemplate - .replace("{{DATA_PATH}}", assetsPath.toAbsolutePath().toString().replace("\\", "\\\\")) - .replace("{{OUTPUT_PATH}}", outputPath.toAbsolutePath().toString().replace("\\", "\\\\")) - .replace("{{SIZE}}", String.valueOf(size)); - - Files.writeString(batchDir.resolve("Program.cs"), program); - - return batchDir; - } - - /** - * Ensures Minecraft assets are downloaded and returns the path to them. - */ - private Path ensureMinecraftAssets(String version) throws IOException, InterruptedException { - Path assetsPath = ToolingConstants.MINECRAFT_ASSETS; - - if (Files.exists(assetsPath.resolve("models")) && Files.exists(assetsPath.resolve("textures"))) { - System.out.println("Minecraft assets already downloaded."); - return assetsPath; - } - - System.out.println("Downloading Minecraft assets (this may take a moment)..."); - AssetDownloader downloader = new AssetDownloader( - version, - assetsPath, - true, // keep JAR (for caching) - true // clean first - ); - downloader.setDebug(debug); - downloader.download(); - - return assetsPath; - } - - private void runProcess(ProcessBuilder pb, String errorMessage) throws IOException, InterruptedException { - pb.redirectErrorStream(true); - Process process = pb.start(); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - ToolingConstants.printDebug(line); - } - } - - int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new IOException(errorMessage + " (exit code: " + exitCode + ")"); - } - } - - private void runProcessWithOutput(ProcessBuilder pb) throws IOException, InterruptedException { - pb.redirectErrorStream(true); - Process process = pb.start(); - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - System.out.println(line); - } - } - - int exitCode = process.waitFor(); - if (exitCode != 0) { - throw new IOException("Process failed with exit code: " + exitCode); - } - } - -} diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetTool.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetTool.java index 68aa630..894bcca 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetTool.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetTool.java @@ -1,7 +1,8 @@ package net.aerh.imagegenerator.tooling.minecraft; -import net.aerh.imagegenerator.tooling.ToolingConstants; -import net.aerh.imagegenerator.tooling.ToolingArguments; +import net.aerh.imagegenerator.tooling.CliArgs; +import net.aerh.imagegenerator.tooling.ToolingSupport; +import net.aerh.imagegenerator.tooling.ToolingPaths; import net.aerh.imagegenerator.tooling.spritesheet.AtlasLayout; import net.aerh.imagegenerator.tooling.spritesheet.OverlayColorConfigGenerator; import net.aerh.imagegenerator.tooling.spritesheet.OverlayGenerator; @@ -9,6 +10,9 @@ import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; /** * Unified CLI tool for Minecraft asset operations. @@ -41,6 +45,21 @@ */ public class MinecraftAssetTool { + public static final String SIZE_FLAG = "--size"; + public static final String VERSION_FLAG = "--version"; + + private static final int DEFAULT_RENDER_SIZE = 256; + + private static final Set FLAGS = Set.of( + "--all", "--pipeline", "--download", "--render", "--spritesheet", "--overlays", "--skip-download", + "--skip-render", "--debug", "--clean", "--no-clean", "--help" + ); + private static final Set VALUED_FLAGS = Set.of( + VERSION_FLAG, SIZE_FLAG, AtlasLayout.TILE_SIZE_FLAG, AtlasLayout.ATLAS_WIDTH_FLAG, + AtlasLayout.ATLAS_HEIGHT_FLAG, AtlasLayout.PADDING_FLAG, ToolingPaths.WORK_DIR_FLAG, + ToolingPaths.OUTPUT_DIR_FLAG, OverlayGenerator.DEFINITIONS_FLAG + ); + private boolean runDownload = false; private boolean runRender = false; private boolean runSpritesheet = false; @@ -50,19 +69,71 @@ public class MinecraftAssetTool { private boolean debug = false; private String minecraftVersion = null; - private int renderSize = 256; - private Integer tileSize; - private int atlasWidth = 4096; - private int atlasHeight = Integer.MAX_VALUE; - private int atlasPadding; + private int renderSize = DEFAULT_RENDER_SIZE; private boolean keepJar = true; - private boolean forceReclone = false; - private boolean forceRebuild = false; - private boolean cleanBuildDir = true; + /** Null until {@code --clean} or {@code --no-clean} decides; the default then depends on the work directory. */ + private Boolean cleanWorkDir = null; + + private ToolingPaths paths = ToolingPaths.defaults(); + private AtlasLayout layout = AtlasLayout.DEFAULT; + private Path overlayDefinitions = null; public static void main(String[] args) { - ToolingArguments.configureDirectories(args); + if (CliArgs.hasFlag(args, "--help")) { + printHelp(); + return; + } + + MinecraftAssetTool tool; + try { + tool = parse(args); + } catch (IllegalArgumentException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + return; + } + + try { + tool.run(); + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + if (tool.debug) { + e.printStackTrace(); + } + System.exit(1); + } + } + + /** + * Reads every flag and validates the resulting configuration, so a bad layout or render size fails here + * rather than after a download and render have already run. + * + * @throws IllegalArgumentException for a value that cannot be used + */ + static MinecraftAssetTool parse(String[] args) { + CliArgs.checkKnown(args, FLAGS, VALUED_FLAGS); + MinecraftAssetTool tool = new MinecraftAssetTool(); + tool.paths = ToolingPaths.fromArgs(args); + tool.layout = AtlasLayout.fromArgs(args, AtlasLayout.DEFAULT); + tool.renderSize = CliArgs.intValue(args, SIZE_FLAG, DEFAULT_RENDER_SIZE); + tool.minecraftVersion = CliArgs.value(args, VERSION_FLAG) + .map(String::trim) + .filter(version -> !version.isEmpty()) + .orElse(null); + tool.overlayDefinitions = CliArgs.pathValue(args, OverlayGenerator.DEFINITIONS_FLAG).orElse(null); + + if (tool.renderSize <= 0) { + throw new IllegalArgumentException(SIZE_FLAG + " must be positive, got " + tool.renderSize); + } + + if (tool.overlayDefinitions != null) { + try { + OverlayGenerator.checkDefinitions(tool.overlayDefinitions); + } catch (IOException e) { + throw new IllegalArgumentException(OverlayGenerator.DEFINITIONS_FLAG + ": " + e.getMessage(), e); + } + } for (String arg : args) { switch (arg) { @@ -80,29 +151,12 @@ public static void main(String[] args) { case "--skip-render" -> tool.skipRender = true; case "--debug" -> { tool.debug = true; - ToolingConstants.setDebugEnabled(true); - } - case "--force-clone" -> tool.forceReclone = true; - case "--force-build" -> tool.forceRebuild = true; - case "--no-clean" -> tool.cleanBuildDir = false; - case "--help" -> { - printHelp(); - return; + ToolingSupport.setDebugEnabled(true); } + case "--clean" -> tool.cleanWorkDir = true; + case "--no-clean" -> tool.cleanWorkDir = false; default -> { - if (arg.startsWith("--version=")) { - tool.minecraftVersion = arg.substring("--version=".length()); - } else if (arg.startsWith("--tile-size=")) { - tool.tileSize = Integer.parseInt(arg.substring("--tile-size=".length())); - } else if (arg.startsWith("--atlas-width=")) { - tool.atlasWidth = Integer.parseInt(arg.substring("--atlas-width=".length())); - } else if (arg.startsWith("--atlas-height=")) { - tool.atlasHeight = Integer.parseInt(arg.substring("--atlas-height=".length())); - } else if (arg.startsWith("--padding=")) { - tool.atlasPadding = Integer.parseInt(arg.substring("--padding=".length())); - } else if (arg.startsWith("--size=")) { - tool.renderSize = Integer.parseInt(arg.substring("--size=".length())); - } + // Valued flags were read above and checkKnown has already rejected anything else. } } } @@ -114,15 +168,7 @@ public static void main(String[] args) { tool.runOverlays = true; } - try { - tool.run(); - } catch (Exception e) { - System.err.println("Error: " + e.getMessage()); - if (tool.debug) { - e.printStackTrace(); - } - System.exit(1); - } + return tool; } private static void printHelp() { @@ -135,25 +181,27 @@ private static void printHelp() { System.out.println("Steps (can combine multiple):"); System.out.println(" --all, --pipeline Run complete pipeline (download + render + spritesheet + overlays)"); System.out.println(" --download Download Minecraft assets from Mojang"); - System.out.println(" --render Render items/blocks using MinecraftRenderer (.NET)"); + System.out.println(" --render Render every block and item icon from the client JAR"); System.out.println(" --spritesheet Generate spritesheet from rendered images"); System.out.println(" --overlays Generate overlay spritesheet and color configs from assets"); System.out.println(); System.out.println("Options:"); System.out.println(" --version= Minecraft version (default: latest release)"); - System.out.println(" --size= Render and default atlas tile size (default: 256)"); - System.out.println(" --tile-size= Override atlas tile size"); + System.out.println(" --size= Render size in pixels (default: 256)"); + System.out.println(" --tile-size= Atlas tile size in pixels (default: 256)"); System.out.println(" --atlas-width= Maximum atlas page width (default: 4096)"); - System.out.println(" --atlas-height= Maximum atlas page height (default: unlimited)"); - System.out.println(" --padding= Transparent gutter on every side of each tile"); - System.out.println(" --work-dir= Extracted assets, rendered images and caches"); + System.out.println(" --atlas-height= Maximum atlas page height (default: unlimited, one page)"); + System.out.println(" --padding= Transparent gutter on every side of each tile (default: 0)"); + System.out.println(" --work-dir= Extracted assets, rendered images and caches (default: tooling/build)"); System.out.println(" --output-dir= Generated spritesheets and JSON directory"); - System.out.println(" --renderer-dir=

MinecraftRenderer checkout directory"); + System.out.println(" (default: generator/src/main/resources/minecraft/assets)"); + System.out.println(" --overlay-definitions="); + System.out.println(" Overlay definitions JSON to use instead of the bundled one"); System.out.println(" --skip-download Skip download step (use cached assets)"); System.out.println(" --skip-render Skip render step (use existing renders)"); - System.out.println(" --force-clone Force re-clone MinecraftRenderer repository"); - System.out.println(" --force-build Force rebuild MinecraftRenderer"); - System.out.println(" --no-clean Keep build directory after completion"); + System.out.println(" --clean Delete the work directory after completion"); + System.out.println(" (the default for tooling/build; a custom --work-dir is kept unless asked)"); + System.out.println(" --no-clean Keep the work directory after completion"); System.out.println(" --debug Enable debug output"); System.out.println(" --help Show this help message"); System.out.println(); @@ -162,7 +210,7 @@ private static void printHelp() { System.out.println(" java MinecraftAssetTool --all --skip-download # Pipeline without download"); System.out.println(" java MinecraftAssetTool --download --version=1.21.4"); System.out.println(" java MinecraftAssetTool --render --size=512"); - System.out.println(" java MinecraftAssetTool --spritesheet"); + System.out.println(" java MinecraftAssetTool --spritesheet --atlas-height=4096 --padding=1"); System.out.println(); System.out.println("Maven profiles:"); System.out.println(" mvn compile -P asset-pipeline -pl tooling"); @@ -171,8 +219,7 @@ private static void printHelp() { System.out.println(" mvn compile -P generate-spritesheet -pl tooling"); System.out.println(); System.out.println("Prerequisites:"); - System.out.println(" - .NET SDK 10.0+ for rendering (https://dotnet.microsoft.com/download)"); - System.out.println(" - Git for cloning MinecraftRenderer"); + System.out.println(" - A JDK and network access to Mojang's servers; icons are rendered in this JVM"); } public void run() throws Exception { @@ -202,8 +249,8 @@ public void run() throws Exception { runOverlaysStep(); } - if (cleanBuildDir) { - cleanBuildDirectory(); + if (cleansWorkDir()) { + cleanWorkDirectory(Path.of("").toAbsolutePath()); } System.out.println("\n" + "=".repeat(60)); @@ -211,18 +258,36 @@ public void run() throws Exception { System.out.println("=".repeat(60)); } - private void printConfig() { - System.out.println(); - System.out.println("Configuration:"); - System.out.println(" Minecraft version: " + (minecraftVersion != null ? minecraftVersion : "latest")); - System.out.println(" Render size: " + renderSize + "px"); - System.out.println(" Steps: " + buildStepsString()); - if (debug) { - System.out.println(" Debug: enabled"); - } + ToolingPaths paths() { + return paths; } - private String buildStepsString() { + AtlasLayout layout() { + return layout; + } + + int renderSize() { + return renderSize; + } + + String minecraftVersion() { + return minecraftVersion; + } + + Path overlayDefinitions() { + return overlayDefinitions; + } + + /** + * Whether the work directory is deleted at the end of the run. The repository's own {@code tooling/build} + * is cleaned unless {@code --no-clean} says otherwise; a work directory the caller chose is left alone + * unless they pass {@code --clean}. + */ + boolean cleansWorkDir() { + return cleanWorkDir != null ? cleanWorkDir : paths.isDefaultWorkDir(); + } + + String stepsSummary() { StringBuilder sb = new StringBuilder(); if (runDownload) sb.append(skipDownload ? "download(skipped) " : "download "); if (runRender) sb.append(skipRender ? "render(skipped) " : "render "); @@ -231,16 +296,28 @@ private String buildStepsString() { return sb.toString().trim(); } + private void printConfig() { + System.out.println(); + System.out.println("Configuration:"); + System.out.println(" Minecraft version: " + (minecraftVersion != null ? minecraftVersion : "latest")); + System.out.println(" Render size: " + renderSize + "px"); + System.out.println(" Atlas layout: " + layout.tileSize() + "px tiles, pages up to " + layout.maxWidth() + "x" + + (layout.maxHeight() == AtlasLayout.UNLIMITED ? "unlimited" : layout.maxHeight()) + ", padding " + layout.padding() + "px"); + System.out.println(" Work dir: " + paths.workDir()); + System.out.println(" Output dir: " + paths.outputDir()); + System.out.println(" Overlay definitions: " + (overlayDefinitions != null ? overlayDefinitions : "bundled")); + System.out.println(" Clean work dir after run: " + (cleansWorkDir() ? "yes" : "no")); + System.out.println(" Steps: " + stepsSummary()); + if (debug) { + System.out.println(" Debug: enabled"); + } + } + private void runDownloadStep() throws IOException, InterruptedException { System.out.println("\n[Download] Downloading Minecraft assets..."); System.out.println("-".repeat(40)); - AssetDownloader downloader = new AssetDownloader( - minecraftVersion, - ToolingConstants.MINECRAFT_ASSETS, - keepJar, - true // clean - ); + AssetDownloader downloader = new AssetDownloader(paths, minecraftVersion, keepJar, true); downloader.setDebug(debug); int count = downloader.download(); this.minecraftVersion = downloader.getResolvedVersion(); @@ -248,70 +325,72 @@ private void runDownloadStep() throws IOException, InterruptedException { System.out.println("[Download] Extracted " + count + " files"); } - private void runRenderStep() throws IOException, InterruptedException { - System.out.println("\n[Render] Running MinecraftRenderer..."); + /** + * Renders every block and item icon in this JVM from the cached client JAR. When no version was given and + * no JAR is cached yet, the download step runs first so the render has something to read; a render that + * fails ends the run, because packing a missing or stale set of icons afterwards would look like success. + */ + private void runRenderStep() throws Exception { + System.out.println("\n[Render] Rendering block and item icons..."); System.out.println("-".repeat(40)); - if (!isDotNetAvailable()) { - System.out.println("WARNING: .NET SDK not found. Skipping render step."); - System.out.println("Install .NET SDK 10.0+ from https://dotnet.microsoft.com/download"); - return; + if (minecraftVersion == null) { + minecraftVersion = paths.cachedClientVersion().orElse(null); } - if (Files.exists(ToolingConstants.RENDERED_ITEMS)) { - System.out.println("Cleaning previous renders: " + ToolingConstants.RENDERED_ITEMS); - ToolingConstants.deleteDirectory(ToolingConstants.RENDERED_ITEMS); + if (minecraftVersion == null || !Files.isRegularFile(paths.jarPath(minecraftVersion))) { + System.out.println("No cached client JAR for the requested version; downloading first."); + runDownloadStep(); } - ItemRenderer renderer = new ItemRenderer(); - renderer.setDebug(debug); - - try { - renderer.setup(forceReclone, forceRebuild); - renderer.renderAllItems(ToolingConstants.RENDERED_ITEMS, minecraftVersion, renderSize); - } catch (Exception e) { - System.out.println("WARNING: Renderer failed: " + e.getMessage()); - if (debug) { - e.printStackTrace(); - } + if (Files.exists(paths.renderedItems())) { + System.out.println("Cleaning previous renders: " + paths.renderedItems()); + ToolingSupport.deleteDirectory(paths.renderedItems()); } + + new ItemIconRenderer(paths).renderAll(paths.renderedItems(), minecraftVersion, renderSize); } private void runSpritesheetStep() { System.out.println("\n[Spritesheet] Generating spritesheets..."); System.out.println("-".repeat(40)); - new SpritesheetGenerator(ToolingConstants.RENDERED_ITEMS.toString(), - new AtlasLayout(tileSize != null ? tileSize : renderSize, atlasWidth, atlasHeight, atlasPadding)).generate(); + new SpritesheetGenerator(paths, layout).generate(); } private void runOverlaysStep() throws IOException { System.out.println("\n[Overlays] Generating overlay spritesheets and color configs..."); System.out.println("-".repeat(40)); - new OverlayGenerator().generate(); - new OverlayColorConfigGenerator(minecraftVersion).generate(); + OverlayGenerator generator = overlayDefinitions != null + ? new OverlayGenerator(paths, overlayDefinitions) + : new OverlayGenerator(paths); + generator.generate(); + new OverlayColorConfigGenerator(paths, minecraftVersion).generate(); } - private void cleanBuildDirectory() { - System.out.println("\n[Cleanup] Removing build directory..."); - try { - ToolingConstants.deleteDirectory(ToolingConstants.BUILD_DIR); - System.out.println("[Cleanup] Build directory removed."); - } catch (IOException e) { - System.err.println("[Cleanup] Failed to remove build directory: " + e.getMessage()); + /** + * Deletes the work directory unless doing so would take the current directory, the output or a custom + * renderer checkout with it. A refused cleanup the caller asked for explicitly is an error; a refused + * default cleanup is only reported. + */ + void cleanWorkDirectory(Path currentDir) { + System.out.println("\n[Cleanup] Removing work directory " + paths.workDir() + "..."); + + Optional refusal = paths.cleanupRefusal(currentDir); + if (refusal.isPresent()) { + if (Boolean.TRUE.equals(cleanWorkDir)) { + throw new IllegalStateException("Refusing to clean: " + refusal.get()); + } + System.err.println("[Cleanup] Skipped: " + refusal.get()); + return; } - } - private boolean isDotNetAvailable() { try { - ProcessBuilder pb = new ProcessBuilder("dotnet", "--version"); - pb.redirectErrorStream(true); - Process process = pb.start(); - boolean finished = process.waitFor(10, java.util.concurrent.TimeUnit.SECONDS); - return finished && process.exitValue() == 0; - } catch (Exception e) { - return false; + ToolingSupport.deleteDirectory(currentDir.resolve(paths.workDir())); + System.out.println("[Cleanup] Work directory removed."); + } catch (IOException e) { + System.err.println("[Cleanup] Failed to remove work directory: " + e.getMessage()); } } diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractor.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractor.java index c256c06..1d1ec01 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractor.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractor.java @@ -1,42 +1,89 @@ package net.aerh.imagegenerator.tooling.minecraft; -import net.aerh.imagegenerator.tooling.ToolingConstants; - import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.Map; /** - * Extracts armor trim color palettes from Minecraft's 8x1 pixel PNG files. - * Each trim material has an 8-pixel wide, 1-pixel tall PNG where each pixel - * represents a color in the trim gradient. + * Reads armor trim colour palettes out of extracted Minecraft assets. + *

+ * Every trim material has an 8x1 PNG whose pixels are that material's gradient, and a key palette holds the + * greys the greyscale trim overlay textures are drawn in. The game recolours an overlay by swapping each key + * grey for the material colour at the same index, so a consumer needs both halves. Two layouts are known: + * current versions keep the materials in {@code textures/palettes/trim/} with the key at + * {@code textures/palettes/trim_base.png}; versions before the palette move kept everything in + * {@code textures/trims/color_palettes/} with the key named {@code trim_palette.png}. */ -public class TrimPaletteExtractor { +public final class TrimPaletteExtractor { + + static final Path CURRENT_MATERIALS_DIR = Path.of("textures/palettes/trim"); + static final Path CURRENT_KEY_FILE = Path.of("textures/palettes/trim_base.png"); + static final Path LEGACY_MATERIALS_DIR = Path.of("textures/trims/color_palettes"); + static final String LEGACY_KEY_NAME = "trim_palette.png"; - private static final Path DEFAULT_PALETTES_PATH = ToolingConstants.MINECRAFT_ASSETS.resolve("textures/trims/color_palettes"); + private TrimPaletteExtractor() { + } /** - * Extracts all trim color palettes from Minecraft assets. - * - * @return map of palette name to array of RGB colors + * The key greys and every material palette, materials in file name order. */ - public static Map extractAllPalettes() { - return extractAllPalettes(DEFAULT_PALETTES_PATH); + public record TrimPalettes(int[] key, Map materials) { + + public static final TrimPalettes NONE = new TrimPalettes(new int[0], Map.of()); + + public boolean isEmpty() { + return materials.isEmpty(); + } } /** - * Extracts all trim color palettes from the specified path. + * Loads the trim palettes from extracted assets, trying the current layout first and then the legacy one. * - * @param palettesPath path to the color_palettes directory + * @return the palettes, or {@link TrimPalettes#NONE} after a message on stderr when neither layout exists * - * @return map of palette name to array of RGB colors + * @throws IOException if a layout is present but its key palette is missing or unreadable + */ + public static TrimPalettes load(Path minecraftAssets) throws IOException { + Path currentDir = minecraftAssets.resolve(CURRENT_MATERIALS_DIR); + if (Files.isDirectory(currentDir)) { + return read(currentDir, minecraftAssets.resolve(CURRENT_KEY_FILE), null); + } + + Path legacyDir = minecraftAssets.resolve(LEGACY_MATERIALS_DIR); + if (Files.isDirectory(legacyDir)) { + return read(legacyDir, legacyDir.resolve(LEGACY_KEY_NAME), LEGACY_KEY_NAME); + } + + System.err.println("Trim palettes not found under " + minecraftAssets + + " (looked in " + CURRENT_MATERIALS_DIR + " and " + LEGACY_MATERIALS_DIR + ")"); + return TrimPalettes.NONE; + } + + private static TrimPalettes read(Path materialsDir, Path keyFile, String excludedName) throws IOException { + if (!Files.isRegularFile(keyFile)) { + throw new IOException("Trim key palette not found: " + keyFile); + } + + int[] key = extractPalette(keyFile.toFile()); + if (key == null) { + throw new IOException("Trim key palette is not a readable image: " + keyFile); + } + + return new TrimPalettes(key, extractAllPalettes(materialsDir, excludedName)); + } + + /** + * Extracts every material palette in a directory, keyed by file name without extension, skipping the file + * named {@code excludedName} (the legacy key palette lives among the materials). */ - public static Map extractAllPalettes(Path palettesPath) { + private static Map extractAllPalettes(Path palettesPath, String excludedName) { Map palettes = new LinkedHashMap<>(); if (!Files.exists(palettesPath)) { @@ -45,13 +92,14 @@ public static Map extractAllPalettes(Path palettesPath) { } File[] paletteFiles = palettesPath.toFile().listFiles((dir, name) -> - name.endsWith(".png") && !name.equals("trim_palette.png") + name.endsWith(".png") && !name.equals(excludedName) ); if (paletteFiles == null) { return palettes; } + Arrays.sort(paletteFiles, Comparator.comparing(File::getName)); for (File file : paletteFiles) { String name = file.getName().replace(".png", ""); int[] colors = extractPalette(file); diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGenerator.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGenerator.java index 7f5f9e3..4063e25 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGenerator.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGenerator.java @@ -9,41 +9,39 @@ import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; -import java.io.File; -import java.io.FileWriter; import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; /** * Base class for texture atlas generation. + *

+ * Packs the entries a subclass loads into one or more fixed-grid pages described by an {@link AtlasLayout}, + * writes each page as a PNG and records every entry's page and position in a JSON array. The first page keeps + * the configured file name; further pages get a {@code -1}, {@code -2} suffix before the extension. */ public abstract class AtlasGenerator { - protected static final int IMAGE_SIZE = 256; protected static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - protected final int atlasWidth; protected final AtlasLayout layout; - protected final String atlasOutputPath; - protected final String jsonOutputPath; + protected final Path atlasOutputPath; + protected final Path jsonOutputPath; - protected AtlasGenerator(int atlasWidth, String atlasOutputPath, String jsonOutputPath) { - this(new AtlasLayout(IMAGE_SIZE, atlasWidth, Integer.MAX_VALUE, 0), atlasOutputPath, jsonOutputPath); - } - - protected AtlasGenerator(AtlasLayout layout, String atlasOutputPath, String jsonOutputPath) { + protected AtlasGenerator(AtlasLayout layout, Path atlasOutputPath, Path jsonOutputPath) { this.layout = layout; - this.atlasWidth = layout.maxWidth(); this.atlasOutputPath = atlasOutputPath; this.jsonOutputPath = jsonOutputPath; } - protected static BufferedImage resizeToAtlasSize(BufferedImage source) { - return ImageUtil.resizeImageRaw(source, IMAGE_SIZE, IMAGE_SIZE); - } - protected BufferedImage resizeToLayoutSize(BufferedImage source) { return ImageUtil.resizeImageRaw(source, layout.tileSize(), layout.tileSize()); } @@ -52,6 +50,10 @@ protected BufferedImage resizeToLayoutSize(BufferedImage source) { protected abstract JsonObject entryToJson(AtlasEntry entry); + /** + * Loads, packs and writes the atlas. Fails with an {@link UncheckedIOException} rather than leaving a + * half-written set of files behind. + */ public void generate() { List entries = loadTextures(); System.out.println("Loaded " + entries.size() + " textures."); @@ -62,43 +64,43 @@ public void generate() { } int capacity = layout.capacity(); - for (int start = 0, page = 0; start < entries.size(); page++) { + int pageCount = 0; + + for (int start = 0; start < entries.size(); pageCount++) { int end = (int) Math.min(entries.size(), (long) start + capacity); List batch = entries.subList(start, end); - BufferedImage atlas = packAtlas(batch); + for (AtlasEntry entry : batch) { - entry.page = page; - entry.atlasWidth = atlas.getWidth(); - entry.atlasHeight = atlas.getHeight(); + entry.page = pageCount; } - saveAtlas(atlas, page); + + saveAtlas(packAtlas(batch), pageOutputPath(pageCount)); start = end; } + + removeStalePages(pageCount); saveCoordinatesJson(entries); System.out.println("Atlas generation complete!"); } protected BufferedImage packAtlas(List entries) { - int texturesPerRow = layout.columns(); - int rows = (entries.size() + texturesPerRow - 1) / texturesPerRow; - int atlasHeight = rows * layout.pitch(); + int columns = layout.columns(); + int rows = (entries.size() + columns - 1) / columns; + int tileSize = layout.tileSize(); - BufferedImage atlas = new BufferedImage(atlasWidth, atlasHeight, BufferedImage.TYPE_INT_ARGB); + BufferedImage atlas = new BufferedImage(layout.maxWidth(), rows * layout.pitch(), BufferedImage.TYPE_INT_ARGB); int[] atlasPixels = ((DataBufferInt) atlas.getRaster().getDataBuffer()).getData(); for (int i = 0; i < entries.size(); i++) { AtlasEntry entry = entries.get(i); - int col = i % texturesPerRow; - int row = i / texturesPerRow; - entry.x = col * layout.pitch() + layout.padding(); - entry.y = row * layout.pitch() + layout.padding(); + entry.x = (i % columns) * layout.pitch() + layout.padding(); + entry.y = (i / columns) * layout.pitch() + layout.padding(); int[] texturePixels = ((DataBufferInt) entry.image.getRaster().getDataBuffer()).getData(); - for (int py = 0; py < layout.tileSize(); py++) { - int atlasOffset = (entry.y + py) * atlasWidth + entry.x; - int textureOffset = py * layout.tileSize(); - System.arraycopy(texturePixels, textureOffset, atlasPixels, atlasOffset, layout.tileSize()); + for (int py = 0; py < tileSize; py++) { + int atlasOffset = (entry.y + py) * atlas.getWidth() + entry.x; + System.arraycopy(texturePixels, py * tileSize, atlasPixels, atlasOffset, tileSize); } double progress = (i + 1) * 100.0 / entries.size(); @@ -109,77 +111,103 @@ protected BufferedImage packAtlas(List entries) { return atlas; } - protected void saveAtlas(BufferedImage atlas) { - saveAtlasToPath(atlas, atlasOutputPath); + /** + * The file a given page is written to: page 0 is {@code atlasOutputPath} itself, later pages get the page + * number appended to the file name, before its extension if it has one. Only the file name is inspected, so + * dots in directory names are left alone. + */ + protected Path pageOutputPath(int page) { + if (page == 0) { + return atlasOutputPath; + } + + NameParts parts = nameParts(); + return atlasOutputPath.resolveSibling(parts.stem() + "-" + page + parts.extension()); } - protected void saveAtlas(BufferedImage atlas, int page) { - if (page == 0) saveAtlas(atlas); - else saveAtlasToPath(atlas, pageOutputPath(page)); + private NameParts nameParts() { + String fileName = atlasOutputPath.getFileName().toString(); + int dot = fileName.lastIndexOf('.'); + return dot < 0 + ? new NameParts(fileName, "") + : new NameParts(fileName.substring(0, dot), fileName.substring(dot)); } - private void saveAtlasToPath(BufferedImage atlas, String path) { - File outputFile = new File(path); - File parentDir = outputFile.getParentFile(); - if (!parentDir.exists() && !parentDir.mkdirs()) { - System.err.println("Failed to create directory: " + parentDir.getAbsolutePath()); - return; - } + private record NameParts(String stem, String extension) { + } + private void saveAtlas(BufferedImage atlas, Path path) { try { - System.out.println("Saving atlas to: " + outputFile.getAbsolutePath()); - ImageIO.write(atlas, "png", outputFile); + createParentDirectories(path); + System.out.println("Saving atlas to: " + path.toAbsolutePath()); + if (!ImageIO.write(atlas, "png", path.toFile())) { + throw new IOException("no PNG writer is available"); + } System.out.println("Atlas saved successfully!"); } catch (IOException e) { - System.err.println("Failed to save atlas: " + e.getMessage()); - e.printStackTrace(); + throw new UncheckedIOException("Failed to save atlas " + path, e); } } - protected String pageOutputPath(int page) { - if (page == 0) return atlasOutputPath; - int dot = atlasOutputPath.lastIndexOf('.'); - return dot < 0 ? atlasOutputPath + "-" + page - : atlasOutputPath.substring(0, dot) + "-" + page + atlasOutputPath.substring(dot); + /** + * Deletes every numbered page file beyond the last page just written. They can only be left over from an + * earlier run that needed more pages, and a consumer would otherwise treat them as current. The directory is + * scanned rather than counted upward, so a gap in the numbering does not hide later pages. + */ + private void removeStalePages(int pageCount) { + NameParts parts = nameParts(); + Pattern numberedPage = Pattern.compile(Pattern.quote(parts.stem()) + "-(\\d{1,9})" + Pattern.quote(parts.extension())); + int firstStale = Math.max(pageCount, 1); + Path directory = atlasOutputPath.toAbsolutePath().getParent(); + + try (Stream files = Files.list(directory)) { + for (Path file : files.toList()) { + Matcher matcher = numberedPage.matcher(file.getFileName().toString()); + if (matcher.matches() && Integer.parseInt(matcher.group(1)) >= firstStale) { + Files.delete(file); + System.out.println("Removed stale atlas page: " + file); + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to remove stale atlas pages next to " + atlasOutputPath, e); + } } protected void saveCoordinatesJson(List entries) { - File jsonFile = new File(jsonOutputPath); - File parentDir = jsonFile.getParentFile(); - if (!parentDir.exists() && !parentDir.mkdirs()) { - System.err.println("Failed to create directory: " + parentDir.getAbsolutePath()); - return; - } - JsonArray jsonArray = new JsonArray(); for (AtlasEntry entry : entries) { JsonObject object = entryToJson(entry); object.addProperty("page", entry.page); - object.addProperty("atlas", new File(pageOutputPath(entry.page)).getName()); - object.addProperty("atlasWidth", entry.atlasWidth); - object.addProperty("atlasHeight", entry.atlasHeight); + object.addProperty("atlas", pageOutputPath(entry.page).getFileName().toString()); jsonArray.add(object); } - try (FileWriter writer = new FileWriter(jsonFile)) { - GSON.toJson(jsonArray, writer); - System.out.println("Coordinates JSON saved to: " + jsonFile.getAbsolutePath()); + try { + createParentDirectories(jsonOutputPath); + try (Writer writer = Files.newBufferedWriter(jsonOutputPath)) { + GSON.toJson(jsonArray, writer); + } + System.out.println("Coordinates JSON saved to: " + jsonOutputPath.toAbsolutePath()); } catch (IOException e) { - System.err.println("Failed to save coordinates JSON: " + e.getMessage()); - e.printStackTrace(); + throw new UncheckedIOException("Failed to save coordinates JSON " + jsonOutputPath, e); + } + } + + private static void createParentDirectories(Path file) throws IOException { + Path parent = file.toAbsolutePath().getParent(); + if (parent != null) { + Files.createDirectories(parent); } } /** - * Atlas entry with name, image, position, and optional metadata. + * Atlas entry with name, image, page, position, and optional metadata. */ public static class AtlasEntry { public final String name; public final BufferedImage image; public final Map metadata; public int page; - public int atlasWidth; - public int atlasHeight; public int x; public int y; diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayout.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayout.java index 0d2e784..1be1294 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayout.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayout.java @@ -1,20 +1,76 @@ package net.aerh.imagegenerator.tooling.spritesheet; -/** Fixed-grid atlas dimensions, including an optional transparent gutter. */ +import net.aerh.imagegenerator.tooling.CliArgs; + +/** + * Fixed-grid atlas dimensions: square tiles of {@code tileSize} pixels, each wrapped in a transparent gutter of + * {@code padding} pixels on every side, packed row by row into pages no larger than {@code maxWidth} by + * {@code maxHeight}. + */ public record AtlasLayout(int tileSize, int maxWidth, int maxHeight, int padding) { + + /** Height value meaning "one page, however tall it needs to be". */ + public static final int UNLIMITED = Integer.MAX_VALUE; + + public static final int DEFAULT_TILE_SIZE = 256; + + /** The layout of the atlas bundled with the generator module: 256px tiles, 16 per row, single page, no gutter. */ + public static final AtlasLayout DEFAULT = new AtlasLayout(DEFAULT_TILE_SIZE, DEFAULT_TILE_SIZE * 16, UNLIMITED, 0); + + public static final String TILE_SIZE_FLAG = "--tile-size"; + public static final String ATLAS_WIDTH_FLAG = "--atlas-width"; + public static final String ATLAS_HEIGHT_FLAG = "--atlas-height"; + public static final String PADDING_FLAG = "--padding"; + public AtlasLayout { - if (tileSize <= 0 || padding < 0 || padding > (Integer.MAX_VALUE - tileSize) / 2) { - throw new IllegalArgumentException("Tile size must be positive and padding non-negative"); + if (tileSize <= 0) { + throw new IllegalArgumentException("Tile size must be positive, got " + tileSize); + } + + if (padding < 0) { + throw new IllegalArgumentException("Padding must be non-negative, got " + padding); + } + + if (padding > (Integer.MAX_VALUE - tileSize) / 2) { + throw new IllegalArgumentException("Padding " + padding + " is too large for tile size " + tileSize); } + int pitch = tileSize + 2 * padding; if (maxWidth < pitch || maxHeight < pitch) { - throw new IllegalArgumentException("Atlas bounds must fit at least one padded tile"); + throw new IllegalArgumentException("Atlas bounds " + maxWidth + "x" + maxHeight + + " must fit at least one padded tile of " + pitch + "px"); } } - public int pitch() { return tileSize + 2 * padding; } - public int columns() { return maxWidth / pitch(); } + /** + * Reads the four layout flags, keeping {@code defaults} for any that are absent, and validates the result. + * + * @throws IllegalArgumentException when a value is not an integer or the combination is not a usable layout + */ + public static AtlasLayout fromArgs(String[] args, AtlasLayout defaults) { + return new AtlasLayout( + CliArgs.intValue(args, TILE_SIZE_FLAG, defaults.tileSize()), + CliArgs.intValue(args, ATLAS_WIDTH_FLAG, defaults.maxWidth()), + CliArgs.intValue(args, ATLAS_HEIGHT_FLAG, defaults.maxHeight()), + CliArgs.intValue(args, PADDING_FLAG, defaults.padding()) + ); + } + + /** Distance between the top-left corners of neighbouring tiles. */ + public int pitch() { + return tileSize + 2 * padding; + } + + public int columns() { + return maxWidth / pitch(); + } + + public int rowsPerPage() { + return maxHeight / pitch(); + } + + /** Tiles per page, saturating at {@link Integer#MAX_VALUE} for unlimited heights. */ public int capacity() { - return (int) Math.min(Integer.MAX_VALUE, (long) columns() * (maxHeight / pitch())); + return (int) Math.min(Integer.MAX_VALUE, (long) columns() * rowsPerPage()); } } diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGenerator.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGenerator.java index 7e55347..a07fb70 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGenerator.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGenerator.java @@ -4,11 +4,12 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import net.aerh.imagegenerator.tooling.ToolingConstants; +import net.aerh.imagegenerator.tooling.ToolingPaths; import net.aerh.imagegenerator.tooling.minecraft.ColorExtractor; import net.aerh.imagegenerator.tooling.minecraft.DyeColorData; import net.aerh.imagegenerator.tooling.minecraft.ExtractedColors; import net.aerh.imagegenerator.tooling.minecraft.TrimPaletteExtractor; +import net.aerh.imagegenerator.tooling.minecraft.TrimPaletteExtractor.TrimPalettes; import java.io.File; import java.io.FileWriter; @@ -16,7 +17,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; -import java.util.stream.Stream; /** * Generates overlay_colors.json by extracting colors and trim palette PNGs @@ -36,25 +36,21 @@ public class OverlayColorConfigGenerator { // Default firework star color (white) private static final int DEFAULT_FIREWORK_COLOR = 0xFFFFFF; - // Grayscale values used in Minecraft's trim overlay textures - private static final int[] TRIM_GRAYSCALE_MAP = { - 0xFFE0E0E0, // Light gray - 0xFFC0C0C0, - 0xFFA0A0A0, - 0xFF808080, // Mid-gray - 0xFF606060, - 0xFF404040, - 0xFF202020, - 0xFF000000 // Black - }; - + private final ToolingPaths paths; private final String version; - public OverlayColorConfigGenerator() { - this(null); + /** + * Reads colours from whichever client JAR is cached in the work directory. + */ + public OverlayColorConfigGenerator(ToolingPaths paths) { + this(paths, null); } - public OverlayColorConfigGenerator(String version) { + /** + * @param version the Minecraft version whose cached JAR to read, or null to use whatever JAR is cached + */ + public OverlayColorConfigGenerator(ToolingPaths paths, String version) { + this.paths = paths; this.version = version; } @@ -65,10 +61,10 @@ public void generate() throws IOException { System.out.println("\nGenerating overlay color config..."); ExtractedColors extractedColors = extractColorsFromJar(); - Map trimPalettes = extractTrimPalettes(); + TrimPalettes trimPalettes = extractTrimPalettes(); if (extractedColors == null) { - throw new IOException("Failed to extract colors from JAR. Ensure the Minecraft JAR and mappings are downloaded."); + throw new IOException("Failed to extract colors from the client JAR; see the errors above."); } JsonArray colorConfig = buildColorConfig(extractedColors, trimPalettes); @@ -76,32 +72,29 @@ public void generate() throws IOException { } private ExtractedColors extractColorsFromJar() throws IOException { - String resolvedVersion = version != null ? version : findCachedVersion(); + String resolvedVersion = version != null ? version : paths.cachedClientVersion().orElse(null); if (resolvedVersion == null) { System.err.println(" ERROR: No cached JAR found. Run 'MinecraftAssetTool --download' first."); return null; } - Path jarPath = ToolingConstants.getJarPath(resolvedVersion); - Path mappingsPath = ToolingConstants.getMappingsPath(resolvedVersion); - - if (!Files.exists(jarPath) || !Files.exists(mappingsPath)) { - System.err.println(" ERROR: JAR or mappings not found"); - if (!Files.exists(jarPath)) { - System.err.println(" Missing JAR: " + jarPath); - } - - if (!Files.exists(mappingsPath)) { - System.err.println(" Missing mappings: " + mappingsPath); - } + Path jarPath = paths.jarPath(resolvedVersion); + Path mappingsPath = paths.mappingsPath(resolvedVersion); + if (!Files.exists(jarPath)) { + System.err.println(" ERROR: Missing JAR: " + jarPath); return null; } - System.out.println(" Extracting colors from JAR (version " + resolvedVersion + ")..."); + // Mojang stopped publishing mappings with 26.1; from then on the JAR carries the real class names. + boolean hasMappings = Files.exists(mappingsPath); + System.out.println(" Extracting colors from JAR (version " + resolvedVersion + + (hasMappings ? ", using mappings" : ", unobfuscated") + ")..."); try { - ColorExtractor extractor = new ColorExtractor(jarPath, mappingsPath); + ColorExtractor extractor = hasMappings + ? new ColorExtractor(jarPath, mappingsPath) + : new ColorExtractor(jarPath); ExtractedColors colors = extractor.extract(); System.out.println(" Successfully extracted " + colors.dyeColors.size() + " dye colors and " + colors.potionColors.size() + " potion colors from JAR"); @@ -113,31 +106,14 @@ private ExtractedColors extractColorsFromJar() throws IOException { } } - private String findCachedVersion() throws IOException { - Path jarCacheDir = ToolingConstants.JAR_CACHE; - if (!Files.exists(jarCacheDir)) { - return null; - } - - try (Stream files = Files.list(jarCacheDir)) { - return files - .filter(p -> p.getFileName().toString().endsWith("-client.jar")) - .findFirst() - .map(p -> p.getFileName().toString() - .replace("minecraft-", "") - .replace("-client.jar", "")) - .orElse(null); - } - } - - private Map extractTrimPalettes() { + private TrimPalettes extractTrimPalettes() throws IOException { System.out.println(" Extracting trim palettes from PNG files..."); - Map palettes = TrimPaletteExtractor.extractAllPalettes(); - System.out.println(" Found " + palettes.size() + " trim palettes"); + TrimPalettes palettes = TrimPaletteExtractor.load(paths.minecraftAssets()); + System.out.println(" Found " + palettes.materials().size() + " trim palettes"); return palettes; } - private JsonArray buildColorConfig(ExtractedColors colors, Map trimPalettes) { + private JsonArray buildColorConfig(ExtractedColors colors, TrimPalettes trimPalettes) { JsonArray config = new JsonArray(); if (colors.dyeColors.isEmpty()) { @@ -150,7 +126,7 @@ private JsonArray buildColorConfig(ExtractedColors colors, Map tr if (trimPalettes.isEmpty()) { System.err.println(" ERROR: No trim palettes found in assets!"); } else { - System.out.println(" Adding armor_trim colors (" + trimPalettes.size() + " palettes)"); + System.out.println(" Adding armor_trim colors (" + trimPalettes.materials().size() + " palettes)"); config.add(createArmorTrimConfig(trimPalettes)); } @@ -179,12 +155,12 @@ private JsonArray buildColorConfig(ExtractedColors colors, Map tr } private void saveConfig(JsonArray config) throws IOException { - File outputDir = new File(ToolingConstants.GENERATOR_JSON); + File outputDir = paths.json().toFile(); if (!outputDir.exists() && !outputDir.mkdirs()) { throw new IOException("Failed to create directory: " + outputDir); } - File outputFile = new File(ToolingConstants.GENERATOR_JSON, "overlay_colors.json"); + File outputFile = new File(outputDir, "overlay_colors.json"); try (FileWriter writer = new FileWriter(outputFile)) { GSON.toJson(config, writer); } @@ -209,18 +185,29 @@ private JsonObject createLeatherArmorConfig(Map dyeColors) return config; } - private JsonObject createArmorTrimConfig(Map palettes) { + /** + * Builds the armor_trim colour options. The map goes from each grey of the key palette to its index, which + * is how the trim overlay textures are drawn, and every material palette with the same number of colours + * becomes an option. A material whose palette length differs from the key cannot be indexed and is skipped. + */ + static JsonObject createArmorTrimConfig(TrimPalettes palettes) { JsonObject config = new JsonObject(); config.addProperty("name", "armor_trim"); + int[] key = palettes.key(); JsonObject map = new JsonObject(); - for (int i = 0; i < TRIM_GRAYSCALE_MAP.length; i++) { - map.addProperty(String.valueOf(TRIM_GRAYSCALE_MAP[i]), i); + for (int i = 0; i < key.length; i++) { + map.addProperty(String.valueOf(key[i]), i); } config.add("map", map); JsonObject options = new JsonObject(); - for (Map.Entry entry : palettes.entrySet()) { + for (Map.Entry entry : palettes.materials().entrySet()) { + if (entry.getValue().length != key.length) { + System.err.println(" WARNING: trim palette '" + entry.getKey() + "' has " + entry.getValue().length + + " colours but the key palette has " + key.length + "; skipping it"); + continue; + } options.add(entry.getKey(), jsonArray(entry.getValue())); } config.add("options", options); @@ -286,7 +273,7 @@ private JsonObject createFireworkConfig(Map dyeColors) { return config; } - private JsonArray jsonArray(int... values) { + private static JsonArray jsonArray(int... values) { JsonArray arr = new JsonArray(); for (int v : values) { arr.add(v); diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGenerator.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGenerator.java index 3b91b7a..9f56352 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGenerator.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGenerator.java @@ -1,45 +1,80 @@ package net.aerh.imagegenerator.tooling.spritesheet; -import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import net.aerh.imagegenerator.tooling.ToolingConstants; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import net.aerh.imagegenerator.tooling.CliArgs; +import net.aerh.imagegenerator.tooling.ToolingPaths; import net.hypixel.nerdbot.marmalade.image.ImageUtil; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; -import java.io.FileReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; /** * Generates the overlay spritesheet from Minecraft texture assets. + *

+ * Which overlays exist comes from a definitions file: a list of special overlays with their colour handling, + * and the file name prefixes that mark dyeable armour overlays. The tooling bundles that file as a classpath + * resource; callers can point at their own with {@value #DEFINITIONS_FLAG}. A supplied file replaces the + * bundled one entirely, so a key it leaves out means "none". */ public class OverlayGenerator extends AtlasGenerator { + public static final String DEFINITIONS_FLAG = "--overlay-definitions"; + + private static final String BUNDLED_DEFINITIONS = "/overlay_definitions.json"; private static final int ATLAS_WIDTH = 1_024; - private static final Path ITEM_TEXTURES = ToolingConstants.MINECRAFT_ASSETS.resolve("textures/item"); - private static final Path TRIM_ITEMS = ToolingConstants.MINECRAFT_ASSETS.resolve("textures/trims/items"); - private static final Path OVERLAY_DEFINITIONS_PATH = Path.of(ToolingConstants.GENERATOR_JSON + "/overlay_definitions.json"); + private final Path itemTextures; + private final Path trimItems; + private final List specialOverlayDefinitions; + private final List armorOverlayPatterns; + + /** + * Uses the definitions bundled with the tooling. + * + * @throws IOException if the bundled definitions are missing or malformed + */ + public OverlayGenerator(ToolingPaths paths) throws IOException { + this(paths, readBundledDefinitions()); + } - private List specialOverlayDefinitions = List.of(); - private List armorOverlayPatterns = List.of("leather_", "wolf_"); + /** + * Uses the caller's own definitions file instead of the bundled one. + * + * @throws IOException if the file cannot be read or is not a valid definitions document + */ + public OverlayGenerator(ToolingPaths paths, Path definitionsFile) throws IOException { + this(paths, readDefinitions(definitionsFile)); + } - public OverlayGenerator() { + private OverlayGenerator(ToolingPaths paths, OverlayDefinitions definitions) { super( - ATLAS_WIDTH, - ToolingConstants.GENERATOR_SPRITESHEETS + "/overlays.png", - ToolingConstants.GENERATOR_JSON + "/overlay_coordinates.json" + new AtlasLayout(AtlasLayout.DEFAULT_TILE_SIZE, ATLAS_WIDTH, AtlasLayout.UNLIMITED, 0), + paths.spritesheets().resolve("overlays.png"), + paths.json().resolve("overlay_coordinates.json") ); + this.itemTextures = paths.minecraftAssets().resolve("textures/item"); + this.trimItems = paths.minecraftAssets().resolve("textures/trims/items"); + this.specialOverlayDefinitions = definitions.specialOverlays(); + this.armorOverlayPatterns = definitions.armorOverlayPatterns(); } public static void main(String[] args) { @@ -48,120 +83,189 @@ public static void main(String[] args) { System.out.println("Overlay Generator"); System.out.println("=".repeat(50)); - if (!Files.exists(ToolingConstants.MINECRAFT_ASSETS)) { - throw new IOException("Minecraft assets not found at: " + ToolingConstants.MINECRAFT_ASSETS + - "\nRun 'MinecraftAssetTool --download' first."); + CliArgs.checkKnown(args, Set.of(), Set.of( + ToolingPaths.WORK_DIR_FLAG, ToolingPaths.OUTPUT_DIR_FLAG, DEFINITIONS_FLAG + )); + ToolingPaths paths = ToolingPaths.fromArgs(args); + Optional definitionsFile = CliArgs.pathValue(args, DEFINITIONS_FLAG); + + if (!Files.exists(paths.minecraftAssets())) { + throw new IOException("Minecraft assets not found at: " + paths.minecraftAssets() + + "\nRun 'MinecraftAssetTool --download' first."); } - new OverlayGenerator().generate(); - new OverlayColorConfigGenerator().generate(); + OverlayGenerator generator = definitionsFile.isPresent() + ? new OverlayGenerator(paths, definitionsFile.get()) + : new OverlayGenerator(paths); + generator.generate(); + new OverlayColorConfigGenerator(paths).generate(); System.out.println("\n" + "=".repeat(50)); System.out.println("Overlay generation complete!"); System.out.println("=".repeat(50)); - } catch (IOException e) { + } catch (IOException | UncheckedIOException | IllegalArgumentException e) { System.err.println("Failed to generate overlays: " + e.getMessage()); e.printStackTrace(); System.exit(1); } } + private static OverlayDefinitions readBundledDefinitions() throws IOException { + try (InputStream stream = OverlayGenerator.class.getResourceAsStream(BUNDLED_DEFINITIONS)) { + if (stream == null) { + throw new IOException("Bundled overlay definitions " + BUNDLED_DEFINITIONS + " are missing from the tooling classpath"); + } + + return parseDefinitions(new InputStreamReader(stream, StandardCharsets.UTF_8), "bundled " + BUNDLED_DEFINITIONS); + } + } + + /** + * Parses a definitions file and discards the result, so an entry point can reject a bad file before it + * spends time on downloads or renders. + * + * @throws IOException if the file cannot be read or is not a valid definitions document + */ + public static void checkDefinitions(Path file) throws IOException { + readDefinitions(file); + } + + private static OverlayDefinitions readDefinitions(Path file) throws IOException { + if (!Files.isRegularFile(file)) { + throw new IOException("Overlay definitions file not found: " + file); + } + + try (Reader reader = Files.newBufferedReader(file)) { + return parseDefinitions(reader, file.toString()); + } + } + + private static OverlayDefinitions parseDefinitions(Reader reader, String source) throws IOException { + JsonObject config; + try { + config = JsonParser.parseReader(reader).getAsJsonObject(); + } catch (JsonParseException | IllegalStateException e) { + throw new IOException("Overlay definitions " + source + " are not a JSON object: " + e.getMessage(), e); + } + + List specials = new ArrayList<>(); + if (config.has("specialOverlays")) { + for (JsonElement element : asArray(config, "specialOverlays", source)) { + if (!element.isJsonObject()) { + throw new IOException("Overlay definitions " + source + ": every specialOverlays entry must be an object"); + } + + JsonObject obj = element.getAsJsonObject(); + specials.add(new OverlayDefinition( + requiredString(obj, "fileName", source), + requiredString(obj, "colorOptions", source), + requiredString(obj, "type", source), + optionalString(obj, "colorMode", source) + )); + } + } + + List patterns = new ArrayList<>(); + if (config.has("armorOverlayPatterns")) { + for (JsonElement element : asArray(config, "armorOverlayPatterns", source)) { + if (!isString(element)) { + throw new IOException("Overlay definitions " + source + ": every armorOverlayPatterns entry must be a string"); + } + patterns.add(element.getAsString()); + } + } + + System.out.println(" Loaded " + specials.size() + " special overlay definitions and " + + patterns.size() + " armor overlay patterns from " + source); + return new OverlayDefinitions(List.copyOf(specials), List.copyOf(patterns)); + } + + private static JsonArray asArray(JsonObject config, String key, String source) throws IOException { + if (!config.get(key).isJsonArray()) { + throw new IOException("Overlay definitions " + source + ": " + key + " must be an array"); + } + + return config.getAsJsonArray(key); + } + + private static String requiredString(JsonObject obj, String key, String source) throws IOException { + JsonElement value = obj.get(key); + if (value == null || value.isJsonNull()) { + throw new IOException("Overlay definitions " + source + ": a specialOverlays entry is missing '" + key + "'"); + } + + if (!isString(value)) { + throw new IOException("Overlay definitions " + source + ": '" + key + "' must be a string"); + } + + return value.getAsString(); + } + + private static String optionalString(JsonObject obj, String key, String source) throws IOException { + JsonElement value = obj.get(key); + if (value == null) { + return null; + } + + if (!isString(value)) { + throw new IOException("Overlay definitions " + source + ": '" + key + "' must be a string when present"); + } + + return value.getAsString(); + } + + private static boolean isString(JsonElement value) { + return value.isJsonPrimitive() && value.getAsJsonPrimitive().isString(); + } + @Override protected List loadTextures() { List overlays = new ArrayList<>(); - loadOverlayDefinitions(); - - System.out.println("\nDiscovering dyeable armor overlays from: " + ITEM_TEXTURES); + System.out.println("\nDiscovering dyeable armor overlays from: " + itemTextures); List armorOverlays = discoverArmorOverlays(); System.out.println(" Found " + armorOverlays.size() + " armor overlays"); - loadOverlays(overlays, ITEM_TEXTURES, armorOverlays); + loadOverlays(overlays, itemTextures, armorOverlays); - System.out.println("\nDiscovering special overlays from config"); + System.out.println("\nDiscovering special overlays from definitions"); List specialOverlays = discoverSpecialOverlays(); System.out.println(" Found " + specialOverlays.size() + " special overlays"); - loadOverlays(overlays, ITEM_TEXTURES, specialOverlays); + loadOverlays(overlays, itemTextures, specialOverlays); - System.out.println("\nDiscovering trim overlays from: " + TRIM_ITEMS); + System.out.println("\nDiscovering trim overlays from: " + trimItems); List trimOverlays = discoverTrimOverlays(); System.out.println(" Found " + trimOverlays.size() + " trim types"); - loadOverlays(overlays, TRIM_ITEMS, trimOverlays); + loadOverlays(overlays, trimItems, trimOverlays); System.out.println("\nTotal overlays loaded: " + overlays.size()); return overlays; } /** - * Loads overlay definitions from overlay_definitions.json config file. - */ - private void loadOverlayDefinitions() { - if (!Files.exists(OVERLAY_DEFINITIONS_PATH)) { - System.err.println(" WARNING: overlay_definitions.json not found, using defaults"); - return; - } - - try (FileReader reader = new FileReader(OVERLAY_DEFINITIONS_PATH.toFile())) { - Gson gson = new Gson(); - JsonObject config = gson.fromJson(reader, JsonObject.class); - - // Load special overlays - if (config.has("specialOverlays")) { - List specials = new ArrayList<>(); - JsonArray specialsArray = config.getAsJsonArray("specialOverlays"); - for (JsonElement element : specialsArray) { - JsonObject obj = element.getAsJsonObject(); - specials.add(new OverlayDefinition( - obj.get("fileName").getAsString(), - obj.get("colorOptions").getAsString(), - obj.get("type").getAsString(), - obj.has("colorMode") ? obj.get("colorMode").getAsString() : null - )); - } - specialOverlayDefinitions = specials; - System.out.println(" Loaded " + specials.size() + " special overlay definitions from config"); - } - - // Load armor overlay patterns - if (config.has("armorOverlayPatterns")) { - List patterns = new ArrayList<>(); - JsonArray patternsArray = config.getAsJsonArray("armorOverlayPatterns"); - for (JsonElement element : patternsArray) { - patterns.add(element.getAsString()); - } - armorOverlayPatterns = patterns; - System.out.println(" Loaded " + patterns.size() + " armor overlay patterns from config"); - } - } catch (IOException e) { - System.err.println(" ERROR: Failed to load overlay_definitions.json: " + e.getMessage()); - } - } - - /** - * Filters special overlay definitions to only include files that exist in assets. + * Keeps the special overlay definitions whose texture exists in the assets. * These have unique color handling so mappings are kept explicit, but we verify existence. */ private List discoverSpecialOverlays() { - if (!Files.exists(ITEM_TEXTURES)) { + if (!Files.exists(itemTextures)) { return List.of(); } return specialOverlayDefinitions.stream() - .filter(def -> ITEM_TEXTURES.resolve(def.fileName + ".png").toFile().exists()) + .filter(def -> Files.exists(itemTextures.resolve(def.fileName + ".png"))) .collect(Collectors.toList()); } /** - * Discovers dyeable armor overlays from the textures directory. - * Uses patterns from overlay_definitions.json (e.g., leather_*, wolf_*) + * Discovers dyeable armor overlays from the textures directory using the configured name prefixes. */ private List discoverArmorOverlays() { List definitions = new ArrayList<>(); - if (!Files.exists(ITEM_TEXTURES)) { + if (!Files.exists(itemTextures)) { return definitions; } - File[] files = ITEM_TEXTURES.toFile().listFiles((dir, name) -> { + File[] files = itemTextures.toFile().listFiles((dir, name) -> { if (!name.endsWith("_overlay.png")) { return false; } @@ -184,34 +288,25 @@ private List discoverArmorOverlays() { } /** - * Discovers armor trim overlay type from the trims/items directory. - * Each unique armor type (helmet, chestplate, etc.) found becomes a trim definition. + * Discovers armor trim overlays from the trims/items directory. The game ships one greyscale overlay per + * armour piece there, named {@code _trim.png}, and recolours it per material at runtime; each such + * file becomes a MAPPED definition named after the file. */ private List discoverTrimOverlays() { - List definitions = new ArrayList<>(); - - if (!Files.exists(TRIM_ITEMS)) { - return definitions; + if (!Files.exists(trimItems)) { + return List.of(); } - File[] files = TRIM_ITEMS.toFile().listFiles((dir, name) -> name.endsWith(".png")); + File[] files = trimItems.toFile().listFiles((dir, name) -> name.endsWith("_trim.png")); if (files == null) { - return definitions; + return List.of(); } - // Extract unique armor types from filenames like "coast_helmet.png" -> "helmet" - // Pattern: {trim_pattern}_{armor_type}.png return Arrays.stream(files) .map(f -> f.getName().replace(".png", "")) - .map(name -> { - int lastUnderscore = name.lastIndexOf('_'); - return lastUnderscore > 0 ? name.substring(lastUnderscore + 1) : null; - }) - .filter(Objects::nonNull) - .distinct() .sorted() - .map(type -> new OverlayDefinition(type + "_trim", "armor_trim", "MAPPED", null)) + .map(name -> new OverlayDefinition(name, "armor_trim", "MAPPED", null)) .collect(Collectors.toList()); } @@ -235,7 +330,7 @@ private void loadOverlays(List overlays, Path sourceDir, List overlays, Path sourceDir, List " + IMAGE_SIZE + "x" + IMAGE_SIZE + ")"); + System.out.println(" Loaded: " + def.fileName + " (" + image.getWidth() + "x" + image.getHeight() + + " -> " + layout.tileSize() + "x" + layout.tileSize() + ")"); } catch (IOException e) { System.err.println(" Error loading " + def.fileName + ": " + e.getMessage()); } @@ -259,7 +355,7 @@ protected JsonObject entryToJson(AtlasEntry entry) { obj.addProperty("name", entry.name); obj.addProperty("x", entry.x); obj.addProperty("y", entry.y); - obj.addProperty("size", IMAGE_SIZE); + obj.addProperty("size", layout.tileSize()); obj.addProperty("type", entry.getMeta("type")); obj.addProperty("colorOptions", entry.getMeta("colorOptions")); @@ -272,4 +368,7 @@ protected JsonObject entryToJson(AtlasEntry entry) { private record OverlayDefinition(String fileName, String colorOptions, String type, String colorMode) { } + + private record OverlayDefinitions(List specialOverlays, List armorOverlayPatterns) { + } } diff --git a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGenerator.java b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGenerator.java index c2ebb76..91145c4 100644 --- a/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGenerator.java +++ b/tooling/src/main/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGenerator.java @@ -1,8 +1,8 @@ package net.aerh.imagegenerator.tooling.spritesheet; import com.google.gson.JsonObject; -import net.aerh.imagegenerator.tooling.ToolingConstants; -import net.aerh.imagegenerator.tooling.ToolingArguments; +import net.aerh.imagegenerator.tooling.CliArgs; +import net.aerh.imagegenerator.tooling.ToolingPaths; import net.aerh.imagegenerator.tooling.minecraft.TrimPaletteExtractor; import net.hypixel.nerdbot.marmalade.image.ImageUtil; @@ -10,6 +10,8 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; @@ -17,51 +19,51 @@ import java.util.Set; /** - * Generates a texture atlas from individual PNG textures. + * Generates the item texture atlas from a directory of individual PNG renders. */ public class SpritesheetGenerator extends AtlasGenerator { - private static final int ATLAS_WIDTH = IMAGE_SIZE * 16; + public static final String INPUT_FLAG = "--input"; + private static final String[] ARMOR_TYPES = {"helmet", "chestplate", "leggings", "boots"}; - private final String inputPath; + private final Path inputDir; + private final Path minecraftAssets; private Set trimMaterials; - public SpritesheetGenerator(String inputPath) { - this(inputPath, new AtlasLayout(IMAGE_SIZE, ATLAS_WIDTH, Integer.MAX_VALUE, 0)); + /** + * Packs the renders found in {@link ToolingPaths#renderedItems()}. + */ + public SpritesheetGenerator(ToolingPaths paths, AtlasLayout layout) { + this(paths.renderedItems(), paths, layout); } - public SpritesheetGenerator(String inputPath, AtlasLayout layout) { + public SpritesheetGenerator(Path inputDir, ToolingPaths paths, AtlasLayout layout) { super( layout, - ToolingConstants.GENERATOR_SPRITESHEETS + "/minecraft_texture_atlas.png", - ToolingConstants.GENERATOR_JSON + "/atlas_coordinates.json" + paths.spritesheets().resolve("minecraft_texture_atlas.png"), + paths.json().resolve("atlas_coordinates.json") ); - this.inputPath = inputPath; + this.inputDir = inputDir; + this.minecraftAssets = paths.minecraftAssets(); } public static void main(String[] args) { - ToolingArguments.configureDirectories(args); - String inputPath = ToolingConstants.RENDERED_ITEMS.toString(); - int tileSize = IMAGE_SIZE, width = ATLAS_WIDTH, height = Integer.MAX_VALUE, padding = 0; - - for (String arg : args) { - if (arg.startsWith("--input=")) { - inputPath = arg.substring("--input=".length()); - } else if (arg.startsWith("--tile-size=")) { - tileSize = Integer.parseInt(arg.substring("--tile-size=".length())); - } else if (arg.startsWith("--atlas-width=")) { - width = Integer.parseInt(arg.substring("--atlas-width=".length())); - } else if (arg.startsWith("--atlas-height=")) { - height = Integer.parseInt(arg.substring("--atlas-height=".length())); - } else if (arg.startsWith("--padding=")) { - padding = Integer.parseInt(arg.substring("--padding=".length())); - } + try { + CliArgs.checkKnown(args, Set.of(), Set.of( + INPUT_FLAG, AtlasLayout.TILE_SIZE_FLAG, AtlasLayout.ATLAS_WIDTH_FLAG, AtlasLayout.ATLAS_HEIGHT_FLAG, + AtlasLayout.PADDING_FLAG, ToolingPaths.WORK_DIR_FLAG, ToolingPaths.OUTPUT_DIR_FLAG + )); + ToolingPaths paths = ToolingPaths.fromArgs(args); + AtlasLayout layout = AtlasLayout.fromArgs(args, AtlasLayout.DEFAULT); + Path inputDir = CliArgs.pathValue(args, INPUT_FLAG).orElse(paths.renderedItems()); + + new SpritesheetGenerator(inputDir, paths, layout).generate(); + System.out.println("Texture atlas generation complete!"); + } catch (IllegalArgumentException | UncheckedIOException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); } - - SpritesheetGenerator generator = new SpritesheetGenerator(inputPath, new AtlasLayout(tileSize, width, height, padding)); - generator.generate(); - System.out.println("Texture atlas generation complete!"); } private boolean isArmorTrimVariant(String name) { @@ -79,13 +81,17 @@ private boolean isArmorTrimVariant(String name) { ); } + /** + * Reads the trim material names so renders of trimmed armour can be left out of the atlas. Assets without + * any palettes give an empty set; a palette layout that is present but broken ends the run, because packing + * every trim variant by mistake is exactly what the filter exists to prevent. + */ private void loadTrimMaterials() { try { - trimMaterials = TrimPaletteExtractor.extractAllPalettes().keySet(); + trimMaterials = TrimPaletteExtractor.load(minecraftAssets).materials().keySet(); System.out.println("Loaded " + trimMaterials.size() + " trim materials from assets."); - } catch (Exception e) { - System.err.println("Failed to load trim materials: " + e.getMessage()); - trimMaterials = Set.of(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load trim materials: " + e.getMessage(), e); } } @@ -94,7 +100,7 @@ protected List loadTextures() { loadTrimMaterials(); List textures = new ArrayList<>(); - File folder = new File(inputPath); + File folder = inputDir.toFile(); File[] files = folder.listFiles(f -> f.isFile() && f.getName().toLowerCase().endsWith(".png")); if (files == null) { diff --git a/generator/src/main/resources/minecraft/assets/json/overlay_definitions.json b/tooling/src/main/resources/overlay_definitions.json similarity index 100% rename from generator/src/main/resources/minecraft/assets/json/overlay_definitions.json rename to tooling/src/main/resources/overlay_definitions.json diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/CliArgsTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/CliArgsTest.java new file mode 100644 index 0000000..e6ed2b0 --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/CliArgsTest.java @@ -0,0 +1,110 @@ +package net.aerh.imagegenerator.tooling; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CliArgsTest { + + @Test + void checkKnownAcceptsKnownFlagsAndValuedFlags() { + assertDoesNotThrow(() -> CliArgs.checkKnown(new String[]{"--clean", "--size=1"}, Set.of("--clean"), Set.of("--size"))); + } + + @Test + void checkKnownRejectsAnUnknownOption() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> CliArgs.checkKnown(new String[]{"--clean", "--sizee=1"}, Set.of("--clean"), Set.of("--size"))); + assertEquals("Unknown option: --sizee=1", error.getMessage()); + } + + @Test + void checkKnownRejectsABareWord() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> CliArgs.checkKnown(new String[]{"help"}, Set.of("--help"), Set.of())); + assertEquals("Unknown option: help", error.getMessage()); + } + + @Test + void checkKnownRejectsAValuedFlagWithoutItsValue() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> CliArgs.checkKnown(new String[]{"--size"}, Set.of(), Set.of("--size"))); + assertEquals("--size needs a value, written as --size=", error.getMessage()); + } + + @Test + void valueIsEmptyWhenTheFlagIsAbsent() { + assertEquals(Optional.empty(), CliArgs.value(new String[]{"--other=1"}, "--padding")); + } + + @Test + void valueIsTheTextAfterTheFirstEqualsSign() { + assertEquals(Optional.of("a=b"), CliArgs.value(new String[]{"--input=a=b"}, "--input")); + } + + @Test + void lastOccurrenceOfAFlagWins() { + assertEquals(Optional.of("2"), CliArgs.value(new String[]{"--padding=1", "--padding=2"}, "--padding")); + } + + @Test + void valueDoesNotMatchAFlagThatMerelyStartsWithTheName() { + assertEquals(Optional.empty(), CliArgs.value(new String[]{"--atlas-width-max=1"}, "--atlas-width")); + } + + @Test + void intValueParsesTheFlag() { + assertEquals(7, CliArgs.intValue(new String[]{"--padding=7"}, "--padding", 0)); + } + + @Test + void intValueFallsBackToTheDefaultWhenAbsent() { + assertEquals(4, CliArgs.intValue(new String[]{}, "--padding", 4)); + } + + @Test + void intValueNamesTheFlagWhenTheValueIsNotAnInteger() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> CliArgs.intValue(new String[]{"--size=big"}, "--size", 0)); + assertEquals("--size expects an integer, got 'big'", error.getMessage()); + } + + @Test + void intValueRejectsAnEmptyValue() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> CliArgs.intValue(new String[]{"--size="}, "--size", 0)); + assertEquals("--size expects an integer, got ''", error.getMessage()); + } + + @Test + void pathValueReturnsThePath() { + assertEquals(Optional.of(Path.of("build/26.3")), CliArgs.pathValue(new String[]{"--work-dir=build/26.3"}, "--work-dir")); + } + + @Test + void pathValueIsEmptyWhenAbsent() { + assertEquals(Optional.empty(), CliArgs.pathValue(new String[]{}, "--work-dir")); + } + + @Test + void pathValueRejectsABlankValue() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> CliArgs.pathValue(new String[]{"--work-dir= "}, "--work-dir")); + assertEquals("--work-dir must not be empty", error.getMessage()); + } + + @Test + void hasFlagMatchesTheWholeArgumentOnly() { + assertTrue(CliArgs.hasFlag(new String[]{"--clean"}, "--clean")); + assertFalse(CliArgs.hasFlag(new String[]{"--cleanup"}, "--clean")); + assertFalse(CliArgs.hasFlag(new String[]{"--clean=true"}, "--clean")); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/PathGuardTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/PathGuardTest.java new file mode 100644 index 0000000..9a4b9ce --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/PathGuardTest.java @@ -0,0 +1,69 @@ +package net.aerh.imagegenerator.tooling; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Path; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PathGuardTest { + + @TempDir + Path root; + + @Test + void aDirectoryContainsItself() { + assertTrue(PathGuard.contains(root.resolve("a"), root.resolve("a"))); + } + + @Test + void aDirectoryContainsItsDescendants() { + assertTrue(PathGuard.contains(root.resolve("a"), root.resolve("a/b/c"))); + } + + @Test + void aDirectoryDoesNotContainASibling() { + assertFalse(PathGuard.contains(root.resolve("a"), root.resolve("ab"))); + } + + @Test + void aDirectoryDoesNotContainItsParent() { + assertFalse(PathGuard.contains(root.resolve("a/b"), root.resolve("a"))); + } + + @Test + void containmentNormalisesDotSegments() { + assertTrue(PathGuard.contains(root.resolve("a/../a"), root.resolve("a/./b"))); + } + + @Test + void deletingTheCurrentDirectoryIsRefused() { + Path current = root.resolve("project"); + Optional refusal = PathGuard.deletionRefusal(current, current); + assertEquals(Optional.of(current + " contains the current directory " + current), refusal); + } + + @Test + void deletingAnAncestorOfTheCurrentDirectoryIsRefused() { + Path current = root.resolve("project/sub"); + assertTrue(PathGuard.deletionRefusal(root.resolve("project"), current).isPresent()); + } + + @Test + void deletingADirectoryInsideTheCurrentDirectoryIsAllowed() { + Path current = root.resolve("project"); + assertEquals(Optional.empty(), PathGuard.deletionRefusal(current.resolve("build"), current)); + } + + @Test + void relativeTargetsAreResolvedAgainstTheCurrentDirectory() { + Path current = root.resolve("project"); + assertEquals(Optional.empty(), PathGuard.deletionRefusal(Path.of("build"), current)); + assertTrue(PathGuard.deletionRefusal(Path.of("."), current).isPresent()); + assertTrue(PathGuard.deletionRefusal(Path.of(".."), current).isPresent()); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/ToolingPathsTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/ToolingPathsTest.java new file mode 100644 index 0000000..54e2dd2 --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/ToolingPathsTest.java @@ -0,0 +1,169 @@ +package net.aerh.imagegenerator.tooling; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ToolingPathsTest { + + @TempDir + Path root; + + @Test + void defaultsUseTheRepositoryLayout() { + ToolingPaths paths = ToolingPaths.defaults(); + assertEquals(Path.of("tooling/build"), paths.workDir()); + assertEquals(Path.of("generator/src/main/resources/minecraft/assets"), paths.outputDir()); + } + + @Test + void derivedPathsHangOffTheWorkAndOutputDirectories() { + ToolingPaths paths = new ToolingPaths(Path.of("w"), Path.of("o")); + assertEquals(Path.of("w/minecraft-assets"), paths.minecraftAssets()); + assertEquals(Path.of("w/rendered-items"), paths.renderedItems()); + assertEquals(Path.of("w/minecraft-jars"), paths.jarCache()); + assertEquals(Path.of("w/minecraft-mappings"), paths.mappingsCache()); + assertEquals(Path.of("w/asset-renderer"), paths.rendererCache()); + assertEquals(Path.of("o/spritesheets"), paths.spritesheets()); + assertEquals(Path.of("o/json"), paths.json()); + } + + @Test + void cachedJarAndMappingsFilesAreNamedByVersion() { + ToolingPaths paths = new ToolingPaths(Path.of("w"), Path.of("o")); + assertEquals(Path.of("w/minecraft-jars/minecraft-1.21.4-client.jar"), paths.jarPath("1.21.4")); + assertEquals(Path.of("w/minecraft-mappings/minecraft-1.21.4-client.txt"), paths.mappingsPath("1.21.4")); + } + + @Test + void flagsOverrideBothDirectories() { + String[] args = {"--work-dir=build/w", "--output-dir=build/o"}; + ToolingPaths paths = ToolingPaths.fromArgs(args); + assertEquals(Path.of("build/w"), paths.workDir()); + assertEquals(Path.of("build/o"), paths.outputDir()); + } + + @Test + void systemPropertiesApplyWhenFlagsAreAbsent() { + System.setProperty(ToolingPaths.WORK_DIR_PROPERTY, "prop/w"); + System.setProperty(ToolingPaths.OUTPUT_DIR_PROPERTY, "prop/o"); + try { + ToolingPaths paths = ToolingPaths.fromArgs(new String[]{}); + assertEquals(Path.of("prop/w"), paths.workDir()); + assertEquals(Path.of("prop/o"), paths.outputDir()); + } finally { + System.clearProperty(ToolingPaths.WORK_DIR_PROPERTY); + System.clearProperty(ToolingPaths.OUTPUT_DIR_PROPERTY); + } + } + + @Test + void flagsBeatSystemProperties() { + System.setProperty(ToolingPaths.WORK_DIR_PROPERTY, "prop/w"); + try { + assertEquals(Path.of("flag/w"), ToolingPaths.fromArgs(new String[]{"--work-dir=flag/w"}).workDir()); + } finally { + System.clearProperty(ToolingPaths.WORK_DIR_PROPERTY); + } + } + + @Test + void aBlankSystemPropertyCountsAsUnset() { + System.setProperty(ToolingPaths.WORK_DIR_PROPERTY, " "); + try { + assertEquals(Path.of("tooling/build"), ToolingPaths.fromArgs(new String[]{}).workDir()); + } finally { + System.clearProperty(ToolingPaths.WORK_DIR_PROPERTY); + } + } + + @Test + void anEmptyDirectoryFlagIsRejected() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> ToolingPaths.fromArgs(new String[]{"--output-dir="})); + assertEquals("--output-dir must not be empty", error.getMessage()); + } + + @Test + void workDirectoryCountsAsDefaultOnlyWhenItIsTheRepositoryBuildDirectory() { + assertTrue(ToolingPaths.defaults().isDefaultWorkDir()); + assertTrue(ToolingPaths.fromArgs(new String[]{"--work-dir=tooling/build"}).isDefaultWorkDir()); + assertFalse(ToolingPaths.fromArgs(new String[]{"--work-dir=build/w"}).isDefaultWorkDir()); + } + + @Test + void dottedAndAbsoluteSpellingsOfTheBuildDirectoryAreStillDefault() { + assertTrue(ToolingPaths.fromArgs(new String[]{"--work-dir=./tooling/build"}).isDefaultWorkDir()); + Path absolute = Path.of("tooling/build").toAbsolutePath(); + assertTrue(ToolingPaths.fromArgs(new String[]{"--work-dir=" + absolute}).isDefaultWorkDir()); + } + + @Test + void cleanupIsAllowedForTheDefaultLayout() { + assertEquals(Optional.empty(), ToolingPaths.defaults().cleanupRefusal(root.resolve("project"))); + } + + @Test + void cleanupIsRefusedWhenTheWorkDirectoryIsTheCurrentDirectory() { + ToolingPaths paths = ToolingPaths.fromArgs(new String[]{"--work-dir=."}); + Optional refusal = paths.cleanupRefusal(root.resolve("project")); + assertTrue(refusal.isPresent()); + assertTrue(refusal.get().contains("contains the current directory"), refusal.get()); + } + + @Test + void cleanupIsRefusedWhenTheWorkDirectoryIsAnAncestorOfTheCurrentDirectory() { + ToolingPaths paths = ToolingPaths.fromArgs(new String[]{"--work-dir=" + root}); + assertTrue(paths.cleanupRefusal(root.resolve("project")).isPresent()); + } + + @Test + void cleanupIsRefusedWhenTheOutputDirectoryIsInsideTheWorkDirectory() { + ToolingPaths paths = ToolingPaths.fromArgs(new String[]{"--work-dir=build/26.3", "--output-dir=build/26.3/generated"}); + Optional refusal = paths.cleanupRefusal(root.resolve("project")); + assertTrue(refusal.isPresent()); + assertTrue(refusal.get().contains("output dir"), refusal.get()); + } + + @Test + void cachedClientVersionIsReadFromTheOnlyCachedJar() throws IOException { + ToolingPaths paths = new ToolingPaths(root.resolve("w"), root.resolve("o")); + Files.createDirectories(paths.jarCache()); + Files.writeString(paths.jarCache().resolve("minecraft-26.3-client.jar"), "x"); + Files.writeString(paths.jarCache().resolve("download-123.tmp"), "x"); + + assertEquals(Optional.of("26.3"), paths.cachedClientVersion()); + } + + @Test + void cachedClientVersionIsEmptyWhenNothingIsCached() throws IOException { + ToolingPaths paths = new ToolingPaths(root.resolve("w"), root.resolve("o")); + assertEquals(Optional.empty(), paths.cachedClientVersion()); + + Files.createDirectories(paths.jarCache()); + assertEquals(Optional.empty(), paths.cachedClientVersion()); + } + + @Test + void severalCachedJarsNeedAnExplicitVersion() throws IOException { + ToolingPaths paths = new ToolingPaths(root.resolve("w"), root.resolve("o")); + Files.createDirectories(paths.jarCache()); + Files.writeString(paths.jarCache().resolve("minecraft-26.2-client.jar"), "x"); + Files.writeString(paths.jarCache().resolve("minecraft-26.3-client.jar"), "x"); + + IOException error = assertThrows(IOException.class, paths::cachedClientVersion); + + assertTrue(error.getMessage().contains("26.2"), error.getMessage()); + assertTrue(error.getMessage().contains("26.3"), error.getMessage()); + assertTrue(error.getMessage().contains("--version"), error.getMessage()); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractorTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractorTest.java new file mode 100644 index 0000000..802818f --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/ColorExtractorTest.java @@ -0,0 +1,72 @@ +package net.aerh.imagegenerator.tooling.minecraft; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ColorExtractorTest { + + private static final String DYE_COLOR = "net.minecraft.world.item.DyeColor"; + + @TempDir + Path directory; + + @Test + void withoutMappingsClassNamesAreUsedAsTheyAre() throws IOException { + Path jar = writeJar("net/minecraft/world/item/DyeColor.class"); + + Map resolved = new ColorExtractor(jar).resolveClassNames(); + + assertEquals(DYE_COLOR, resolved.get(DYE_COLOR)); + assertEquals("net.minecraft.world.effect.MobEffects", resolved.get("net.minecraft.world.effect.MobEffects")); + } + + @Test + void withMappingsClassNamesAreTranslatedToTheirObfuscatedForm() throws IOException { + Path jar = writeJar("dcl.class"); + Path mappings = directory.resolve("mappings.txt"); + Files.writeString(mappings, """ + net.minecraft.world.item.DyeColor -> dcl: + int id -> a + net.minecraft.world.effect.MobEffects -> bzq: + """); + + Map resolved = new ColorExtractor(jar, mappings).resolveClassNames(); + + assertEquals("dcl", resolved.get(DYE_COLOR)); + assertEquals("bzq", resolved.get("net.minecraft.world.effect.MobEffects")); + } + + @Test + void withoutMappingsAJarThatDoesNotHaveTheNamedClassesIsRejected() throws IOException { + Path jar = writeJar("dcl.class"); + + IOException error = assertThrows(IOException.class, () -> new ColorExtractor(jar).resolveClassNames()); + + assertTrue(error.getMessage().contains("obfuscated"), error.getMessage()); + assertTrue(error.getMessage().contains(jar.getFileName().toString()), error.getMessage()); + } + + private Path writeJar(String... entryNames) throws IOException { + Path jar = directory.resolve("client.jar"); + try (OutputStream out = Files.newOutputStream(jar); ZipOutputStream zip = new ZipOutputStream(out)) { + for (String entryName : entryNames) { + zip.putNextEntry(new ZipEntry(entryName)); + zip.write(new byte[]{(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}); + zip.closeEntry(); + } + } + return jar; + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/ItemIconRendererTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/ItemIconRendererTest.java new file mode 100644 index 0000000..27dc1cf --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/ItemIconRendererTest.java @@ -0,0 +1,141 @@ +package net.aerh.imagegenerator.tooling.minecraft; + +import lib.minecraft.renderer.call.result.AtlasResult; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class ItemIconRendererTest { + + private static final int RED = 0xffff0000; + private static final int GREEN = 0xff00ff00; + + @TempDir + Path output; + + @Test + void itemIdsComeFromTheItemDefinitionsInTheJar() throws IOException { + Path jar = output.resolve("client.jar"); + try (OutputStream out = Files.newOutputStream(jar); ZipOutputStream zip = new ZipOutputStream(out)) { + for (String entry : List.of( + "assets/minecraft/items/stone.json", + "assets/minecraft/items/chest.json", + "assets/minecraft/items/acacia_boat.json", + "assets/minecraft/items/README.txt", + "assets/minecraft/items/nested/not_an_item.json", + "assets/minecraft/models/item/stone.json", + "assets/minecraft/models/item/iron_helmet_gold_trim.json", + "assets/minecraft/textures/item/stone.png" + )) { + zip.putNextEntry(new ZipEntry(entry)); + zip.closeEntry(); + } + } + + assertEquals(List.of("minecraft:acacia_boat", "minecraft:chest", "minecraft:stone"), ItemIconRenderer.itemIdsFromJar(jar)); + } + + @Test + void aJarWithoutItemDefinitionsIsAnError() throws IOException { + Path jar = output.resolve("old-client.jar"); + try (OutputStream out = Files.newOutputStream(jar); ZipOutputStream zip = new ZipOutputStream(out)) { + zip.putNextEntry(new ZipEntry("assets/minecraft/models/item/stone.json")); + zip.closeEntry(); + } + + IOException error = assertThrows(IOException.class, () -> ItemIconRenderer.itemIdsFromJar(jar)); + assertTrue(error.getMessage().contains("assets/minecraft/items/"), error.getMessage()); + } + + @Test + void vanillaIdsDropTheNamespace() { + assertEquals("acacia_boat.png", ItemIconRenderer.iconFileName("minecraft:acacia_boat")); + } + + @Test + void otherNamespacesStayInTheFileName() { + assertEquals("pack_fancy_sword.png", ItemIconRenderer.iconFileName("pack:fancy_sword")); + assertEquals("bare.png", ItemIconRenderer.iconFileName("bare")); + } + + @Test + void batchesSplitIdsInOrderWithAShortTail() { + List> batches = ItemIconRenderer.batches(List.of("a", "b", "c", "d", "e"), 2); + + assertEquals(List.of(List.of("a", "b"), List.of("c", "d"), List.of("e")), batches); + assertEquals(List.of(), ItemIconRenderer.batches(List.of(), 2)); + } + + @Test + void batchSizeMustBePositive() { + assertThrows(IllegalArgumentException.class, () -> ItemIconRenderer.batches(List.of("a"), 0)); + } + + @Test + void tilesAreSlicedIntoOneFilePerId() throws IOException { + BufferedImage sheet = new BufferedImage(4, 2, BufferedImage.TYPE_INT_ARGB); + fill(sheet, 0, 0, 2, 2, RED); + fill(sheet, 2, 0, 2, 2, GREEN); + List tiles = List.of( + tile("minecraft:red_dye", AtlasResult.Tile.Kind.ITEM, AtlasResult.Tile.Source.BLOCK_MODEL, 0, 0), + tile("minecraft:green_block", AtlasResult.Tile.Kind.BLOCK, AtlasResult.Tile.Source.BLOCK_MODEL, 1, 0) + ); + + int written = ItemIconRenderer.writeTiles(sheet, tiles, output); + + assertEquals(2, written); + BufferedImage red = ImageIO.read(output.resolve("red_dye.png").toFile()); + BufferedImage green = ImageIO.read(output.resolve("green_block.png").toFile()); + assertEquals(2, red.getWidth()); + assertEquals(RED, red.getRGB(1, 1)); + assertEquals(GREEN, green.getRGB(0, 0)); + } + + @Test + void emptyTilesAreNotWritten() throws IOException { + BufferedImage sheet = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB); + List tiles = List.of( + tile("minecraft:air", AtlasResult.Tile.Kind.ITEM, AtlasResult.Tile.Source.EMPTY, 0, 0) + ); + + assertEquals(0, ItemIconRenderer.writeTiles(sheet, tiles, output)); + assertFalse(Files.exists(output.resolve("air.png"))); + } + + @Test + void tilesOutsideTheSheetAreAnError() { + BufferedImage sheet = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB); + List tiles = List.of( + tile("minecraft:stone", AtlasResult.Tile.Kind.BLOCK, AtlasResult.Tile.Source.BLOCK_MODEL, 1, 0) + ); + + IOException error = assertThrows(IOException.class, () -> ItemIconRenderer.writeTiles(sheet, tiles, output)); + assertTrue(error.getMessage().contains("minecraft:stone"), error.getMessage()); + } + + private static AtlasResult.Tile tile(String id, AtlasResult.Tile.Kind kind, AtlasResult.Tile.Source source, int col, int row) { + return new AtlasResult.Tile(id, kind, source, col, row, col * 2, row * 2, 2, 2, List.of()); + } + + private static void fill(BufferedImage image, int x0, int y0, int width, int height, int argb) { + for (int y = y0; y < y0 + height; y++) { + for (int x = x0; x < x0 + width; x++) { + image.setRGB(x, y, argb); + } + } + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetToolTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetToolTest.java new file mode 100644 index 0000000..1084ead --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/MinecraftAssetToolTest.java @@ -0,0 +1,170 @@ +package net.aerh.imagegenerator.tooling.minecraft; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MinecraftAssetToolTest { + + @TempDir + Path root; + + @Test + void renderSizeDoesNotChangeTheAtlasTileSize() { + MinecraftAssetTool tool = MinecraftAssetTool.parse(new String[]{"--size=64"}); + assertEquals(64, tool.renderSize()); + assertEquals(256, tool.layout().tileSize()); + } + + @Test + void tileSizeIsSetExplicitly() { + MinecraftAssetTool tool = MinecraftAssetTool.parse(new String[]{"--tile-size=64"}); + assertEquals(64, tool.layout().tileSize()); + assertEquals(256, tool.renderSize()); + } + + @Test + void layoutFlagsAreValidatedBeforeAnythingRuns() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> MinecraftAssetTool.parse(new String[]{"--padding=-1"})); + assertEquals("Padding must be non-negative, got -1", error.getMessage()); + } + + @Test + void renderSizeMustBePositive() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> MinecraftAssetTool.parse(new String[]{"--size=0"})); + assertEquals("--size must be positive, got 0", error.getMessage()); + } + + @Test + void blankVersionMeansLatest() { + assertNull(MinecraftAssetTool.parse(new String[]{"--version="}).minecraftVersion()); + assertEquals("1.21.4", MinecraftAssetTool.parse(new String[]{"--version=1.21.4"}).minecraftVersion()); + } + + @Test + void versionIsTrimmed() { + assertEquals("26.3", MinecraftAssetTool.parse(new String[]{"--version= 26.3 "}).minecraftVersion()); + } + + @Test + void unknownOptionsAreRejected() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> MinecraftAssetTool.parse(new String[]{"--spritesheet", "--atlas-hieght=2048"})); + assertEquals("Unknown option: --atlas-hieght=2048", error.getMessage()); + } + + @Test + void aValuedOptionWithoutAValueIsRejected() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> MinecraftAssetTool.parse(new String[]{"--size"})); + assertEquals("--size needs a value, written as --size=", error.getMessage()); + } + + @Test + void aValidOverlayDefinitionsFileIsAccepted() throws IOException { + Path definitions = root.resolve("defs.json"); + Files.writeString(definitions, "{\"specialOverlays\": [], \"armorOverlayPatterns\": [\"wolf_\"]}"); + MinecraftAssetTool tool = MinecraftAssetTool.parse(new String[]{"--overlay-definitions=" + definitions}); + assertEquals(definitions, tool.overlayDefinitions()); + assertNull(MinecraftAssetTool.parse(new String[]{}).overlayDefinitions()); + } + + @Test + void aMissingOverlayDefinitionsFileFailsAtParseTime() { + Path definitions = root.resolve("missing.json"); + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> MinecraftAssetTool.parse(new String[]{"--overlay-definitions=" + definitions})); + assertTrue(error.getMessage().contains("missing.json"), error.getMessage()); + } + + @Test + void aMalformedOverlayDefinitionsFileFailsAtParseTime() throws IOException { + Path definitions = root.resolve("broken.json"); + Files.writeString(definitions, "{ not json"); + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> MinecraftAssetTool.parse(new String[]{"--overlay-definitions=" + definitions})); + assertTrue(error.getMessage().contains("broken.json"), error.getMessage()); + } + + @Test + void anAbsolutePathToTheRepositoryWorkDirectoryStillCleansByDefault() { + Path absolute = Path.of("tooling/build").toAbsolutePath(); + assertTrue(MinecraftAssetTool.parse(new String[]{"--work-dir=" + absolute}).cleansWorkDir()); + } + + @Test + void noStepFlagsMeansTheWholePipeline() { + assertEquals("download render spritesheet overlays", MinecraftAssetTool.parse(new String[]{}).stepsSummary()); + assertEquals("spritesheet", MinecraftAssetTool.parse(new String[]{"--spritesheet"}).stepsSummary()); + } + + @Test + void cleanupIsOnByDefaultForTheRepositoryWorkDirectory() { + assertTrue(MinecraftAssetTool.parse(new String[]{}).cleansWorkDir()); + } + + @Test + void cleanupIsOffByDefaultForACustomWorkDirectory() { + assertFalse(MinecraftAssetTool.parse(new String[]{"--work-dir=build/w"}).cleansWorkDir()); + } + + @Test + void cleanFlagTurnsCleanupOnForACustomWorkDirectory() { + assertTrue(MinecraftAssetTool.parse(new String[]{"--work-dir=build/w", "--clean"}).cleansWorkDir()); + } + + @Test + void noCleanFlagTurnsCleanupOff() { + assertFalse(MinecraftAssetTool.parse(new String[]{"--no-clean"}).cleansWorkDir()); + } + + @Test + void cleanupDeletesTheWorkDirectory() throws IOException { + Path project = root.resolve("project"); + Path workDir = project.resolve("tooling/build"); + Files.createDirectories(workDir.resolve("rendered-items")); + Files.writeString(workDir.resolve("rendered-items/stone.png"), "x"); + + MinecraftAssetTool.parse(new String[]{}).cleanWorkDirectory(project); + + assertFalse(Files.exists(workDir)); + } + + @Test + void anExplicitlyRequestedCleanupThatWouldDeleteTheCurrentDirectoryFails() throws IOException { + Path project = root.resolve("project"); + Files.createDirectories(project); + Files.writeString(project.resolve("keep.txt"), "x"); + MinecraftAssetTool tool = MinecraftAssetTool.parse(new String[]{"--work-dir=.", "--clean"}); + + IllegalStateException error = assertThrows(IllegalStateException.class, () -> tool.cleanWorkDirectory(project)); + + assertTrue(error.getMessage().contains("contains the current directory"), error.getMessage()); + assertTrue(Files.exists(project.resolve("keep.txt"))); + } + + @Test + void anImplicitCleanupThatIsRefusedOnlyWarns() throws IOException { + Path project = root.resolve("project"); + Path output = project.resolve("tooling/build/out"); + Files.createDirectories(output); + Files.writeString(output.resolve("atlas.png"), "x"); + MinecraftAssetTool tool = MinecraftAssetTool.parse(new String[]{"--output-dir=tooling/build/out"}); + assertTrue(tool.cleansWorkDir(), "default work dir still cleans by default"); + + tool.cleanWorkDirectory(project); + + assertTrue(Files.exists(output.resolve("atlas.png")), "output inside the work dir must survive"); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractorTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractorTest.java new file mode 100644 index 0000000..c2260f8 --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/minecraft/TrimPaletteExtractorTest.java @@ -0,0 +1,119 @@ +package net.aerh.imagegenerator.tooling.minecraft; + +import net.aerh.imagegenerator.tooling.minecraft.TrimPaletteExtractor.TrimPalettes; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TrimPaletteExtractorTest { + + /** The greys of the 26.3 {@code textures/palettes/trim_base.png}. */ + private static final int[] GREYS = { + 0xfff1f1f1, 0xffe1e1e1, 0xffd0d0d0, 0xffbcbcbc, 0xffa5a5a5, 0xff898989, 0xff636363, 0xff000000 + }; + private static final int[] GOLD = { + 0xfffffd90, 0xffecd93f, 0xffdeb12d, 0xffb16712, 0xffa0450a, 0xff803503, 0xff712d00, 0xff572300 + }; + private static final int[] AMETHYST = { + 0xffe4c2ff, 0xffd49cff, 0xffc478ff, 0xffb45cf5, 0xff9b43d8, 0xff8233be, 0xff6b26a2, 0xff4f1a7a + }; + + @TempDir + Path assets; + + @Test + void currentLayoutProvidesTheKeyAndEveryMaterial() throws IOException { + writePalette(assets.resolve("textures/palettes/trim_base.png"), GREYS); + writePalette(assets.resolve("textures/palettes/trim/gold.png"), GOLD); + writePalette(assets.resolve("textures/palettes/trim/gold_darker.png"), AMETHYST); + + TrimPalettes palettes = TrimPaletteExtractor.load(assets); + + assertArrayEquals(GREYS, palettes.key()); + assertEquals(Set.of("gold", "gold_darker"), palettes.materials().keySet()); + assertArrayEquals(GOLD, palettes.materials().get("gold")); + } + + @Test + void legacyLayoutUsesTrimPaletteAsTheKeyAndNotAsAMaterial() throws IOException { + writePalette(assets.resolve("textures/trims/color_palettes/trim_palette.png"), GREYS); + writePalette(assets.resolve("textures/trims/color_palettes/amethyst.png"), AMETHYST); + + TrimPalettes palettes = TrimPaletteExtractor.load(assets); + + assertArrayEquals(GREYS, palettes.key()); + assertEquals(Set.of("amethyst"), palettes.materials().keySet()); + assertArrayEquals(AMETHYST, palettes.materials().get("amethyst")); + } + + @Test + void currentLayoutWinsWhenBothLayoutsExist() throws IOException { + writePalette(assets.resolve("textures/palettes/trim_base.png"), GREYS); + writePalette(assets.resolve("textures/palettes/trim/gold.png"), GOLD); + writePalette(assets.resolve("textures/trims/color_palettes/trim_palette.png"), GREYS); + writePalette(assets.resolve("textures/trims/color_palettes/amethyst.png"), AMETHYST); + + assertEquals(Set.of("gold"), TrimPaletteExtractor.load(assets).materials().keySet()); + } + + @Test + void noLayoutGivesEmptyPalettes() throws IOException { + TrimPalettes palettes = TrimPaletteExtractor.load(assets); + + assertTrue(palettes.isEmpty()); + assertEquals(0, palettes.key().length); + } + + @Test + void currentLayoutWithoutTheBasePaletteIsAnError() throws IOException { + writePalette(assets.resolve("textures/palettes/trim/gold.png"), GOLD); + + IOException error = assertThrows(IOException.class, () -> TrimPaletteExtractor.load(assets)); + assertTrue(error.getMessage().contains("trim_base.png"), error.getMessage()); + } + + @Test + void legacyLayoutWithoutTheKeyPaletteIsAnError() throws IOException { + writePalette(assets.resolve("textures/trims/color_palettes/amethyst.png"), AMETHYST); + + IOException error = assertThrows(IOException.class, () -> TrimPaletteExtractor.load(assets)); + assertTrue(error.getMessage().contains("trim_palette.png"), error.getMessage()); + } + + @Test + void filesThatAreNotPngsAreNotMaterials() throws IOException { + writePalette(assets.resolve("textures/palettes/trim_base.png"), GREYS); + writePalette(assets.resolve("textures/palettes/trim/gold.png"), GOLD); + Files.writeString(assets.resolve("textures/palettes/trim/gold.png.mcmeta"), "{}"); + + assertEquals(Set.of("gold"), TrimPaletteExtractor.load(assets).materials().keySet()); + } + + @Test + void extractPaletteReadsPixelsLeftToRight() throws IOException { + Path file = assets.resolve("gold.png"); + writePalette(file, GOLD); + + assertArrayEquals(GOLD, TrimPaletteExtractor.extractPalette(file.toFile())); + } + + private static void writePalette(Path file, int[] colors) throws IOException { + BufferedImage image = new BufferedImage(colors.length, 1, BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < colors.length; x++) { + image.setRGB(x, 0, colors[x]); + } + Files.createDirectories(file.getParent()); + ImageIO.write(image, "png", file.toFile()); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGeneratorPagingTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGeneratorPagingTest.java new file mode 100644 index 0000000..346132c --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasGeneratorPagingTest.java @@ -0,0 +1,284 @@ +package net.aerh.imagegenerator.tooling.spritesheet; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AtlasGeneratorPagingTest { + + private static final int RED = 0xffff0000; + private static final int GREEN = 0xff00ff00; + private static final int BLUE = 0xff0000ff; + + @TempDir + Path directory; + + @Test + void defaultLayoutPacksSixteenTilesPerRowWithoutGutters() throws IOException { + List entries = solidEntries(17, 256); + new TestAtlas(AtlasLayout.DEFAULT, entries).generate(); + + BufferedImage page = ImageIO.read(directory.resolve("atlas.png").toFile()); + assertEquals(4096, page.getWidth()); + assertEquals(512, page.getHeight()); + assertFalse(Files.exists(directory.resolve("atlas-1.png"))); + + JsonArray json = readCoordinates(); + for (int i = 0; i < entries.size(); i++) { + JsonObject coordinate = json.get(i).getAsJsonObject(); + assertEquals((i % 16) * 256, coordinate.get("x").getAsInt(), "x of entry " + i); + assertEquals((i / 16) * 256, coordinate.get("y").getAsInt(), "y of entry " + i); + assertEquals(256, coordinate.get("size").getAsInt()); + assertEquals(0, coordinate.get("page").getAsInt()); + assertEquals("atlas.png", coordinate.get("atlas").getAsString()); + } + } + + @Test + void pagesKeepPixelsAndTransparentGutters() throws IOException { + List entries = List.of(entry("red", RED, 2), entry("green", GREEN, 2), entry("blue", BLUE, 2)); + new TestAtlas(new AtlasLayout(2, 8, 4, 1), entries).generate(); + + BufferedImage first = ImageIO.read(directory.resolve("atlas.png").toFile()); + BufferedImage second = ImageIO.read(directory.resolve("atlas-1.png").toFile()); + assertEquals(8, first.getWidth()); + assertEquals(4, first.getHeight()); + assertTile(first, 1, 1, 2, RED); + assertTile(first, 5, 1, 2, GREEN); + assertTile(second, 1, 1, 2, BLUE); + assertTransparent(first, 0, 0, 8, 1); + assertTransparent(first, 0, 3, 8, 1); + assertTransparent(first, 3, 0, 2, 4); + + JsonArray json = readCoordinates(); + assertEquals(0, json.get(0).getAsJsonObject().get("page").getAsInt()); + assertEquals("atlas.png", json.get(0).getAsJsonObject().get("atlas").getAsString()); + assertEquals(1, json.get(2).getAsJsonObject().get("page").getAsInt()); + assertEquals("atlas-1.png", json.get(2).getAsJsonObject().get("atlas").getAsString()); + } + + @Test + void singlePageKeepsTheOriginalFilename() throws IOException { + new TestAtlas(new AtlasLayout(2, 4, 4, 0), List.of(entry("red", RED, 2))).generate(); + + assertTrue(Files.isRegularFile(directory.resolve("atlas.png"))); + assertFalse(Files.exists(directory.resolve("atlas-0.png"))); + assertFalse(Files.exists(directory.resolve("atlas-1.png"))); + + JsonObject coordinate = readCoordinates().get(0).getAsJsonObject(); + assertEquals("red", coordinate.get("name").getAsString()); + assertEquals(0, coordinate.get("x").getAsInt()); + assertEquals(0, coordinate.get("y").getAsInt()); + assertEquals(2, coordinate.get("size").getAsInt()); + } + + @Test + void exactlyCapacityEntriesFillOnePage() throws IOException { + new TestAtlas(new AtlasLayout(2, 4, 4, 0), solidEntries(4, 2)).generate(); + + assertTrue(Files.exists(directory.resolve("atlas.png"))); + assertFalse(Files.exists(directory.resolve("atlas-1.png"))); + for (int i = 0; i < 4; i++) { + assertEquals(0, readCoordinates().get(i).getAsJsonObject().get("page").getAsInt()); + } + } + + @Test + void oneEntryOverCapacityStartsASecondPage() throws IOException { + new TestAtlas(new AtlasLayout(2, 4, 4, 0), solidEntries(5, 2)).generate(); + + assertTrue(Files.exists(directory.resolve("atlas-1.png"))); + assertFalse(Files.exists(directory.resolve("atlas-2.png"))); + + JsonObject last = readCoordinates().get(4).getAsJsonObject(); + assertEquals(1, last.get("page").getAsInt()); + assertEquals(0, last.get("x").getAsInt()); + assertEquals(0, last.get("y").getAsInt()); + } + + @Test + void paddedLayoutOffsetsBothAxesAndLeavesGuttersBetweenRows() throws IOException { + List entries = List.of( + entry("a", RED, 2), entry("b", GREEN, 2), entry("c", BLUE, 2), entry("d", RED, 2) + ); + new TestAtlas(new AtlasLayout(2, 8, 8, 1), entries).generate(); + + BufferedImage page = ImageIO.read(directory.resolve("atlas.png").toFile()); + assertEquals(8, page.getWidth()); + assertEquals(8, page.getHeight()); + assertTile(page, 1, 1, 2, RED); + assertTile(page, 5, 1, 2, GREEN); + assertTile(page, 1, 5, 2, BLUE); + assertTile(page, 5, 5, 2, RED); + assertTransparent(page, 0, 3, 8, 2); + assertTransparent(page, 3, 0, 2, 8); + assertTransparent(page, 0, 7, 8, 1); + assertTransparent(page, 7, 0, 1, 8); + + JsonArray json = readCoordinates(); + assertEquals(1, json.get(2).getAsJsonObject().get("x").getAsInt()); + assertEquals(5, json.get(2).getAsJsonObject().get("y").getAsInt()); + } + + @Test + void lastPageIsOnlyAsTallAsItsRows() throws IOException { + new TestAtlas(new AtlasLayout(2, 4, 8, 0), solidEntries(10, 2)).generate(); + + BufferedImage first = ImageIO.read(directory.resolve("atlas.png").toFile()); + BufferedImage second = ImageIO.read(directory.resolve("atlas-1.png").toFile()); + assertEquals(8, first.getHeight()); + assertEquals(2, second.getHeight()); + assertEquals(4, second.getWidth()); + } + + @Test + void stalePagesFromAnEarlierRunAreRemoved() throws IOException { + Files.write(directory.resolve("atlas-1.png"), new byte[]{1, 2, 3}); + Files.write(directory.resolve("atlas-2.png"), new byte[]{1, 2, 3}); + + new TestAtlas(new AtlasLayout(2, 4, 4, 0), List.of(entry("red", RED, 2))).generate(); + + assertFalse(Files.exists(directory.resolve("atlas-1.png"))); + assertFalse(Files.exists(directory.resolve("atlas-2.png"))); + } + + @Test + void stalePagesBeyondAGapAreRemovedAndUnrelatedFilesAreKept() throws IOException { + Files.write(directory.resolve("atlas-3.png"), new byte[]{1, 2, 3}); + Files.write(directory.resolve("other-1.png"), new byte[]{1, 2, 3}); + Files.write(directory.resolve("atlas-x.png"), new byte[]{1, 2, 3}); + + new TestAtlas(new AtlasLayout(2, 4, 4, 0), List.of(entry("red", RED, 2))).generate(); + + assertFalse(Files.exists(directory.resolve("atlas-3.png"))); + assertTrue(Files.exists(directory.resolve("other-1.png")), "a different atlas's pages are left alone"); + assertTrue(Files.exists(directory.resolve("atlas-x.png")), "only numbered suffixes are pages"); + } + + @Test + void aShrinkingRunKeepsThePagesItWroteAndRemovesTheRest() throws IOException { + Files.write(directory.resolve("atlas-1.png"), new byte[]{1, 2, 3}); + Files.write(directory.resolve("atlas-2.png"), new byte[]{1, 2, 3}); + + new TestAtlas(new AtlasLayout(2, 4, 4, 0), solidEntries(5, 2)).generate(); + + assertEquals(4, ImageIO.read(directory.resolve("atlas-1.png").toFile()).getWidth()); + assertFalse(Files.exists(directory.resolve("atlas-2.png"))); + } + + @Test + void coordinatesCarryPageAndAtlasButNotPageDimensions() throws IOException { + new TestAtlas(new AtlasLayout(2, 4, 4, 0), solidEntries(5, 2)).generate(); + + JsonArray json = readCoordinates(); + assertEquals(5, json.size()); + for (int i = 0; i < json.size(); i++) { + assertEquals(Set.of("name", "x", "y", "size", "page", "atlas"), json.get(i).getAsJsonObject().keySet()); + } + } + + @Test + void pageOutputPathNumbersPagesBeforeTheExtension() { + TestAtlas atlas = new TestAtlas(AtlasLayout.DEFAULT, List.of()); + assertEquals(directory.resolve("atlas.png"), atlas.pageOutputPath(0)); + assertEquals(directory.resolve("atlas-2.png"), atlas.pageOutputPath(2)); + } + + @Test + void pageOutputPathIgnoresDotsInDirectoryNames() { + Path output = directory.resolve("v26.3").resolve("atlas"); + TestAtlas atlas = new TestAtlas(AtlasLayout.DEFAULT, List.of(), output); + assertEquals(output, atlas.pageOutputPath(0)); + assertEquals(directory.resolve("v26.3").resolve("atlas-1"), atlas.pageOutputPath(1)); + } + + @Test + void generatingNothingWritesNoFiles() { + new TestAtlas(new AtlasLayout(2, 4, 4, 0), List.of()).generate(); + + assertFalse(Files.exists(directory.resolve("atlas.png"))); + assertFalse(Files.exists(directory.resolve("coordinates.json"))); + } + + private JsonArray readCoordinates() throws IOException { + return JsonParser.parseString(Files.readString(directory.resolve("coordinates.json"))).getAsJsonArray(); + } + + private static List solidEntries(int count, int size) { + List entries = new ArrayList<>(); + for (int i = 0; i < count; i++) { + entries.add(entry("entry" + i, 0xff000000 | (i * 0x010101), size)); + } + return entries; + } + + private static AtlasGenerator.AtlasEntry entry(String name, int argb, int size) { + BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + image.setRGB(x, y, argb); + } + } + return new AtlasGenerator.AtlasEntry(name, image); + } + + private static void assertTile(BufferedImage image, int x0, int y0, int size, int argb) { + for (int y = y0; y < y0 + size; y++) { + for (int x = x0; x < x0 + size; x++) { + assertEquals(argb, image.getRGB(x, y), "pixel at " + x + "," + y); + } + } + } + + private static void assertTransparent(BufferedImage image, int x0, int y0, int width, int height) { + for (int y = y0; y < y0 + height; y++) { + for (int x = x0; x < x0 + width; x++) { + assertEquals(0, image.getRGB(x, y), "pixel at " + x + "," + y + " should be transparent"); + } + } + } + + private class TestAtlas extends AtlasGenerator { + + private final List entries; + + TestAtlas(AtlasLayout layout, List entries) { + this(layout, entries, directory.resolve("atlas.png")); + } + + TestAtlas(AtlasLayout layout, List entries, Path atlasOutputPath) { + super(layout, atlasOutputPath, directory.resolve("coordinates.json")); + this.entries = entries; + } + + @Override + protected List loadTextures() { + return entries; + } + + @Override + protected JsonObject entryToJson(AtlasEntry entry) { + JsonObject json = new JsonObject(); + json.addProperty("name", entry.name); + json.addProperty("x", entry.x); + json.addProperty("y", entry.y); + json.addProperty("size", layout.tileSize()); + return json; + } + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayoutTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayoutTest.java index d448b02..3f2f82f 100644 --- a/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayoutTest.java +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/AtlasLayoutTest.java @@ -1,81 +1,107 @@ package net.aerh.imagegenerator.tooling.spritesheet; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import static org.junit.jupiter.api.Assertions.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class AtlasLayoutTest { - @TempDir Path directory; - - @Test void pagesKeepPixelsAndTransparentGutters() throws Exception { - var entries = List.of(entry("red", 0xffff0000), entry("green", 0xff00ff00), entry("blue", 0xff0000ff)); - new TestAtlas(new AtlasLayout(2, 8, 4, 1), entries).generate(); - var first = ImageIO.read(directory.resolve("atlas.png").toFile()); - var second = ImageIO.read(directory.resolve("atlas-1.png").toFile()); - assertEquals(8, first.getWidth()); - assertEquals(4, first.getHeight()); - assertEquals(0xffff0000, first.getRGB(1, 1)); - assertEquals(0xff00ff00, first.getRGB(5, 1)); - assertEquals(0xff0000ff, second.getRGB(1, 1)); - assertEquals(0, first.getRGB(0, 1)); - assertEquals(0, first.getRGB(3, 1)); - assertEquals(0, first.getRGB(4, 1)); - var json = JsonParser.parseString(Files.readString(directory.resolve("coordinates.json"))).getAsJsonArray(); - assertEquals(0, json.get(0).getAsJsonObject().get("page").getAsInt()); - assertEquals(1, json.get(2).getAsJsonObject().get("page").getAsInt()); - assertEquals("atlas-1.png", json.get(2).getAsJsonObject().get("atlas").getAsString()); - assertEquals(4, json.get(2).getAsJsonObject().get("atlasHeight").getAsInt()); + + @Test + void defaultLayoutMatchesTheBundledAtlas() { + assertEquals(256, AtlasLayout.DEFAULT.tileSize()); + assertEquals(4096, AtlasLayout.DEFAULT.maxWidth()); + assertEquals(AtlasLayout.UNLIMITED, AtlasLayout.DEFAULT.maxHeight()); + assertEquals(0, AtlasLayout.DEFAULT.padding()); + assertEquals(Integer.MAX_VALUE, AtlasLayout.UNLIMITED); + } + + @Test + void pitchIsTilePlusPaddingOnBothSides() { + assertEquals(10, new AtlasLayout(8, 100, 100, 1).pitch()); + assertEquals(8, new AtlasLayout(8, 100, 100, 0).pitch()); + } + + @Test + void columnsAndRowsRoundDownWhenBoundsAreNotAMultipleOfPitch() { + AtlasLayout layout = new AtlasLayout(8, 35, 21, 1); + assertEquals(3, layout.columns()); + assertEquals(2, layout.rowsPerPage()); + assertEquals(6, layout.capacity()); + } + + @Test + void unlimitedHeightGivesUnlimitedRowsWithoutOverflow() { + AtlasLayout layout = new AtlasLayout(1, 4096, AtlasLayout.UNLIMITED, 0); + assertEquals(Integer.MAX_VALUE, layout.rowsPerPage()); + assertEquals(Integer.MAX_VALUE, layout.capacity()); + } + + @Test + void zeroTileSizeIsRejectedWithTheOffendingValue() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new AtlasLayout(0, 8, 8, 0)); + assertEquals("Tile size must be positive, got 0", error.getMessage()); + } + + @Test + void negativePaddingIsRejectedWithTheOffendingValue() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new AtlasLayout(4, 8, 8, -1)); + assertEquals("Padding must be non-negative, got -1", error.getMessage()); + } + + @Test + void paddingThatOverflowsThePitchIsRejectedAsTooLarge() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new AtlasLayout(Integer.MAX_VALUE, 8, 8, 1)); + assertEquals("Padding 1 is too large for tile size " + Integer.MAX_VALUE, error.getMessage()); + } + + @Test + void boundsSmallerThanOnePaddedTileAreRejected() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> new AtlasLayout(4, 5, 5, 1)); + assertEquals("Atlas bounds 5x5 must fit at least one padded tile of 6px", error.getMessage()); + } + + @Test + void parsingWithoutFlagsReturnsTheGivenDefaults() { + AtlasLayout defaults = new AtlasLayout(16, 64, 64, 2); + assertEquals(defaults, AtlasLayout.fromArgs(new String[]{"--input=x", "--debug"}, defaults)); } - @Test void singlePageKeepsOriginalFilenameAndCoordinateFields() throws Exception { - new TestAtlas(new AtlasLayout(2, 4, 4, 0), List.of(entry("red", 0xffff0000))).generate(); - assertTrue(Files.isRegularFile(directory.resolve("atlas.png"))); - assertFalse(Files.exists(directory.resolve("atlas-0.png"))); - var coordinate = JsonParser.parseString(Files.readString(directory.resolve("coordinates.json"))) - .getAsJsonArray().get(0).getAsJsonObject(); - assertEquals(0, coordinate.get("x").getAsInt()); - assertEquals(0, coordinate.get("y").getAsInt()); - assertEquals(2, coordinate.get("size").getAsInt()); + @Test + void parsingReadsEveryLayoutFlag() { + String[] args = {"--tile-size=32", "--atlas-width=2048", "--atlas-height=1024", "--padding=3"}; + assertEquals(new AtlasLayout(32, 2048, 1024, 3), AtlasLayout.fromArgs(args, AtlasLayout.DEFAULT)); } - @Test void invalidLayoutsFailBeforePacking() { - assertThrows(IllegalArgumentException.class, () -> new AtlasLayout(0, 8, 8, 0)); - assertThrows(IllegalArgumentException.class, () -> new AtlasLayout(4, 5, 5, 1)); - assertThrows(IllegalArgumentException.class, () -> new AtlasLayout(4, 8, 8, -1)); - assertThrows(IllegalArgumentException.class, () -> new AtlasLayout(Integer.MAX_VALUE, 8, 8, 1)); + @Test + void parsingOverridesOnlyTheFlagsThatArePresent() { + AtlasLayout layout = AtlasLayout.fromArgs(new String[]{"--padding=1"}, AtlasLayout.DEFAULT); + assertEquals(new AtlasLayout(256, 4096, AtlasLayout.UNLIMITED, 1), layout); } - @Test void unlimitedHeightDoesNotOverflowCapacity() { - assertEquals(Integer.MAX_VALUE, new AtlasLayout(1, 4096, Integer.MAX_VALUE, 0).capacity()); + @Test + void parsingNamesTheFlagWhenTheValueIsNotAnInteger() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> AtlasLayout.fromArgs(new String[]{"--padding=x"}, AtlasLayout.DEFAULT)); + assertEquals("--padding expects an integer, got 'x'", error.getMessage()); } - private AtlasGenerator.AtlasEntry entry(String name, int color) { - var image = new BufferedImage(2, 2, BufferedImage.TYPE_INT_ARGB); - for (int y = 0; y < 2; y++) for (int x = 0; x < 2; x++) image.setRGB(x, y, color); - return new AtlasGenerator.AtlasEntry(name, image); + @Test + void parsingRejectsAnEmptyValue() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> AtlasLayout.fromArgs(new String[]{"--atlas-width="}, AtlasLayout.DEFAULT)); + assertEquals("--atlas-width expects an integer, got ''", error.getMessage()); } - private class TestAtlas extends AtlasGenerator { - private final List entries; - TestAtlas(AtlasLayout layout, List entries) { - super(layout, directory.resolve("atlas.png").toString(), directory.resolve("coordinates.json").toString()); - this.entries = entries; - } - protected List loadTextures() { return entries; } - protected JsonObject entryToJson(AtlasEntry entry) { - var json = new JsonObject(); - json.addProperty("name", entry.name); - json.addProperty("x", entry.x); - json.addProperty("y", entry.y); - json.addProperty("size", layout.tileSize()); - return json; - } + @Test + void parsingStillValidatesTheCombinedLayout() { + IllegalArgumentException error = assertThrows(IllegalArgumentException.class, + () -> AtlasLayout.fromArgs(new String[]{"--atlas-width=10"}, AtlasLayout.DEFAULT)); + assertTrue(error.getMessage().startsWith("Atlas bounds 10x"), error.getMessage()); } } diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGeneratorTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGeneratorTest.java new file mode 100644 index 0000000..b519436 --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayColorConfigGeneratorTest.java @@ -0,0 +1,62 @@ +package net.aerh.imagegenerator.tooling.spritesheet; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import net.aerh.imagegenerator.tooling.minecraft.TrimPaletteExtractor.TrimPalettes; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class OverlayColorConfigGeneratorTest { + + private static final int[] GREYS = { + 0xfff1f1f1, 0xffe1e1e1, 0xffd0d0d0, 0xffbcbcbc, 0xffa5a5a5, 0xff898989, 0xff636363, 0xff000000 + }; + private static final int[] GOLD = { + 0xfffffd90, 0xffecd93f, 0xffdeb12d, 0xffb16712, 0xffa0450a, 0xff803503, 0xff712d00, 0xff572300 + }; + + @Test + void armorTrimMapUsesTheKeyPaletteGreysInOrder() { + JsonObject config = OverlayColorConfigGenerator.createArmorTrimConfig(new TrimPalettes(GREYS, Map.of("gold", GOLD))); + + JsonObject map = config.getAsJsonObject("map"); + assertEquals(GREYS.length, map.size()); + for (int index = 0; index < GREYS.length; index++) { + assertEquals(index, map.get(String.valueOf(GREYS[index])).getAsInt(), "index of grey " + index); + } + } + + @Test + void armorTrimOptionsListEveryMaterialPalette() { + Map materials = new LinkedHashMap<>(); + materials.put("gold", GOLD); + materials.put("gold_darker", GREYS); + + JsonObject config = OverlayColorConfigGenerator.createArmorTrimConfig(new TrimPalettes(GREYS, materials)); + + assertEquals("armor_trim", config.get("name").getAsString()); + assertEquals(Set.of("gold", "gold_darker"), config.getAsJsonObject("options").keySet()); + JsonArray gold = config.getAsJsonObject("options").getAsJsonArray("gold"); + assertEquals(GOLD.length, gold.size()); + assertEquals(GOLD[0], gold.get(0).getAsInt()); + assertFalse(config.get("allowHexColors").getAsBoolean()); + assertFalse(config.get("useDefaultIfMissing").getAsBoolean()); + } + + @Test + void materialsWhoseLengthDiffersFromTheKeyAreLeftOut() { + Map materials = new LinkedHashMap<>(); + materials.put("gold", GOLD); + materials.put("broken", new int[]{0xff000000, 0xffffffff}); + + JsonObject config = OverlayColorConfigGenerator.createArmorTrimConfig(new TrimPalettes(GREYS, materials)); + + assertEquals(Set.of("gold"), config.getAsJsonObject("options").keySet()); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGeneratorTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGeneratorTest.java new file mode 100644 index 0000000..60fdbf3 --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/OverlayGeneratorTest.java @@ -0,0 +1,180 @@ +package net.aerh.imagegenerator.tooling.spritesheet; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import net.aerh.imagegenerator.tooling.ToolingPaths; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class OverlayGeneratorTest { + + @TempDir + Path root; + + @Test + void armorAndTrimOverlaysAreDiscoveredFromTheMinecraftAssets() throws IOException { + writeItemTexture("leather_boots_overlay"); + writeItemTexture("stone"); + // The game ships one greyscale overlay per armour piece, named "_trim.png". + writeTrimTexture("helmet_trim"); + writeTrimTexture("chestplate_trim"); + writeTrimTexture("coast_helmet"); + + new OverlayGenerator(paths()).generate(); + + Map entries = entriesByName(); + assertEquals("NORMAL", entries.get("leather_boots_overlay").get("type").getAsString()); + assertEquals("leather_armor", entries.get("leather_boots_overlay").get("colorOptions").getAsString()); + assertTrue(entries.containsKey("helmet_trim"), "helmet_trim should be discovered: " + entries.keySet()); + assertTrue(entries.containsKey("chestplate_trim"), "chestplate_trim should be discovered: " + entries.keySet()); + assertEquals("MAPPED", entries.get("helmet_trim").get("type").getAsString()); + assertEquals("armor_trim", entries.get("chestplate_trim").get("colorOptions").getAsString()); + assertFalse(entries.containsKey("coast_helmet"), "only *_trim.png files are trim overlays"); + assertFalse(entries.containsKey("trim_trim")); + assertFalse(entries.containsKey("stone")); + } + + @Test + void overlaysStayOnOneTwoFiftySixPixelPage() throws IOException { + writeItemTexture("leather_boots_overlay"); + writeTrimTexture("helmet_trim"); + + new OverlayGenerator(paths()).generate(); + + BufferedImage sheet = ImageIO.read(paths().spritesheets().resolve("overlays.png").toFile()); + assertEquals(1024, sheet.getWidth()); + assertEquals(256, sheet.getHeight()); + assertFalse(Files.exists(paths().spritesheets().resolve("overlays-1.png"))); + for (JsonObject entry : entriesByName().values()) { + assertEquals(256, entry.get("size").getAsInt()); + assertEquals(0, entry.get("page").getAsInt()); + assertEquals("overlays.png", entry.get("atlas").getAsString()); + } + } + + @Test + void bundledDefinitionsIncludeSpecialOverlaysThatExistInTheAssets() throws IOException { + writeItemTexture("potion_overlay"); + writeItemTexture("tipped_arrow_head"); + + new OverlayGenerator(paths()).generate(); + + Map entries = entriesByName(); + assertEquals("potion", entries.get("potion_overlay").get("colorOptions").getAsString()); + assertEquals("OVERLAY", entries.get("potion_overlay").get("colorMode").getAsString()); + assertEquals("effect", entries.get("tipped_arrow_head").get("colorOptions").getAsString()); + assertFalse(entries.containsKey("firework_star_overlay"), "special overlays without a texture are skipped"); + } + + @Test + void aDefinitionsFileOverrideReplacesTheBundledOne() throws IOException { + writeItemTexture("leather_boots_overlay"); + writeItemTexture("wolf_armor_overlay"); + writeItemTexture("custom_overlay"); + Path definitions = root.resolve("definitions.json"); + Files.writeString(definitions, """ + { + "specialOverlays": [ + {"fileName": "custom_overlay", "colorOptions": "potion", "type": "NORMAL"} + ], + "armorOverlayPatterns": ["wolf_"] + } + """); + + new OverlayGenerator(paths(), definitions).generate(); + + Map entries = entriesByName(); + assertTrue(entries.containsKey("custom_overlay")); + assertTrue(entries.containsKey("wolf_armor_overlay")); + assertFalse(entries.containsKey("leather_boots_overlay"), "patterns come from the override only"); + assertFalse(entries.get("custom_overlay").has("colorMode")); + } + + @Test + void aMissingDefinitionsFileIsAnError() { + IOException error = assertThrows(IOException.class, + () -> new OverlayGenerator(paths(), root.resolve("missing.json"))); + assertTrue(error.getMessage().contains("missing.json"), error.getMessage()); + } + + @Test + void aNullColorModeInTheDefinitionsIsAnError() throws IOException { + Path definitions = root.resolve("definitions.json"); + Files.writeString(definitions, """ + {"specialOverlays": [{"fileName": "x", "colorOptions": "potion", "type": "NORMAL", "colorMode": null}]} + """); + + IOException error = assertThrows(IOException.class, () -> new OverlayGenerator(paths(), definitions)); + + assertTrue(error.getMessage().contains("colorMode"), error.getMessage()); + assertTrue(error.getMessage().contains("definitions.json"), error.getMessage()); + } + + @Test + void aNonStringArmorPatternIsAnError() throws IOException { + Path definitions = root.resolve("definitions.json"); + Files.writeString(definitions, "{\"armorOverlayPatterns\": [\"leather_\", {\"x\": 1}]}"); + + IOException error = assertThrows(IOException.class, () -> new OverlayGenerator(paths(), definitions)); + + assertTrue(error.getMessage().contains("armorOverlayPatterns"), error.getMessage()); + } + + @Test + void aMalformedDefinitionsFileIsAnError() throws IOException { + Path definitions = root.resolve("broken.json"); + Files.writeString(definitions, "{ not json"); + + IOException error = assertThrows(IOException.class, () -> new OverlayGenerator(paths(), definitions)); + assertTrue(error.getMessage().contains("broken.json"), error.getMessage()); + } + + private ToolingPaths paths() { + return new ToolingPaths(root.resolve("work"), root.resolve("out")); + } + + private Map entriesByName() throws IOException { + Path json = paths().json().resolve("overlay_coordinates.json"); + assertTrue(Files.isRegularFile(json), "overlay coordinates should exist"); + JsonArray array = JsonParser.parseString(Files.readString(json)).getAsJsonArray(); + Map entries = new HashMap<>(); + for (JsonElement element : array) { + entries.put(element.getAsJsonObject().get("name").getAsString(), element.getAsJsonObject()); + } + return entries; + } + + private void writeItemTexture(String name) throws IOException { + writePng(paths().minecraftAssets().resolve("textures/item").resolve(name + ".png")); + } + + private void writeTrimTexture(String name) throws IOException { + writePng(paths().minecraftAssets().resolve("textures/trims/items").resolve(name + ".png")); + } + + private static void writePng(Path file) throws IOException { + BufferedImage image = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < 16; y++) { + for (int x = 0; x < 16; x++) { + image.setRGB(x, y, 0xff808080); + } + } + Files.createDirectories(file.getParent()); + ImageIO.write(image, "png", file.toFile()); + } +} diff --git a/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGeneratorTest.java b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGeneratorTest.java new file mode 100644 index 0000000..4198b2c --- /dev/null +++ b/tooling/src/test/java/net/aerh/imagegenerator/tooling/spritesheet/SpritesheetGeneratorTest.java @@ -0,0 +1,168 @@ +package net.aerh.imagegenerator.tooling.spritesheet; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import net.aerh.imagegenerator.tooling.ToolingPaths; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SpritesheetGeneratorTest { + + private static final int RED = 0xffff0000; + private static final int GREEN = 0xff00ff00; + private static final AtlasLayout LAYOUT = new AtlasLayout(2, 8, 8, 0); + + @TempDir + Path root; + + @Test + void packsEveryPngInTheInputDirectoryInNameOrder() throws IOException { + Path input = root.resolve("input"); + writePng(input.resolve("b.png"), GREEN); + writePng(input.resolve("a.png"), RED); + Files.createDirectories(input); + Files.writeString(input.resolve("notes.txt"), "not an image"); + + new SpritesheetGenerator(input, paths(), LAYOUT).generate(); + + BufferedImage atlas = ImageIO.read(paths().spritesheets().resolve("minecraft_texture_atlas.png").toFile()); + assertEquals(RED, atlas.getRGB(0, 0)); + assertEquals(GREEN, atlas.getRGB(2, 0)); + assertEquals(List.of("a", "b"), names()); + assertEquals(2, readCoordinates().get(0).getAsJsonObject().get("size").getAsInt()); + } + + @Test + void fullyTransparentImagesAreSkipped() throws IOException { + Path input = root.resolve("input"); + writePng(input.resolve("solid.png"), RED); + writePng(input.resolve("empty.png"), 0); + + new SpritesheetGenerator(input, paths(), LAYOUT).generate(); + + assertEquals(List.of("solid"), names()); + } + + @Test + void armorTrimVariantsAreLeftOutWhenTheAssetsHaveTrimPalettes() throws IOException { + Path input = root.resolve("input"); + writePng(input.resolve("iron_helmet.png"), RED); + writePng(input.resolve("iron_helmet_amethyst_trim.png"), GREEN); + Path palettes = paths().minecraftAssets().resolve("textures/palettes"); + writePalette(palettes.resolve("trim_base.png")); + writePalette(palettes.resolve("trim/amethyst.png")); + + new SpritesheetGenerator(input, paths(), LAYOUT).generate(); + + assertEquals(List.of("iron_helmet"), names()); + } + + @Test + void armorTrimVariantsAreLeftOutWithTheLegacyPaletteLayout() throws IOException { + Path input = root.resolve("input"); + writePng(input.resolve("iron_helmet.png"), RED); + writePng(input.resolve("iron_helmet_amethyst_trim.png"), GREEN); + Path palettes = paths().minecraftAssets().resolve("textures/trims/color_palettes"); + writePalette(palettes.resolve("trim_palette.png")); + writePalette(palettes.resolve("amethyst.png")); + + new SpritesheetGenerator(input, paths(), LAYOUT).generate(); + + assertEquals(List.of("iron_helmet"), names()); + } + + @Test + void trimVariantsStayWhenNoPalettesAreAvailable() throws IOException { + Path input = root.resolve("input"); + writePng(input.resolve("iron_helmet.png"), RED); + writePng(input.resolve("iron_helmet_amethyst_trim.png"), GREEN); + + new SpritesheetGenerator(input, paths(), LAYOUT).generate(); + + assertEquals(List.of("iron_helmet", "iron_helmet_amethyst_trim"), names()); + } + + @Test + void aPaletteLayoutWithoutItsKeyFailsTheRunInsteadOfPackingTrims() throws IOException { + Path input = root.resolve("input"); + writePng(input.resolve("iron_helmet.png"), RED); + writePng(input.resolve("iron_helmet_amethyst_trim.png"), GREEN); + writePalette(paths().minecraftAssets().resolve("textures/palettes/trim/amethyst.png")); + + UncheckedIOException error = assertThrows(UncheckedIOException.class, + () -> new SpritesheetGenerator(input, paths(), LAYOUT).generate()); + + assertTrue(error.getMessage().contains("trim_base.png"), error.getMessage()); + assertFalse(Files.exists(paths().spritesheets()), "nothing should be written"); + } + + @Test + void inputDefaultsToTheRenderedItemsDirectory() throws IOException { + writePng(paths().renderedItems().resolve("stone.png"), RED); + + new SpritesheetGenerator(paths(), LAYOUT).generate(); + + assertEquals(List.of("stone"), names()); + } + + @Test + void missingInputDirectoryWritesNothing() { + new SpritesheetGenerator(root.resolve("missing"), paths(), LAYOUT).generate(); + + assertFalse(Files.exists(paths().spritesheets())); + assertFalse(Files.exists(paths().json())); + } + + private ToolingPaths paths() { + return new ToolingPaths(root.resolve("work"), root.resolve("out")); + } + + private JsonArray readCoordinates() throws IOException { + Path json = paths().json().resolve("atlas_coordinates.json"); + assertTrue(Files.isRegularFile(json), "coordinates file should exist"); + return JsonParser.parseString(Files.readString(json)).getAsJsonArray(); + } + + private List names() throws IOException { + List names = new ArrayList<>(); + for (JsonElement element : readCoordinates()) { + names.add(element.getAsJsonObject().get("name").getAsString()); + } + return names; + } + + private static void writePng(Path file, int argb) throws IOException { + BufferedImage image = new BufferedImage(4, 4, BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < 4; y++) { + for (int x = 0; x < 4; x++) { + image.setRGB(x, y, argb); + } + } + Files.createDirectories(file.getParent()); + ImageIO.write(image, "png", file.toFile()); + } + + private static void writePalette(Path file) throws IOException { + BufferedImage image = new BufferedImage(8, 1, BufferedImage.TYPE_INT_ARGB); + for (int x = 0; x < 8; x++) { + image.setRGB(x, 0, 0xff000000 | (x * 0x202020)); + } + Files.createDirectories(file.getParent()); + ImageIO.write(image, "png", file.toFile()); + } +}