minerender 2.1.0-alpha.115 → 2.1.0-alpha.117

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -324,6 +324,9 @@ var init_Caching = __esm({
324
324
  static {
325
325
  this.usernameUuidCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
326
326
  }
327
+ static {
328
+ this.biomeCache = _Caching.createAssetCache();
329
+ }
327
330
  static createAssetCache() {
328
331
  return import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(10)).expireAfterAccess(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
329
332
  }
@@ -353,7 +356,8 @@ var init_Caching = __esm({
353
356
  this.fontCache,
354
357
  this.bannerPatternCache,
355
358
  this.mineRenderDataCache,
356
- this.usernameUuidCache
359
+ this.usernameUuidCache,
360
+ this.biomeCache
357
361
  ];
358
362
  }
359
363
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -1659,7 +1663,8 @@ var init_MineRenderData = __esm({
1659
1663
  }
1660
1664
  /**
1661
1665
  * Uses the exact version when published. Otherwise, selects the nearest patch in the same
1662
- * release line (older on ties), then the latest earlier release. Unpublished snapshots reject.
1666
+ * release line (older on ties), then the latest earlier release or the oldest published release.
1667
+ * Unpublished snapshots reject.
1663
1668
  */
1664
1669
  static async resolve(root) {
1665
1670
  const requestRoot = root?.replace(/\/+$/, "");
@@ -1668,11 +1673,11 @@ var init_MineRenderData = __esm({
1668
1673
  const scope = AssetLoader.persistentScope;
1669
1674
  return this.cached(`resolve:${requestRoot === void 0 ? "sources" : "explicit"}:${root}`, scope, defaultRoot, async () => {
1670
1675
  const requestedVersion = root.slice(root.lastIndexOf("/") + 1);
1671
- let manifest = await this.manifest(root, release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1676
+ let manifest = await this.manifest(root, requestRoot !== void 0 && release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1672
1677
  if (manifest) return { requestedVersion, version: manifest.minecraftVersion, root, manifest };
1673
1678
  const requested = release(requestedVersion);
1674
- if (!requested || compare(requested, [1, 16, 5]) < 0) {
1675
- throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release from 1.16.5 onward`);
1679
+ if (!requested) {
1680
+ throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release`);
1676
1681
  }
1677
1682
  const base = root.slice(0, root.lastIndexOf("/"));
1678
1683
  const index = await this.cached(`versions:${base}`, scope, defaultRoot, async () => {
@@ -1682,9 +1687,9 @@ var init_MineRenderData = __esm({
1682
1687
  }
1683
1688
  return value.versions;
1684
1689
  });
1685
- const versions = index.map((entry) => ({ name: entry.name, number: release(entry.name) })).filter((entry) => !!entry.number && compare(entry.number, [1, 16, 5]) >= 0).sort((left, right) => compare(left.number, right.number));
1690
+ const versions = index.map((entry) => ({ name: entry.name, number: release(entry.name) })).filter((entry) => !!entry.number).sort((left, right) => compare(left.number, right.number));
1686
1691
  const sameLine = versions.filter((entry) => entry.number[0] === requested[0] && entry.number[1] === requested[1]);
1687
- const selected = sameLine.sort((left, right) => Math.abs((left.number[2] ?? 0) - (requested[2] ?? 0)) - Math.abs((right.number[2] ?? 0) - (requested[2] ?? 0)) || compare(left.number, right.number))[0] ?? versions.filter((entry) => compare(entry.number, requested) <= 0).pop();
1692
+ const selected = sameLine.sort((left, right) => Math.abs((left.number[2] ?? 0) - (requested[2] ?? 0)) - Math.abs((right.number[2] ?? 0) - (requested[2] ?? 0)) || compare(left.number, right.number))[0] ?? versions.filter((entry) => compare(entry.number, requested) <= 0).pop() ?? versions[0];
1688
1693
  if (!selected) throw new Error(`No compatible MineRender data is published for ${requestedVersion}`);
1689
1694
  const resolvedRoot = `${base}/${selected.name}`;
1690
1695
  manifest = await this.manifest(resolvedRoot, selected.name, resolvedRoot);
@@ -1731,7 +1736,7 @@ ${key}` : key, async () => {
1731
1736
  case "blockStates":
1732
1737
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1733
1738
  case "blockTints":
1734
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1739
+ return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color)) && (value.biome === void 0 || ["grass", "foliage", "dry_foliage", "water"].includes(value.biome));
1735
1740
  case "fluids":
1736
1741
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1737
1742
  case "dyes":
@@ -1800,12 +1805,7 @@ var init_BannerPatterns = __esm({
1800
1805
  static async getLayers(value, target, root) {
1801
1806
  if (value === void 0) return [];
1802
1807
  if (!Array.isArray(value)) throw new Error("banner_patterns must be an array");
1803
- const activeRoot = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
1804
- const checkSources = () => {
1805
- if (activeRoot !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading banner patterns; retry the request");
1806
- };
1807
1808
  const colors = value.length ? await this.getColors(root) : void 0;
1808
- checkSources();
1809
1809
  return Promise.all(value.slice(0, 16).map(async (layer) => {
1810
1810
  if (!layer || typeof layer !== "object" || Array.isArray(layer)) throw new Error("Each banner pattern requires a pattern and dye color");
1811
1811
  if (typeof layer.color !== "string" || !Object.prototype.hasOwnProperty.call(colors, layer.color)) throw new Error(`Unsupported dye color ${layer.color}`);
@@ -1815,7 +1815,6 @@ var init_BannerPatterns = __esm({
1815
1815
  const id = this.identifier(pattern2);
1816
1816
  const key = new AssetKey(id.namespace, id.getFullPath(), "banner_pattern", void 0, "data", ".json", root);
1817
1817
  pattern2 = await Caching.bannerPatternCache.get(key.serialize(), () => AssetLoader.get(key, "json" /* JSON */));
1818
- checkSources();
1819
1818
  if (!pattern2) throw new Error(`Missing banner pattern ${layer.pattern}`);
1820
1819
  }
1821
1820
  if (!pattern2 || typeof pattern2 !== "object" || Array.isArray(pattern2) || typeof pattern2.translation_key !== "string") throw new Error("Banner patterns require asset_id and translation_key");
@@ -1875,15 +1874,19 @@ var init_Colormaps = __esm({
1875
1874
  Colormaps = class {
1876
1875
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1877
1876
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1878
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1877
+ return this.getColor("grass", origin, temperature, downfall);
1878
+ }
1879
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1880
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1881
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1879
1882
  const temp = Math.fround(temperature);
1880
1883
  const rain = Math.fround(downfall) * temp;
1881
1884
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1882
1885
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1883
1886
  const image = await ModelTextures.get(key);
1884
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1887
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1885
1888
  if (image.width !== 256 || image.height !== 256) {
1886
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1889
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1887
1890
  }
1888
1891
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1889
1892
  return red << 16 | green << 8 | blue;
@@ -2100,30 +2103,24 @@ var init_Models = __esm({
2100
2103
  }).then((r) => r?.files ?? []);
2101
2104
  }
2102
2105
  static async loadAndMerge(key, context = {}) {
2103
- const checkSources = this.sourceCheck();
2104
2106
  const model = key.type === "item" ? await this.getItemModel(key, context) : await this.getRaw(key);
2105
2107
  if (!model) {
2106
2108
  return void 0;
2107
2109
  }
2108
2110
  const merged = await ModelMerger.mergeWithParents(model);
2109
- checkSources();
2110
2111
  return merged;
2111
2112
  }
2112
2113
  static async getItemModel(key, context = {}) {
2113
- const checkSources = this.sourceCheck();
2114
2114
  const preview = this.snapshotContext(key, context);
2115
2115
  const dataKey = await this.applyDefaults(key, preview);
2116
- checkSources();
2117
2116
  return this.getPreparedItemModel(key, preview, dataKey);
2118
2117
  }
2119
2118
  static async getPreparedItemModel(key, preview, dataKey) {
2120
- const checkSources = this.sourceCheck();
2121
2119
  const itemId = `${key.namespace}:${key.path}`;
2122
2120
  const itemKey2 = new AssetKey(key.namespace, key.path, "items", void 0, key.rootType, ".json", key.root);
2123
2121
  const cacheKey = itemKey2.serialize() + this.contextKey(preview) + dataKey;
2124
2122
  const model = await this.PERSISTENT_CACHE.getOrLoad(`${this.ITEM_CACHE_VERSION}:${AssetLoader.persistentKey(cacheKey)}`, async () => {
2125
2123
  const result = await AssetLoader.getFirst([itemKey2, key], "json" /* JSON */);
2126
- checkSources();
2127
2124
  if (!result) return void 0;
2128
2125
  if (result.key.assetType !== "items") return { ...result.asset, key, itemId, components: preview.components };
2129
2126
  const load = async (selected) => {
@@ -2142,10 +2139,8 @@ var init_Models = __esm({
2142
2139
  return { ...model3, itemId, components: preview.components, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
2143
2140
  };
2144
2141
  const model2 = await load(this.selectItemModel(result.asset.model, key, preview));
2145
- checkSources();
2146
2142
  return model2;
2147
2143
  });
2148
- checkSources();
2149
2144
  const restore = (model2) => ({
2150
2145
  ...model2,
2151
2146
  key: Object.assign(new AssetKey("", ""), model2.key),
@@ -2209,22 +2204,14 @@ var init_Models = __esm({
2209
2204
  }
2210
2205
  static async applyDefaults(key, preview) {
2211
2206
  if (key.type !== "item") return "";
2212
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2213
2207
  const resolved = await MineRenderData.resolve(key.root);
2214
2208
  const defaults = await ItemDefaults.get(`${key.namespace}:${key.path}`, key.root);
2215
2209
  preview.components = { ...defaults, ...preview.components };
2216
2210
  if (resolved.manifest.dataVersion < 3337 && key.namespace === DEFAULT_NAMESPACE && ["potion", "splash_potion", "lingering_potion"].includes(key.path) && !Object.prototype.hasOwnProperty.call(preview.components, "minecraft:enchantment_glint_override") && await ItemTints.hasPotionEffects(preview.components["minecraft:potion_contents"], key.root)) {
2217
2211
  preview.components["minecraft:enchantment_glint_override"] = true;
2218
2212
  }
2219
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item defaults; retry the request");
2220
2213
  return `|data:${resolved.root}:${resolved.manifest.datasets.itemDefaults?.sha256}:${resolved.manifest.datasets.potionColors?.sha256}`;
2221
2214
  }
2222
- static sourceCheck() {
2223
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2224
- return () => {
2225
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item models; retry the request");
2226
- };
2227
- }
2228
2215
  static contextKey(context) {
2229
2216
  const itemReferences = Object.fromEntries(Object.entries(context.itemReferences).map(([id, key]) => [id, key.serialize()]));
2230
2217
  return `|${this.ITEM_CACHE_VERSION}:${JSON.stringify(this.snapshotJson({ ...context, itemReferences }))}`;
@@ -2411,7 +2398,6 @@ var init_Models = __esm({
2411
2398
  * @param key - Model key, for example `AssetKey.parse("models", "minecraft:item/diamond_sword")`.
2412
2399
  */
2413
2400
  static async getMerged(key, context = {}) {
2414
- const checkSources = this.sourceCheck();
2415
2401
  if (!key.assetType) {
2416
2402
  key.assetType = "models";
2417
2403
  }
@@ -2420,15 +2406,12 @@ var init_Models = __esm({
2420
2406
  }
2421
2407
  const preview = this.snapshotContext(key, context);
2422
2408
  const dataKey = await this.applyDefaults(key, preview);
2423
- checkSources();
2424
2409
  const keyStr = AssetLoader.persistentKey(key.serialize() + (key.type === "item" ? this.contextKey(preview) + dataKey : ""));
2425
2410
  const model = await Caching.mergedModelCache.get(keyStr, async () => {
2426
2411
  const raw = key.type === "item" ? await this.getPreparedItemModel(key, preview, dataKey) : await this.getRaw(key);
2427
2412
  const merged = raw ? await ModelMerger.mergeWithParents(raw) : void 0;
2428
- checkSources();
2429
2413
  return merged;
2430
2414
  });
2431
- checkSources();
2432
2415
  return model;
2433
2416
  }
2434
2417
  /** Alias for {@link getMerged}. */
@@ -7614,6 +7597,7 @@ __export(index_node_exports, {
7614
7597
  BannerPatterns: () => BannerPatterns,
7615
7598
  BasicAssetKey: () => BasicAssetKey,
7616
7599
  BatchedExecutor: () => BatchedExecutor,
7600
+ Biomes: () => Biomes,
7617
7601
  BlockEntities: () => BlockEntities,
7618
7602
  BlockObject: () => BlockObject,
7619
7603
  BlockStateResolver: () => BlockStateResolver,
@@ -7956,6 +7940,116 @@ init_AssetLoader();
7956
7940
  init_Assets();
7957
7941
  init_BannerPatterns();
7958
7942
 
7943
+ // src/assets/Biomes.ts
7944
+ init_AssetKey();
7945
+ init_AssetLoader();
7946
+ init_AssetParsers();
7947
+ init_Caching();
7948
+ init_MineRenderError();
7949
+ init_Colormaps();
7950
+
7951
+ // src/assets/_biomes/SwampGrassColor.ts
7952
+ var permutation;
7953
+ var gradients = [[1, 1], [-1, 1], [1, -1], [-1, -1], [1, 0], [-1, 0], [1, 0], [-1, 0], [0, 1], [0, -1], [0, 1], [0, -1]];
7954
+ var skew = (Math.sqrt(3) - 1) / 2;
7955
+ var unskew = (3 - Math.sqrt(3)) / 6;
7956
+ function createPermutation() {
7957
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7958
+ let seed = 2345n ^ multiplier;
7959
+ const next = (bits) => {
7960
+ seed = seed * multiplier + 11n & mask;
7961
+ return Number(seed >> BigInt(48 - bits));
7962
+ };
7963
+ for (let i = 0; i < 3; i++) {
7964
+ next(26);
7965
+ next(27);
7966
+ }
7967
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7968
+ for (let i = 0; i < 256; i++) {
7969
+ const bound = 256 - i;
7970
+ let offset;
7971
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7972
+ else {
7973
+ let bits;
7974
+ do {
7975
+ bits = next(31);
7976
+ offset = bits % bound;
7977
+ } while (bits - offset + bound - 1 > 2147483647);
7978
+ }
7979
+ const other = i + offset;
7980
+ [values[i], values[other]] = [values[other], values[i]];
7981
+ }
7982
+ return values;
7983
+ }
7984
+ function swampGrassColor(x, z) {
7985
+ const p10 = permutation ??= createPermutation();
7986
+ x *= 0.0225;
7987
+ z *= 0.0225;
7988
+ const s = (x + z) * skew;
7989
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7990
+ const t = (i + j) * unskew;
7991
+ const dx = x - (i - t), dz = z - (j - t);
7992
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7993
+ const corner = (gx, gz, a, b) => {
7994
+ let falloff = 0.5 - a * a - b * b;
7995
+ if (falloff < 0) return 0;
7996
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7997
+ falloff *= falloff;
7998
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7999
+ };
8000
+ const noise = 70 * (corner(i, j, dx, dz) + corner(i + stepX, j + stepZ, dx - stepX + unskew, dz - stepZ + unskew) + corner(i + 1, j + 1, dx - 1 + 2 * unskew, dz - 1 + 2 * unskew));
8001
+ return noise < -0.1 ? 5011004 : 6975545;
8002
+ }
8003
+
8004
+ // src/assets/Biomes.ts
8005
+ var Biomes = class {
8006
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
8007
+ static async get(id, origin) {
8008
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
8009
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
8010
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
8011
+ return Caching.biomeCache.get(key.serialize(), async () => {
8012
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
8013
+ if (asset2 === void 0) return void 0;
8014
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
8015
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
8016
+ }
8017
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
8018
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
8019
+ }
8020
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
8021
+ const color2 = asset2.effects[`${kind}_color`];
8022
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
8023
+ }
8024
+ const modifier = asset2.effects.grass_color_modifier;
8025
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
8026
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
8027
+ }
8028
+ return asset2;
8029
+ });
8030
+ }
8031
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
8032
+ static async getColor(id, kind, origin, x = 0, z = 0) {
8033
+ const biome = await this.get(id, origin);
8034
+ if (!biome) return void 0;
8035
+ const rawColor = biome.effects[`${kind}_color`];
8036
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
8037
+ if (kind === "water") return color2;
8038
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
8039
+ if (modifier === "swamp") return swampGrassColor(x, z);
8040
+ if (color2 === void 0) {
8041
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
8042
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
8043
+ }
8044
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
8045
+ }
8046
+ static parseColor(value, id, kind) {
8047
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
8048
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
8049
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
8050
+ }
8051
+ };
8052
+
7959
8053
  // src/assets/BlockEntities.ts
7960
8054
  var import_three10 = require("three");
7961
8055
  init_AssetKey();
@@ -12394,8 +12488,20 @@ var BlockStateResolver = class _BlockStateResolver {
12394
12488
  init_Colormaps();
12395
12489
  init_MineRenderData();
12396
12490
  var BlockTints = class {
12397
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12398
- static async get(key, state, model, tints) {
12491
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12492
+ static async biomeSource(key) {
12493
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12494
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12495
+ }
12496
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12497
+ static async getBiomeColor(key, biome, position) {
12498
+ if (!biome) return void 0;
12499
+ const x = position?.[0], z = position?.[2];
12500
+ const source = await this.biomeSource(key);
12501
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12502
+ }
12503
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12504
+ static async get(key, state, model, tints, biomeColor) {
12399
12505
  if (!key) return tints;
12400
12506
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12401
12507
  if (!candidates) return tints;
@@ -12411,8 +12517,12 @@ var BlockTints = class {
12411
12517
  }
12412
12518
  }
12413
12519
  if (!indices.size) return tints;
12520
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12521
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12414
12522
  let color2;
12415
- switch (rule.source) {
12523
+ if (biome !== void 0) {
12524
+ color2 = biome;
12525
+ } else switch (rule.source) {
12416
12526
  case "constant":
12417
12527
  color2 = rule.color;
12418
12528
  break;
@@ -12449,7 +12559,6 @@ init_AssetKey();
12449
12559
  init_ModelCulling();
12450
12560
  init_FluidGeometry();
12451
12561
  init_MineRenderData();
12452
- init_AssetLoader();
12453
12562
  init_Entities();
12454
12563
  var BlockObject = class _BlockObject extends SceneObject {
12455
12564
  constructor(blockState3, options) {
@@ -12471,6 +12580,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12471
12580
  this._entityGeneration = 0;
12472
12581
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12473
12582
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12583
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12474
12584
  }
12475
12585
  static {
12476
12586
  this.DEFAULT_OPTIONS = (0, import_ts_deepmerge6.default)({}, ModelObject.DEFAULT_OPTIONS, {
@@ -12478,19 +12588,12 @@ var BlockObject = class _BlockObject extends SceneObject {
12478
12588
  });
12479
12589
  }
12480
12590
  async init() {
12481
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
12482
- const checkSources = () => {
12483
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading a block; retry the request");
12484
- };
12485
12591
  this._fluidRules = await MineRenderData.get("fluids", this.blockState.key?.root);
12486
- checkSources();
12487
12592
  if (this.options.applyDefaultState) {
12488
12593
  this._setState(await BlockStateResolver.defaults(this.blockState));
12489
- checkSources();
12490
12594
  }
12491
12595
  if (this.options.initialState !== void 0) this._setState(this.options.initialState);
12492
12596
  await this.recreateModels();
12493
- checkSources();
12494
12597
  }
12495
12598
  dispose() {
12496
12599
  this.clearModels();
@@ -12668,11 +12771,16 @@ var BlockObject = class _BlockObject extends SceneObject {
12668
12771
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12669
12772
  this._fluidSampler = sample;
12670
12773
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12671
- if (this._fluidKey === surface.key) return;
12774
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12775
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12776
+ if (this._fluidKey === fluidKey) return;
12672
12777
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12673
12778
  const replacement = await this.scene.addSceneObject(
12674
12779
  { key },
12675
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12780
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12781
+ ...this.options,
12782
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12783
+ })
12676
12784
  );
12677
12785
  const previous = this._fluidModel;
12678
12786
  const matrix = previous ? this.getModelMatrix(previous) : new import_three34.Matrix4().makeTranslation(...this.position.toArray());
@@ -12681,7 +12789,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12681
12789
  this._models = this._models.filter((model) => model !== previous);
12682
12790
  this._models.push(replacement);
12683
12791
  this._fluidModel = replacement;
12684
- this._fluidKey = surface.key;
12792
+ this._fluidKey = fluidKey;
12685
12793
  this._isInstanced ||= isInstanceReference(replacement);
12686
12794
  this._instanceCounter = this._isInstanced ? 1 : 0;
12687
12795
  if (previous) this.removeModels([previous]);
@@ -12704,6 +12812,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12704
12812
  }
12705
12813
  async recreateModels() {
12706
12814
  this.clearModels();
12815
+ this.waterTint = this.fluidKind === "water" && this.options.biome && this.options.tints?.[0] === void 0 ? await Biomes.getColor(this.options.biome, "water", this.blockState.key) : void 0;
12707
12816
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12708
12817
  await this.updateFluid(this._fluidSampler);
12709
12818
  return;
@@ -12738,7 +12847,13 @@ var BlockObject = class _BlockObject extends SceneObject {
12738
12847
  const model = await Models.getMerged(modelKey);
12739
12848
  const options = {
12740
12849
  ...this.options,
12741
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12850
+ tints: model ? await BlockTints.get(
12851
+ this.blockState.key,
12852
+ this.state,
12853
+ model,
12854
+ this.options.tints,
12855
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12856
+ ) : this.options.tints,
12742
12857
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12743
12858
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12744
12859
  };
@@ -16391,7 +16506,6 @@ init_util2();
16391
16506
  // src/world/AnvilParser.ts
16392
16507
  init_MineRenderError();
16393
16508
  init_MineRenderData();
16394
- init_AssetLoader();
16395
16509
  init_NBTHelper();
16396
16510
 
16397
16511
  // src/world/ChunkData.ts
@@ -16662,14 +16776,12 @@ var AnvilParser = class {
16662
16776
  /** Decodes all stored chunks. Gzip, zlib, LZ4, and uncompressed payloads are supported. */
16663
16777
  static async parse(data, options = {}) {
16664
16778
  options = { ...options };
16665
- const checkSources = this.assetGuard();
16666
16779
  options.signal?.throwIfAborted();
16667
16780
  const bytes = this.bytes(data);
16668
16781
  const chunks = [];
16669
16782
  for (const location of this.locations(bytes)) {
16670
16783
  chunks.push(await this.abortable(this.readChunk(bytes, location, options), options.signal));
16671
16784
  }
16672
- checkSources();
16673
16785
  return { chunks };
16674
16786
  }
16675
16787
  /**
@@ -16680,22 +16792,12 @@ var AnvilParser = class {
16680
16792
  */
16681
16793
  static async parseChunk(data, localX, localZ, options = {}) {
16682
16794
  options = { ...options };
16683
- const checkSources = this.assetGuard();
16684
16795
  options.signal?.throwIfAborted();
16685
16796
  const bytes = this.bytes(data);
16686
16797
  const location = this.location(bytes, localX, localZ);
16687
16798
  const chunk = location ? await this.abortable(this.readChunk(bytes, location, options), options.signal) : void 0;
16688
- checkSources();
16689
16799
  return chunk;
16690
16800
  }
16691
- static assetGuard() {
16692
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
16693
- return () => {
16694
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) {
16695
- throw new MineRenderError("Asset sources changed while parsing Anvil data; retry the request");
16696
- }
16697
- };
16698
- }
16699
16801
  /**
16700
16802
  * Decodes one chunk from an `entities/r.x.z.mca` file without migrating its NBT.
16701
16803
  * @param localX - Chunk x within the region, from 0 to 31.
@@ -17217,24 +17319,48 @@ var SectionModels = class {
17217
17319
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17218
17320
  }
17219
17321
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17220
- async get(blockState3, properties = {}, position) {
17221
- const variantPosition = position ? [...position] : void 0;
17222
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17322
+ async get(blockState3, properties = {}, position, biome) {
17323
+ const biomePosition = position ? [...position] : void 0;
17324
+ const prepared = await this.state(blockState3, properties);
17325
+ if (!prepared) return void 0;
17326
+ return Promise.all(prepared.groups.map(async (group) => {
17327
+ const variant = BlockStateResolver.choose(group, biomePosition);
17328
+ const choice = prepared.choices.get(variant);
17329
+ const tints = await BlockTints.get(
17330
+ blockState3.key,
17331
+ prepared.state,
17332
+ choice.model,
17333
+ void 0,
17334
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17335
+ );
17336
+ return this.template(variant, choice, tints);
17337
+ }));
17338
+ }
17339
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17340
+ async prepareState(blockState3, properties = {}, biomeColor) {
17341
+ const prepared = await this.state(blockState3, properties);
17342
+ if (!prepared) return void 0;
17343
+ let color2;
17344
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17345
+ const templates = /* @__PURE__ */ new Map();
17346
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17347
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17348
+ templates.set(variant, await this.template(variant, choice, tints));
17349
+ }));
17350
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17223
17351
  }
17224
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17225
- async prepareState(blockState3, properties = {}) {
17352
+ async state(blockState3, properties) {
17226
17353
  let states = this.states.get(blockState3);
17227
17354
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17228
17355
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17229
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17230
- if (!prepared) return void 0;
17231
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17356
+ return cached(states, key, () => this.prepare(blockState3, properties));
17232
17357
  }
17233
17358
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17234
17359
  fluidAtlas(kind, root) {
17235
- return cached(this.fluidAtlases, kind, async () => {
17360
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17361
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17236
17362
  const atlas = await UVMapper.getAtlas({
17237
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17363
+ key,
17238
17364
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17239
17365
  elements: []
17240
17366
  });
@@ -17246,64 +17372,67 @@ var SectionModels = class {
17246
17372
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17247
17373
  const groups = BlockStateResolver.matching(blockState3, state);
17248
17374
  if (!groups.length) return void 0;
17249
- const templates = /* @__PURE__ */ new Map();
17375
+ const prepared = /* @__PURE__ */ new Map();
17250
17376
  for (const group of groups) {
17251
17377
  const choices = Array.isArray(group) ? group : [group];
17252
17378
  if (!choices.length) return void 0;
17253
17379
  for (const variant of choices) {
17254
17380
  if (!variant.model) return void 0;
17255
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17381
+ const modelKey = AssetKey.parse("models", variant.model);
17382
+ modelKey.root = blockState3.key?.root;
17383
+ const model = await Models.getMerged(modelKey);
17256
17384
  if (!model?.elements?.length) return void 0;
17257
17385
  const atlas = await UVMapper.getAtlas(model);
17258
17386
  const rotation = BlockStateResolver.rotation(variant);
17259
17387
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17260
- const tints = await BlockTints.get(blockState3.key, state, model);
17261
- let models = this.models.get(model);
17262
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17263
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17264
- const template = await cached(models, key, async () => {
17265
- const object4 = new SectionModel(model, {
17266
- instanceMeshes: false,
17267
- mergeMeshes: true,
17268
- tints,
17269
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17270
- });
17271
- await object4.init();
17272
- let geometry = object4.children[0].geometry;
17273
- geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17274
- object4.clear();
17275
- const keep = [];
17276
- const cullFaces = [];
17277
- for (const element of atlas.model.elements) {
17278
- for (const face of CUBE_FACES) {
17279
- const data = element.faces[face];
17280
- keep.push(data !== void 0);
17281
- if (!data) continue;
17282
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17283
- if (local < 0) {
17284
- cullFaces.push(0);
17285
- continue;
17286
- }
17287
- const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17288
- const world = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
17289
- cullFaces.push(world < 0 ? 0 : 1 << world);
17290
- }
17291
- }
17292
- geometry = compactQuads(geometry, keep);
17293
- const template2 = {
17294
- geometry,
17295
- atlas,
17296
- cullFaces: new Uint8Array(cullFaces),
17297
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17298
- layer: atlas.hasTranslucency ? 1 : 0
17299
- };
17300
- this.templates.add(template2);
17301
- return template2;
17302
- });
17303
- templates.set(variant, template);
17388
+ prepared.set(variant, { model, atlas, rotation });
17304
17389
  }
17305
17390
  }
17306
- return { groups, templates };
17391
+ return { state, groups, choices: prepared };
17392
+ }
17393
+ async template(variant, { model, atlas, rotation }, tints) {
17394
+ let models = this.models.get(model);
17395
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17396
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17397
+ return cached(models, key, async () => {
17398
+ const object4 = new SectionModel(model, {
17399
+ instanceMeshes: false,
17400
+ mergeMeshes: true,
17401
+ tints,
17402
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17403
+ });
17404
+ await object4.init();
17405
+ let geometry = object4.children[0].geometry;
17406
+ geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17407
+ object4.clear();
17408
+ const keep = [];
17409
+ const cullFaces = [];
17410
+ for (const element of atlas.model.elements) {
17411
+ for (const face of CUBE_FACES) {
17412
+ const data = element.faces[face];
17413
+ keep.push(data !== void 0);
17414
+ if (!data) continue;
17415
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17416
+ if (local < 0) {
17417
+ cullFaces.push(0);
17418
+ continue;
17419
+ }
17420
+ const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17421
+ const world = CUBE_FACE_OFFSETS.findIndex(([x, y, z]) => Math.abs(normal.x - x) < 1e-6 && Math.abs(normal.y - y) < 1e-6 && Math.abs(normal.z - z) < 1e-6);
17422
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17423
+ }
17424
+ }
17425
+ geometry = compactQuads(geometry, keep);
17426
+ const template = {
17427
+ geometry,
17428
+ atlas,
17429
+ cullFaces: new Uint8Array(cullFaces),
17430
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17431
+ layer: atlas.hasTranslucency ? 1 : 0
17432
+ };
17433
+ this.templates.add(template);
17434
+ return template;
17435
+ });
17307
17436
  }
17308
17437
  /** Disposes prepared geometry and clears this world's template caches. */
17309
17438
  clear() {
@@ -17389,7 +17518,6 @@ function resolveSavedEntity(entity) {
17389
17518
 
17390
17519
  // src/world/_entities/WorldEntities.ts
17391
17520
  init_BannerPatterns();
17392
- init_AssetLoader();
17393
17521
  var WorldEntities = class {
17394
17522
  constructor(scene) {
17395
17523
  this.scene = scene;
@@ -17397,10 +17525,6 @@ var WorldEntities = class {
17397
17525
  }
17398
17526
  /** Captures column ownership before asynchronous block or asset loading starts. */
17399
17527
  prepare(entities = [], column) {
17400
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
17401
- const checkSources = () => {
17402
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading saved entities; retry the request");
17403
- };
17404
17528
  const placements = entities.flatMap((entity) => {
17405
17529
  const resolved = resolveSavedEntity(entity);
17406
17530
  if (!resolved) return [];
@@ -17416,17 +17540,13 @@ var WorldEntities = class {
17416
17540
  let object4;
17417
17541
  try {
17418
17542
  if (!current()) return;
17419
- checkSources();
17420
17543
  const models = await Entities.getEntityList();
17421
- checkSources();
17422
17544
  if (!models.includes(placement.key.path) || !current()) return;
17423
17545
  const model = await Entities.getEntity(placement.key, placement.texture, { when: placement.when });
17424
- checkSources();
17425
17546
  if (!model || !current()) return;
17426
17547
  let tints = placement.tints;
17427
17548
  if (placement.woolDye) {
17428
17549
  const dye = (await BannerPatterns.getColors(placement.key.root))[placement.woolDye];
17429
- checkSources();
17430
17550
  const tint = placement.woolDye === "white" ? 15132390 : Math.floor((dye >> 16 & 255) * 0.75) << 16 | Math.floor((dye >> 8 & 255) * 0.75) << 8 | Math.floor((dye & 255) * 0.75);
17431
17551
  tints = { ...tints, wool_color: tint };
17432
17552
  }
@@ -17436,7 +17556,6 @@ var WorldEntities = class {
17436
17556
  object4.position.fromArray(placement.position).multiplyScalar(16).addScalar(-8);
17437
17557
  object4.rotation.y = placement.yaw;
17438
17558
  await object4.init();
17439
- checkSources();
17440
17559
  if (!current()) return;
17441
17560
  placement.owner.add(object4);
17442
17561
  this.scene.add(object4);
@@ -17633,7 +17752,6 @@ var MineRenderWorld = class {
17633
17752
  }
17634
17753
  try {
17635
17754
  await this.clearChunkColumn(chunk.x, chunk.z);
17636
- let sliceStart = performance.now();
17637
17755
  for (const section of chunk.sections) {
17638
17756
  if (section.biomes) {
17639
17757
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17647,6 +17765,9 @@ var MineRenderWorld = class {
17647
17765
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17648
17766
  column.set(section.y, copy);
17649
17767
  }
17768
+ }
17769
+ let sliceStart = performance.now();
17770
+ for (const section of chunk.sections) {
17650
17771
  if (!executor) {
17651
17772
  if (!occupiedSections.has(section.y)) continue;
17652
17773
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17811,18 +17932,34 @@ var MineRenderWorld = class {
17811
17932
  const key = this.worldPosToChunkKey(pos);
17812
17933
  let chunk = this._chunks.get(key);
17813
17934
  if (typeof chunk === "undefined") {
17935
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17814
17936
  chunk = new Chunk(
17815
17937
  this.scene,
17816
- Math.floor(pos.x / 16),
17817
- Math.floor(pos.y / 16),
17818
- Math.floor(pos.z / 16),
17938
+ x,
17939
+ y,
17940
+ z,
17819
17941
  (positions) => this.updateCulling(positions),
17820
17942
  this.sectionModels,
17821
- (x, y, z) => {
17822
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17943
+ (x2, y2, z2) => {
17944
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17823
17945
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17824
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17946
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17825
17947
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17948
+ },
17949
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17950
+ async (biomes) => {
17951
+ if (this._chunks.get(key) !== chunk) return;
17952
+ const columnKey = `${x}_${z}`;
17953
+ let column = this.biomes.get(columnKey);
17954
+ if (biomes) {
17955
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17956
+ column.set(y, biomes);
17957
+ } else {
17958
+ column?.delete(y);
17959
+ if (!column?.size) this.biomes.delete(columnKey);
17960
+ }
17961
+ await chunk.refreshBiomes();
17962
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17826
17963
  }
17827
17964
  );
17828
17965
  this._chunks.set(key, chunk);
@@ -17877,6 +18014,7 @@ function buildSectionGeometry(input) {
17877
18014
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17878
18015
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17879
18016
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
18017
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17880
18018
  for (const template of templateData) {
17881
18019
  const { quads } = template;
17882
18020
  if (template.positions.length !== quads * 12 || template.normals.length !== quads * 12 || template.uvs.length !== quads * 8 || template.uvBounds.length !== quads * 16 || template.colors.length !== quads * 12 || template.indices.length !== quads * 6 || template.cullFaces.length !== quads) {
@@ -17954,7 +18092,9 @@ function buildSectionGeometry(input) {
17954
18092
  height
17955
18093
  });
17956
18094
  }
17957
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
18095
+ const tintIndex = (x + z * 16 + y * 256) * 3;
18096
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
18097
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17958
18098
  }
17959
18099
  pages.push(...[...fluidPages.values()].sort((a, b) => Number(atlases[a.placements[0].atlas].animated) - Number(atlases[b.placements[0].atlas].animated) || a.placements[0].atlas - b.placements[0].atlas));
17960
18100
  }
@@ -18003,7 +18143,7 @@ function buildSectionGeometry(input) {
18003
18143
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
18004
18144
  }
18005
18145
  }
18006
- for (const { quads, x, y, z, data } of page.fluids) {
18146
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
18007
18147
  const atlas = atlases[data.atlas];
18008
18148
  const placement = page.placements[0];
18009
18149
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -18027,7 +18167,7 @@ function buildSectionGeometry(input) {
18027
18167
  mapU((rect[0] + rect[2]) / atlas.width),
18028
18168
  mapV(1 - rect[1] / atlas.height)
18029
18169
  ], vertex * 4);
18030
- colors.set(data.tint, vertex * 3);
18170
+ colors.set(tint, vertex * 3);
18031
18171
  }
18032
18172
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
18033
18173
  indexOffset += 6;
@@ -18169,7 +18309,7 @@ function toInput(entries, maxAtlasSize, fluids) {
18169
18309
  input.cullMasks[i] = cullMask;
18170
18310
  }
18171
18311
  if (fluids) {
18172
- input.fluids = { cells: fluids.cells };
18312
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
18173
18313
  for (const kind of ["water", "lava"]) {
18174
18314
  const atlas = fluids[kind];
18175
18315
  if (!atlas) continue;
@@ -18270,11 +18410,14 @@ var SectionMesh = class _SectionMesh extends import_three43.Group {
18270
18410
  init_FluidGeometry();
18271
18411
  init_FluidQuads();
18272
18412
  init_CubeFace();
18413
+ var WATER_TINT2 = new import_three44.Color(4159204).toArray();
18273
18414
  var Chunk = class _Chunk {
18274
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18415
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18275
18416
  this.onBlocksChanged = onBlocksChanged;
18276
18417
  this.sectionModels = sectionModels;
18277
18418
  this.neighborCell = neighborCell;
18419
+ this.biomeAt = biomeAt;
18420
+ this.onBiomesChanged = onBiomesChanged;
18278
18421
  this.data = new ChunkData();
18279
18422
  this.renderedBlocks = /* @__PURE__ */ new Map();
18280
18423
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18299,6 +18442,56 @@ var Chunk = class _Chunk {
18299
18442
  const index = _Chunk.chunkPosToBlockIndex(pos);
18300
18443
  return this.renderedBlocks.get(index);
18301
18444
  }
18445
+ getBiomeAt(posOrX, y, z) {
18446
+ if (typeof posOrX === "number") return this.getBiomeAt(new import_three44.Vector3(posOrX, y, z));
18447
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new import_three44.Vector3(...posOrX));
18448
+ const pos = this.worldPosToChunkPos(posOrX);
18449
+ _Chunk.chunkPosToBlockIndex(pos);
18450
+ return this.biomeAt ? this.biomeAt(posOrX.x, posOrX.y, posOrX.z) : this.biomes?.[Math.floor(pos.x / 4) + Math.floor(pos.z / 4) * 4 + Math.floor(pos.y / 4) * 16];
18451
+ }
18452
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18453
+ setBiomes(biomes) {
18454
+ const copy = biomes ? [...biomes] : void 0;
18455
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18456
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18457
+ }
18458
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18459
+ this.biomes = copy;
18460
+ return this.refreshBiomes();
18461
+ }
18462
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18463
+ async refreshBiomes() {
18464
+ const affected = /* @__PURE__ */ new Map();
18465
+ const blocks = [];
18466
+ for (const index of this.renderedBlocks.keys()) {
18467
+ const block = this.data.get(index);
18468
+ const stateKey = ChunkData.paletteKey(block);
18469
+ let matches = affected.get(stateKey);
18470
+ if (!matches) {
18471
+ const key = AssetKey.parse("blockstates", block.type);
18472
+ matches = BlockStates.get(key).then(async (state) => {
18473
+ const origin = state?.key ?? key;
18474
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18475
+ return !!source || fluid?.kind === "water";
18476
+ });
18477
+ affected.set(stateKey, matches);
18478
+ }
18479
+ if (await matches) blocks.push({ index, block });
18480
+ }
18481
+ if (!blocks.length) return;
18482
+ const hidden = new Set(this.hiddenBlocks);
18483
+ try {
18484
+ await this.placeBlocks(blocks);
18485
+ } finally {
18486
+ for (const index of hidden) {
18487
+ this.hiddenBlocks.add(index);
18488
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18489
+ }
18490
+ const positions = blocks.map(({ index }) => new import_three44.Vector3(...this.positionAt(index)));
18491
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18492
+ else await this.rebuildSectionMesh();
18493
+ }
18494
+ }
18302
18495
  async setBlockAt(posOrX, yOrBlock, z, block) {
18303
18496
  if (typeof posOrX == "number") {
18304
18497
  return this.setBlockAt(new import_three44.Vector3(posOrX, yOrBlock, z), block);
@@ -18337,14 +18530,15 @@ var Chunk = class _Chunk {
18337
18530
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18338
18531
  } : void 0
18339
18532
  }));
18340
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18341
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18342
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18343
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18344
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18345
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18346
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18347
- }));
18533
+ const states = /* @__PURE__ */ new Map();
18534
+ for (const { index, key, block } of placements) {
18535
+ if (key === void 0) continue;
18536
+ let state = states.get(key);
18537
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18538
+ state.indices.push(index);
18539
+ }
18540
+ const biomes = /* @__PURE__ */ new Map();
18541
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18348
18542
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18349
18543
  const positions = [];
18350
18544
  let failed = false, failure;
@@ -18368,7 +18562,8 @@ var Chunk = class _Chunk {
18368
18562
  if (!readBlock) continue;
18369
18563
  const resolved = resolutions.get(key);
18370
18564
  if (resolved.status === "rejected") throw resolved.reason;
18371
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18565
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18566
+ const context = contexts.get(index);
18372
18567
  if (!blockState3) {
18373
18568
  this.data.assign(index, 0);
18374
18569
  continue;
@@ -18376,9 +18571,10 @@ var Chunk = class _Chunk {
18376
18571
  const variantPosition = worldPos.toArray();
18377
18572
  if (fluid) {
18378
18573
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18574
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18379
18575
  this.meshDirty = true;
18380
18576
  }
18381
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18577
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18382
18578
  if (templates) {
18383
18579
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18384
18580
  this.meshDirty = true;
@@ -18388,7 +18584,9 @@ var Chunk = class _Chunk {
18388
18584
  instanceMeshes: true,
18389
18585
  maxInstanceCount: 2e3,
18390
18586
  initialState: stored.properties,
18391
- variantPosition
18587
+ variantPosition,
18588
+ biome: context?.biome,
18589
+ biomePosition: this.biomePosition(index, blockState3, stored)
18392
18590
  });
18393
18591
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18394
18592
  }
@@ -18411,24 +18609,21 @@ var Chunk = class _Chunk {
18411
18609
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18412
18610
  this.sectionBlocks.clear();
18413
18611
  this.fluidCells.fill(0);
18612
+ this.waterTints = void 0;
18414
18613
  this.renderedBlocks.clear();
18415
18614
  this.hiddenBlocks.clear();
18416
18615
  this.meshDirty = true;
18417
18616
  this.data.copyFrom(data);
18418
- const ids = /* @__PURE__ */ new Set();
18617
+ const ids = /* @__PURE__ */ new Map();
18419
18618
  for (let index = 0; index < 4096; index++) {
18420
18619
  const id = this.data.idAt(index);
18421
- if (id) ids.add(id);
18422
- }
18423
- const palette = [...ids];
18424
- const settled = await Promise.allSettled(palette.map(async (id) => {
18425
- const state = this.data.stateAt(id);
18426
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18427
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18428
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18429
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18430
- return { blockState: blockState3, perBlock, fluid, prepared };
18431
- }));
18620
+ if (!id) continue;
18621
+ let indices = ids.get(id);
18622
+ if (!indices) ids.set(id, indices = []);
18623
+ indices.push(index);
18624
+ }
18625
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18626
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18432
18627
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18433
18628
  const perBlocks = [];
18434
18629
  let failed = false, failure;
@@ -18438,24 +18633,28 @@ var Chunk = class _Chunk {
18438
18633
  try {
18439
18634
  const resolved = resolutions.get(id);
18440
18635
  if (resolved.status === "rejected") throw resolved.reason;
18441
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18636
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18637
+ const context = contexts.get(index);
18442
18638
  if (!blockState3) {
18443
18639
  this.data.assign(index, 0);
18444
18640
  this.fluidCells[index] = 0;
18445
18641
  continue;
18446
18642
  }
18447
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18643
+ if (fluid) {
18644
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18645
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18646
+ }
18448
18647
  const variantPosition = [
18449
18648
  this.x * 16 + index % 16,
18450
18649
  this.y * 16 + Math.floor(index / 256),
18451
18650
  this.z * 16 + Math.floor(index / 16) % 16
18452
18651
  ];
18453
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18652
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18454
18653
  if (templates) {
18455
18654
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18456
18655
  this.meshDirty = true;
18457
18656
  } else {
18458
- perBlocks.push({ index, blockState: blockState3 });
18657
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18459
18658
  }
18460
18659
  const readBlock = this.data.snapshot(index);
18461
18660
  this.renderedBlocks.set(index, { get block() {
@@ -18468,7 +18667,7 @@ var Chunk = class _Chunk {
18468
18667
  failed = true;
18469
18668
  }
18470
18669
  }
18471
- for (const { index, blockState: blockState3 } of perBlocks) {
18670
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18472
18671
  let object4;
18473
18672
  try {
18474
18673
  object4 = await this.scene.addBlock(blockState3, {
@@ -18476,11 +18675,9 @@ var Chunk = class _Chunk {
18476
18675
  instanceMeshes: true,
18477
18676
  maxInstanceCount: 2e3,
18478
18677
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18479
- variantPosition: [
18480
- this.x * 16 + index % 16,
18481
- this.y * 16 + Math.floor(index / 256),
18482
- this.z * 16 + Math.floor(index / 16) % 16
18483
- ]
18678
+ variantPosition: this.positionAt(index),
18679
+ biome,
18680
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18484
18681
  });
18485
18682
  object4.setPosition(new import_three44.Vector3(
18486
18683
  this.x * 256 + index % 16 * 16,
@@ -18499,6 +18696,68 @@ var Chunk = class _Chunk {
18499
18696
  }
18500
18697
  if (failed) throw failure;
18501
18698
  }
18699
+ positionAt(index) {
18700
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18701
+ }
18702
+ biomePosition(index, blockState3, stored) {
18703
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18704
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18705
+ return position;
18706
+ }
18707
+ async resolveState(stored, indices, biomes) {
18708
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18709
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18710
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18711
+ const hasBiomes = this.getBiomeAt(this.x * 16, this.y * 16, this.z * 16) !== void 0 || this.biomeAt?.(this.x * 16, this.y * 16 - 1, this.z * 16) !== void 0;
18712
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18713
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18714
+ const contexts = /* @__PURE__ */ new Map();
18715
+ const groups = /* @__PURE__ */ new Map();
18716
+ if (blockState3 && (source || fluid?.kind === "water")) {
18717
+ for (const index of indices) {
18718
+ const position = this.biomePosition(index, blockState3, stored);
18719
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18720
+ if (!biome) continue;
18721
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18722
+ let group = groups.get(key);
18723
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18724
+ group.indices.push(index);
18725
+ }
18726
+ }
18727
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18728
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18729
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18730
+ const available = () => {
18731
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18732
+ let found = biomes.get(key);
18733
+ if (!found) {
18734
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18735
+ biomes.set(key, found);
18736
+ }
18737
+ return found;
18738
+ };
18739
+ const context = { biome };
18740
+ if (perBlock) {
18741
+ if (!await available()) context.biome = void 0;
18742
+ } else {
18743
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18744
+ if (fluid?.kind === "water" && await available()) {
18745
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18746
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new import_three44.Color(color2).toArray();
18747
+ }
18748
+ }
18749
+ for (const index of indices2) contexts.set(index, context);
18750
+ }));
18751
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18752
+ }
18753
+ setWaterTint(index, tint) {
18754
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18755
+ if (!this.waterTints) {
18756
+ this.waterTints = new Float32Array(4096 * 3);
18757
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18758
+ }
18759
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18760
+ }
18502
18761
  /** Changes visibility at world block coordinates while preserving the block's data. */
18503
18762
  async setBlockVisibleAt(pos, visible) {
18504
18763
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18602,7 +18861,7 @@ var Chunk = class _Chunk {
18602
18861
  if (block && !block.object) origins.set(kind, block.block.type);
18603
18862
  }
18604
18863
  if (origins.size) {
18605
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18864
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18606
18865
  for (let y = -1; y <= 16; y++) {
18607
18866
  for (let z = -1; z <= 16; z++) {
18608
18867
  for (let x = -1; x <= 16; x++) {
@@ -18644,8 +18903,13 @@ var Chunk = class _Chunk {
18644
18903
  this.renderedBlocks.clear();
18645
18904
  this.hiddenBlocks.clear();
18646
18905
  this.fluidCells.fill(0);
18906
+ this.waterTints = void 0;
18647
18907
  this.data.clear();
18648
- if (notify) await this.onBlocksChanged?.(positions);
18908
+ this.biomes = void 0;
18909
+ if (notify) {
18910
+ await this.onBiomesChanged?.(void 0);
18911
+ await this.onBlocksChanged?.(positions);
18912
+ }
18649
18913
  }
18650
18914
  async dispose() {
18651
18915
  await this.clear();
@@ -19031,6 +19295,7 @@ var NodeRenderer = class _NodeRenderer extends Renderer {
19031
19295
  BannerPatterns,
19032
19296
  BasicAssetKey,
19033
19297
  BatchedExecutor,
19298
+ Biomes,
19034
19299
  BlockEntities,
19035
19300
  BlockObject,
19036
19301
  BlockStateResolver,