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.
@@ -323,6 +323,9 @@ var init_Caching = __esm({
323
323
  static {
324
324
  this.usernameUuidCache = Caches.builder().expireAfterWrite(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
325
325
  }
326
+ static {
327
+ this.biomeCache = _Caching.createAssetCache();
328
+ }
326
329
  static createAssetCache() {
327
330
  return Caches.builder().expireAfterWrite(Time.minutes(10)).expireAfterAccess(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
328
331
  }
@@ -352,7 +355,8 @@ var init_Caching = __esm({
352
355
  this.fontCache,
353
356
  this.bannerPatternCache,
354
357
  this.mineRenderDataCache,
355
- this.usernameUuidCache
358
+ this.usernameUuidCache,
359
+ this.biomeCache
356
360
  ];
357
361
  }
358
362
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -1658,7 +1662,8 @@ var init_MineRenderData = __esm({
1658
1662
  }
1659
1663
  /**
1660
1664
  * Uses the exact version when published. Otherwise, selects the nearest patch in the same
1661
- * release line (older on ties), then the latest earlier release. Unpublished snapshots reject.
1665
+ * release line (older on ties), then the latest earlier release or the oldest published release.
1666
+ * Unpublished snapshots reject.
1662
1667
  */
1663
1668
  static async resolve(root) {
1664
1669
  const requestRoot = root?.replace(/\/+$/, "");
@@ -1667,11 +1672,11 @@ var init_MineRenderData = __esm({
1667
1672
  const scope = AssetLoader.persistentScope;
1668
1673
  return this.cached(`resolve:${requestRoot === void 0 ? "sources" : "explicit"}:${root}`, scope, defaultRoot, async () => {
1669
1674
  const requestedVersion = root.slice(root.lastIndexOf("/") + 1);
1670
- let manifest = await this.manifest(root, release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1675
+ let manifest = await this.manifest(root, requestRoot !== void 0 && release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1671
1676
  if (manifest) return { requestedVersion, version: manifest.minecraftVersion, root, manifest };
1672
1677
  const requested = release(requestedVersion);
1673
- if (!requested || compare(requested, [1, 16, 5]) < 0) {
1674
- throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release from 1.16.5 onward`);
1678
+ if (!requested) {
1679
+ throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release`);
1675
1680
  }
1676
1681
  const base = root.slice(0, root.lastIndexOf("/"));
1677
1682
  const index = await this.cached(`versions:${base}`, scope, defaultRoot, async () => {
@@ -1681,9 +1686,9 @@ var init_MineRenderData = __esm({
1681
1686
  }
1682
1687
  return value.versions;
1683
1688
  });
1684
- 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));
1689
+ const versions = index.map((entry) => ({ name: entry.name, number: release(entry.name) })).filter((entry) => !!entry.number).sort((left, right) => compare(left.number, right.number));
1685
1690
  const sameLine = versions.filter((entry) => entry.number[0] === requested[0] && entry.number[1] === requested[1]);
1686
- 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();
1691
+ 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];
1687
1692
  if (!selected) throw new Error(`No compatible MineRender data is published for ${requestedVersion}`);
1688
1693
  const resolvedRoot = `${base}/${selected.name}`;
1689
1694
  manifest = await this.manifest(resolvedRoot, selected.name, resolvedRoot);
@@ -1730,7 +1735,7 @@ ${key}` : key, async () => {
1730
1735
  case "blockStates":
1731
1736
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1732
1737
  case "blockTints":
1733
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1738
+ 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));
1734
1739
  case "fluids":
1735
1740
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1736
1741
  case "dyes":
@@ -1799,12 +1804,7 @@ var init_BannerPatterns = __esm({
1799
1804
  static async getLayers(value, target, root) {
1800
1805
  if (value === void 0) return [];
1801
1806
  if (!Array.isArray(value)) throw new Error("banner_patterns must be an array");
1802
- const activeRoot = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
1803
- const checkSources = () => {
1804
- if (activeRoot !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading banner patterns; retry the request");
1805
- };
1806
1807
  const colors = value.length ? await this.getColors(root) : void 0;
1807
- checkSources();
1808
1808
  return Promise.all(value.slice(0, 16).map(async (layer) => {
1809
1809
  if (!layer || typeof layer !== "object" || Array.isArray(layer)) throw new Error("Each banner pattern requires a pattern and dye color");
1810
1810
  if (typeof layer.color !== "string" || !Object.prototype.hasOwnProperty.call(colors, layer.color)) throw new Error(`Unsupported dye color ${layer.color}`);
@@ -1814,7 +1814,6 @@ var init_BannerPatterns = __esm({
1814
1814
  const id = this.identifier(pattern2);
1815
1815
  const key = new AssetKey(id.namespace, id.getFullPath(), "banner_pattern", void 0, "data", ".json", root);
1816
1816
  pattern2 = await Caching.bannerPatternCache.get(key.serialize(), () => AssetLoader.get(key, "json" /* JSON */));
1817
- checkSources();
1818
1817
  if (!pattern2) throw new Error(`Missing banner pattern ${layer.pattern}`);
1819
1818
  }
1820
1819
  if (!pattern2 || typeof pattern2 !== "object" || Array.isArray(pattern2) || typeof pattern2.translation_key !== "string") throw new Error("Banner patterns require asset_id and translation_key");
@@ -1874,15 +1873,19 @@ var init_Colormaps = __esm({
1874
1873
  Colormaps = class {
1875
1874
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1876
1875
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1877
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1876
+ return this.getColor("grass", origin, temperature, downfall);
1877
+ }
1878
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1879
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1880
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1878
1881
  const temp = Math.fround(temperature);
1879
1882
  const rain = Math.fround(downfall) * temp;
1880
1883
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1881
1884
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1882
1885
  const image = await ModelTextures.get(key);
1883
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1886
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1884
1887
  if (image.width !== 256 || image.height !== 256) {
1885
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1888
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1886
1889
  }
1887
1890
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1888
1891
  return red << 16 | green << 8 | blue;
@@ -2099,30 +2102,24 @@ var init_Models = __esm({
2099
2102
  }).then((r) => r?.files ?? []);
2100
2103
  }
2101
2104
  static async loadAndMerge(key, context = {}) {
2102
- const checkSources = this.sourceCheck();
2103
2105
  const model = key.type === "item" ? await this.getItemModel(key, context) : await this.getRaw(key);
2104
2106
  if (!model) {
2105
2107
  return void 0;
2106
2108
  }
2107
2109
  const merged = await ModelMerger.mergeWithParents(model);
2108
- checkSources();
2109
2110
  return merged;
2110
2111
  }
2111
2112
  static async getItemModel(key, context = {}) {
2112
- const checkSources = this.sourceCheck();
2113
2113
  const preview = this.snapshotContext(key, context);
2114
2114
  const dataKey = await this.applyDefaults(key, preview);
2115
- checkSources();
2116
2115
  return this.getPreparedItemModel(key, preview, dataKey);
2117
2116
  }
2118
2117
  static async getPreparedItemModel(key, preview, dataKey) {
2119
- const checkSources = this.sourceCheck();
2120
2118
  const itemId = `${key.namespace}:${key.path}`;
2121
2119
  const itemKey2 = new AssetKey(key.namespace, key.path, "items", void 0, key.rootType, ".json", key.root);
2122
2120
  const cacheKey = itemKey2.serialize() + this.contextKey(preview) + dataKey;
2123
2121
  const model = await this.PERSISTENT_CACHE.getOrLoad(`${this.ITEM_CACHE_VERSION}:${AssetLoader.persistentKey(cacheKey)}`, async () => {
2124
2122
  const result = await AssetLoader.getFirst([itemKey2, key], "json" /* JSON */);
2125
- checkSources();
2126
2123
  if (!result) return void 0;
2127
2124
  if (result.key.assetType !== "items") return { ...result.asset, key, itemId, components: preview.components };
2128
2125
  const load = async (selected) => {
@@ -2141,10 +2138,8 @@ var init_Models = __esm({
2141
2138
  return { ...model3, itemId, components: preview.components, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
2142
2139
  };
2143
2140
  const model2 = await load(this.selectItemModel(result.asset.model, key, preview));
2144
- checkSources();
2145
2141
  return model2;
2146
2142
  });
2147
- checkSources();
2148
2143
  const restore = (model2) => ({
2149
2144
  ...model2,
2150
2145
  key: Object.assign(new AssetKey("", ""), model2.key),
@@ -2208,22 +2203,14 @@ var init_Models = __esm({
2208
2203
  }
2209
2204
  static async applyDefaults(key, preview) {
2210
2205
  if (key.type !== "item") return "";
2211
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2212
2206
  const resolved = await MineRenderData.resolve(key.root);
2213
2207
  const defaults = await ItemDefaults.get(`${key.namespace}:${key.path}`, key.root);
2214
2208
  preview.components = { ...defaults, ...preview.components };
2215
2209
  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)) {
2216
2210
  preview.components["minecraft:enchantment_glint_override"] = true;
2217
2211
  }
2218
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item defaults; retry the request");
2219
2212
  return `|data:${resolved.root}:${resolved.manifest.datasets.itemDefaults?.sha256}:${resolved.manifest.datasets.potionColors?.sha256}`;
2220
2213
  }
2221
- static sourceCheck() {
2222
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2223
- return () => {
2224
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item models; retry the request");
2225
- };
2226
- }
2227
2214
  static contextKey(context) {
2228
2215
  const itemReferences = Object.fromEntries(Object.entries(context.itemReferences).map(([id, key]) => [id, key.serialize()]));
2229
2216
  return `|${this.ITEM_CACHE_VERSION}:${JSON.stringify(this.snapshotJson({ ...context, itemReferences }))}`;
@@ -2410,7 +2397,6 @@ var init_Models = __esm({
2410
2397
  * @param key - Model key, for example `AssetKey.parse("models", "minecraft:item/diamond_sword")`.
2411
2398
  */
2412
2399
  static async getMerged(key, context = {}) {
2413
- const checkSources = this.sourceCheck();
2414
2400
  if (!key.assetType) {
2415
2401
  key.assetType = "models";
2416
2402
  }
@@ -2419,15 +2405,12 @@ var init_Models = __esm({
2419
2405
  }
2420
2406
  const preview = this.snapshotContext(key, context);
2421
2407
  const dataKey = await this.applyDefaults(key, preview);
2422
- checkSources();
2423
2408
  const keyStr = AssetLoader.persistentKey(key.serialize() + (key.type === "item" ? this.contextKey(preview) + dataKey : ""));
2424
2409
  const model = await Caching.mergedModelCache.get(keyStr, async () => {
2425
2410
  const raw = key.type === "item" ? await this.getPreparedItemModel(key, preview, dataKey) : await this.getRaw(key);
2426
2411
  const merged = raw ? await ModelMerger.mergeWithParents(raw) : void 0;
2427
- checkSources();
2428
2412
  return merged;
2429
2413
  });
2430
- checkSources();
2431
2414
  return model;
2432
2415
  }
2433
2416
  /** Alias for {@link getMerged}. */
@@ -7751,6 +7734,116 @@ init_AssetLoader();
7751
7734
  init_Assets();
7752
7735
  init_BannerPatterns();
7753
7736
 
7737
+ // src/assets/Biomes.ts
7738
+ init_AssetKey();
7739
+ init_AssetLoader();
7740
+ init_AssetParsers();
7741
+ init_Caching();
7742
+ init_MineRenderError();
7743
+ init_Colormaps();
7744
+
7745
+ // src/assets/_biomes/SwampGrassColor.ts
7746
+ var permutation;
7747
+ 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]];
7748
+ var skew = (Math.sqrt(3) - 1) / 2;
7749
+ var unskew = (3 - Math.sqrt(3)) / 6;
7750
+ function createPermutation() {
7751
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7752
+ let seed = 2345n ^ multiplier;
7753
+ const next = (bits) => {
7754
+ seed = seed * multiplier + 11n & mask;
7755
+ return Number(seed >> BigInt(48 - bits));
7756
+ };
7757
+ for (let i = 0; i < 3; i++) {
7758
+ next(26);
7759
+ next(27);
7760
+ }
7761
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7762
+ for (let i = 0; i < 256; i++) {
7763
+ const bound = 256 - i;
7764
+ let offset;
7765
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7766
+ else {
7767
+ let bits;
7768
+ do {
7769
+ bits = next(31);
7770
+ offset = bits % bound;
7771
+ } while (bits - offset + bound - 1 > 2147483647);
7772
+ }
7773
+ const other = i + offset;
7774
+ [values[i], values[other]] = [values[other], values[i]];
7775
+ }
7776
+ return values;
7777
+ }
7778
+ function swampGrassColor(x, z) {
7779
+ const p10 = permutation ??= createPermutation();
7780
+ x *= 0.0225;
7781
+ z *= 0.0225;
7782
+ const s = (x + z) * skew;
7783
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7784
+ const t = (i + j) * unskew;
7785
+ const dx = x - (i - t), dz = z - (j - t);
7786
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7787
+ const corner = (gx, gz, a, b) => {
7788
+ let falloff = 0.5 - a * a - b * b;
7789
+ if (falloff < 0) return 0;
7790
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7791
+ falloff *= falloff;
7792
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7793
+ };
7794
+ 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));
7795
+ return noise < -0.1 ? 5011004 : 6975545;
7796
+ }
7797
+
7798
+ // src/assets/Biomes.ts
7799
+ var Biomes = class {
7800
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7801
+ static async get(id, origin) {
7802
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7803
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7804
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7805
+ return Caching.biomeCache.get(key.serialize(), async () => {
7806
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7807
+ if (asset2 === void 0) return void 0;
7808
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7809
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7810
+ }
7811
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7812
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7813
+ }
7814
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7815
+ const color2 = asset2.effects[`${kind}_color`];
7816
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7817
+ }
7818
+ const modifier = asset2.effects.grass_color_modifier;
7819
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7820
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7821
+ }
7822
+ return asset2;
7823
+ });
7824
+ }
7825
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7826
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7827
+ const biome = await this.get(id, origin);
7828
+ if (!biome) return void 0;
7829
+ const rawColor = biome.effects[`${kind}_color`];
7830
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7831
+ if (kind === "water") return color2;
7832
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7833
+ if (modifier === "swamp") return swampGrassColor(x, z);
7834
+ if (color2 === void 0) {
7835
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7836
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7837
+ }
7838
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7839
+ }
7840
+ static parseColor(value, id, kind) {
7841
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7842
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7843
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7844
+ }
7845
+ };
7846
+
7754
7847
  // src/assets/BlockEntities.ts
7755
7848
  init_AssetKey();
7756
7849
  init_AssetLoader();
@@ -12230,8 +12323,20 @@ var BlockStateResolver = class _BlockStateResolver {
12230
12323
  init_Colormaps();
12231
12324
  init_MineRenderData();
12232
12325
  var BlockTints = class {
12233
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12234
- static async get(key, state, model, tints) {
12326
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12327
+ static async biomeSource(key) {
12328
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12329
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12330
+ }
12331
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12332
+ static async getBiomeColor(key, biome, position) {
12333
+ if (!biome) return void 0;
12334
+ const x = position?.[0], z = position?.[2];
12335
+ const source = await this.biomeSource(key);
12336
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12337
+ }
12338
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12339
+ static async get(key, state, model, tints, biomeColor) {
12235
12340
  if (!key) return tints;
12236
12341
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12237
12342
  if (!candidates) return tints;
@@ -12247,8 +12352,12 @@ var BlockTints = class {
12247
12352
  }
12248
12353
  }
12249
12354
  if (!indices.size) return tints;
12355
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12356
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12250
12357
  let color2;
12251
- switch (rule.source) {
12358
+ if (biome !== void 0) {
12359
+ color2 = biome;
12360
+ } else switch (rule.source) {
12252
12361
  case "constant":
12253
12362
  color2 = rule.color;
12254
12363
  break;
@@ -12285,7 +12394,6 @@ import { Euler as Euler10, Matrix4 as Matrix411, Quaternion as Quaternion5, Vect
12285
12394
  init_ModelCulling();
12286
12395
  init_FluidGeometry();
12287
12396
  init_MineRenderData();
12288
- init_AssetLoader();
12289
12397
  init_Entities();
12290
12398
  var BlockObject = class _BlockObject extends SceneObject {
12291
12399
  constructor(blockState3, options) {
@@ -12307,6 +12415,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12307
12415
  this._entityGeneration = 0;
12308
12416
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12309
12417
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12418
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12310
12419
  }
12311
12420
  static {
12312
12421
  this.DEFAULT_OPTIONS = (0, import_ts_deepmerge6.default)({}, ModelObject.DEFAULT_OPTIONS, {
@@ -12314,19 +12423,12 @@ var BlockObject = class _BlockObject extends SceneObject {
12314
12423
  });
12315
12424
  }
12316
12425
  async init() {
12317
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
12318
- const checkSources = () => {
12319
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading a block; retry the request");
12320
- };
12321
12426
  this._fluidRules = await MineRenderData.get("fluids", this.blockState.key?.root);
12322
- checkSources();
12323
12427
  if (this.options.applyDefaultState) {
12324
12428
  this._setState(await BlockStateResolver.defaults(this.blockState));
12325
- checkSources();
12326
12429
  }
12327
12430
  if (this.options.initialState !== void 0) this._setState(this.options.initialState);
12328
12431
  await this.recreateModels();
12329
- checkSources();
12330
12432
  }
12331
12433
  dispose() {
12332
12434
  this.clearModels();
@@ -12504,11 +12606,16 @@ var BlockObject = class _BlockObject extends SceneObject {
12504
12606
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12505
12607
  this._fluidSampler = sample;
12506
12608
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12507
- if (this._fluidKey === surface.key) return;
12609
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12610
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12611
+ if (this._fluidKey === fluidKey) return;
12508
12612
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12509
12613
  const replacement = await this.scene.addSceneObject(
12510
12614
  { key },
12511
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12615
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12616
+ ...this.options,
12617
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12618
+ })
12512
12619
  );
12513
12620
  const previous = this._fluidModel;
12514
12621
  const matrix = previous ? this.getModelMatrix(previous) : new Matrix411().makeTranslation(...this.position.toArray());
@@ -12517,7 +12624,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12517
12624
  this._models = this._models.filter((model) => model !== previous);
12518
12625
  this._models.push(replacement);
12519
12626
  this._fluidModel = replacement;
12520
- this._fluidKey = surface.key;
12627
+ this._fluidKey = fluidKey;
12521
12628
  this._isInstanced ||= isInstanceReference(replacement);
12522
12629
  this._instanceCounter = this._isInstanced ? 1 : 0;
12523
12630
  if (previous) this.removeModels([previous]);
@@ -12540,6 +12647,7 @@ var BlockObject = class _BlockObject extends SceneObject {
12540
12647
  }
12541
12648
  async recreateModels() {
12542
12649
  this.clearModels();
12650
+ 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;
12543
12651
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12544
12652
  await this.updateFluid(this._fluidSampler);
12545
12653
  return;
@@ -12574,7 +12682,13 @@ var BlockObject = class _BlockObject extends SceneObject {
12574
12682
  const model = await Models.getMerged(modelKey);
12575
12683
  const options = {
12576
12684
  ...this.options,
12577
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12685
+ tints: model ? await BlockTints.get(
12686
+ this.blockState.key,
12687
+ this.state,
12688
+ model,
12689
+ this.options.tints,
12690
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12691
+ ) : this.options.tints,
12578
12692
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12579
12693
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12580
12694
  };
@@ -16235,7 +16349,6 @@ init_util2();
16235
16349
  // src/world/AnvilParser.ts
16236
16350
  init_MineRenderError();
16237
16351
  init_MineRenderData();
16238
- init_AssetLoader();
16239
16352
  init_NBTHelper();
16240
16353
 
16241
16354
  // src/world/ChunkData.ts
@@ -16506,14 +16619,12 @@ var AnvilParser = class {
16506
16619
  /** Decodes all stored chunks. Gzip, zlib, LZ4, and uncompressed payloads are supported. */
16507
16620
  static async parse(data, options = {}) {
16508
16621
  options = { ...options };
16509
- const checkSources = this.assetGuard();
16510
16622
  options.signal?.throwIfAborted();
16511
16623
  const bytes = this.bytes(data);
16512
16624
  const chunks = [];
16513
16625
  for (const location of this.locations(bytes)) {
16514
16626
  chunks.push(await this.abortable(this.readChunk(bytes, location, options), options.signal));
16515
16627
  }
16516
- checkSources();
16517
16628
  return { chunks };
16518
16629
  }
16519
16630
  /**
@@ -16524,22 +16635,12 @@ var AnvilParser = class {
16524
16635
  */
16525
16636
  static async parseChunk(data, localX, localZ, options = {}) {
16526
16637
  options = { ...options };
16527
- const checkSources = this.assetGuard();
16528
16638
  options.signal?.throwIfAborted();
16529
16639
  const bytes = this.bytes(data);
16530
16640
  const location = this.location(bytes, localX, localZ);
16531
16641
  const chunk = location ? await this.abortable(this.readChunk(bytes, location, options), options.signal) : void 0;
16532
- checkSources();
16533
16642
  return chunk;
16534
16643
  }
16535
- static assetGuard() {
16536
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
16537
- return () => {
16538
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) {
16539
- throw new MineRenderError("Asset sources changed while parsing Anvil data; retry the request");
16540
- }
16541
- };
16542
- }
16543
16644
  /**
16544
16645
  * Decodes one chunk from an `entities/r.x.z.mca` file without migrating its NBT.
16545
16646
  * @param localX - Chunk x within the region, from 0 to 31.
@@ -16991,7 +17092,7 @@ var AnvilWorldSource = class {
16991
17092
  };
16992
17093
 
16993
17094
  // src/world/Chunk.ts
16994
- import { Vector3 as Vector318 } from "three";
17095
+ import { Color as Color13, Vector3 as Vector318 } from "three";
16995
17096
  init_BlockStates();
16996
17097
  init_AssetKey();
16997
17098
 
@@ -17061,24 +17162,48 @@ var SectionModels = class {
17061
17162
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17062
17163
  }
17063
17164
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17064
- async get(blockState3, properties = {}, position) {
17065
- const variantPosition = position ? [...position] : void 0;
17066
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17165
+ async get(blockState3, properties = {}, position, biome) {
17166
+ const biomePosition = position ? [...position] : void 0;
17167
+ const prepared = await this.state(blockState3, properties);
17168
+ if (!prepared) return void 0;
17169
+ return Promise.all(prepared.groups.map(async (group) => {
17170
+ const variant = BlockStateResolver.choose(group, biomePosition);
17171
+ const choice = prepared.choices.get(variant);
17172
+ const tints = await BlockTints.get(
17173
+ blockState3.key,
17174
+ prepared.state,
17175
+ choice.model,
17176
+ void 0,
17177
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17178
+ );
17179
+ return this.template(variant, choice, tints);
17180
+ }));
17181
+ }
17182
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17183
+ async prepareState(blockState3, properties = {}, biomeColor) {
17184
+ const prepared = await this.state(blockState3, properties);
17185
+ if (!prepared) return void 0;
17186
+ let color2;
17187
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ??= biomeColor() : biomeColor;
17188
+ const templates = /* @__PURE__ */ new Map();
17189
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17190
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17191
+ templates.set(variant, await this.template(variant, choice, tints));
17192
+ }));
17193
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17067
17194
  }
17068
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17069
- async prepareState(blockState3, properties = {}) {
17195
+ async state(blockState3, properties) {
17070
17196
  let states = this.states.get(blockState3);
17071
17197
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17072
17198
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17073
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17074
- if (!prepared) return void 0;
17075
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17199
+ return cached(states, key, () => this.prepare(blockState3, properties));
17076
17200
  }
17077
17201
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17078
17202
  fluidAtlas(kind, root) {
17079
- return cached(this.fluidAtlases, kind, async () => {
17203
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17204
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17080
17205
  const atlas = await UVMapper.getAtlas({
17081
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17206
+ key,
17082
17207
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17083
17208
  elements: []
17084
17209
  });
@@ -17090,64 +17215,67 @@ var SectionModels = class {
17090
17215
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17091
17216
  const groups = BlockStateResolver.matching(blockState3, state);
17092
17217
  if (!groups.length) return void 0;
17093
- const templates = /* @__PURE__ */ new Map();
17218
+ const prepared = /* @__PURE__ */ new Map();
17094
17219
  for (const group of groups) {
17095
17220
  const choices = Array.isArray(group) ? group : [group];
17096
17221
  if (!choices.length) return void 0;
17097
17222
  for (const variant of choices) {
17098
17223
  if (!variant.model) return void 0;
17099
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17224
+ const modelKey = AssetKey.parse("models", variant.model);
17225
+ modelKey.root = blockState3.key?.root;
17226
+ const model = await Models.getMerged(modelKey);
17100
17227
  if (!model?.elements?.length) return void 0;
17101
17228
  const atlas = await UVMapper.getAtlas(model);
17102
17229
  const rotation = BlockStateResolver.rotation(variant);
17103
17230
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17104
- const tints = await BlockTints.get(blockState3.key, state, model);
17105
- let models = this.models.get(model);
17106
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17107
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17108
- const template = await cached(models, key, async () => {
17109
- const object4 = new SectionModel(model, {
17110
- instanceMeshes: false,
17111
- mergeMeshes: true,
17112
- tints,
17113
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17114
- });
17115
- await object4.init();
17116
- let geometry = object4.children[0].geometry;
17117
- geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17118
- object4.clear();
17119
- const keep = [];
17120
- const cullFaces = [];
17121
- for (const element of atlas.model.elements) {
17122
- for (const face of CUBE_FACES) {
17123
- const data = element.faces[face];
17124
- keep.push(data !== void 0);
17125
- if (!data) continue;
17126
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17127
- if (local < 0) {
17128
- cullFaces.push(0);
17129
- continue;
17130
- }
17131
- const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17132
- 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);
17133
- cullFaces.push(world < 0 ? 0 : 1 << world);
17134
- }
17135
- }
17136
- geometry = compactQuads(geometry, keep);
17137
- const template2 = {
17138
- geometry,
17139
- atlas,
17140
- cullFaces: new Uint8Array(cullFaces),
17141
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17142
- layer: atlas.hasTranslucency ? 1 : 0
17143
- };
17144
- this.templates.add(template2);
17145
- return template2;
17146
- });
17147
- templates.set(variant, template);
17231
+ prepared.set(variant, { model, atlas, rotation });
17148
17232
  }
17149
17233
  }
17150
- return { groups, templates };
17234
+ return { state, groups, choices: prepared };
17235
+ }
17236
+ async template(variant, { model, atlas, rotation }, tints) {
17237
+ let models = this.models.get(model);
17238
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17239
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17240
+ return cached(models, key, async () => {
17241
+ const object4 = new SectionModel(model, {
17242
+ instanceMeshes: false,
17243
+ mergeMeshes: true,
17244
+ tints,
17245
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17246
+ });
17247
+ await object4.init();
17248
+ let geometry = object4.children[0].geometry;
17249
+ geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17250
+ object4.clear();
17251
+ const keep = [];
17252
+ const cullFaces = [];
17253
+ for (const element of atlas.model.elements) {
17254
+ for (const face of CUBE_FACES) {
17255
+ const data = element.faces[face];
17256
+ keep.push(data !== void 0);
17257
+ if (!data) continue;
17258
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17259
+ if (local < 0) {
17260
+ cullFaces.push(0);
17261
+ continue;
17262
+ }
17263
+ const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17264
+ 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);
17265
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17266
+ }
17267
+ }
17268
+ geometry = compactQuads(geometry, keep);
17269
+ const template = {
17270
+ geometry,
17271
+ atlas,
17272
+ cullFaces: new Uint8Array(cullFaces),
17273
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17274
+ layer: atlas.hasTranslucency ? 1 : 0
17275
+ };
17276
+ this.templates.add(template);
17277
+ return template;
17278
+ });
17151
17279
  }
17152
17280
  /** Disposes prepared geometry and clears this world's template caches. */
17153
17281
  clear() {
@@ -17233,7 +17361,6 @@ function resolveSavedEntity(entity) {
17233
17361
 
17234
17362
  // src/world/_entities/WorldEntities.ts
17235
17363
  init_BannerPatterns();
17236
- init_AssetLoader();
17237
17364
  var WorldEntities = class {
17238
17365
  constructor(scene) {
17239
17366
  this.scene = scene;
@@ -17241,10 +17368,6 @@ var WorldEntities = class {
17241
17368
  }
17242
17369
  /** Captures column ownership before asynchronous block or asset loading starts. */
17243
17370
  prepare(entities = [], column) {
17244
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
17245
- const checkSources = () => {
17246
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading saved entities; retry the request");
17247
- };
17248
17371
  const placements = entities.flatMap((entity) => {
17249
17372
  const resolved = resolveSavedEntity(entity);
17250
17373
  if (!resolved) return [];
@@ -17260,17 +17383,13 @@ var WorldEntities = class {
17260
17383
  let object4;
17261
17384
  try {
17262
17385
  if (!current()) return;
17263
- checkSources();
17264
17386
  const models = await Entities.getEntityList();
17265
- checkSources();
17266
17387
  if (!models.includes(placement.key.path) || !current()) return;
17267
17388
  const model = await Entities.getEntity(placement.key, placement.texture, { when: placement.when });
17268
- checkSources();
17269
17389
  if (!model || !current()) return;
17270
17390
  let tints = placement.tints;
17271
17391
  if (placement.woolDye) {
17272
17392
  const dye = (await BannerPatterns.getColors(placement.key.root))[placement.woolDye];
17273
- checkSources();
17274
17393
  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);
17275
17394
  tints = { ...tints, wool_color: tint };
17276
17395
  }
@@ -17280,7 +17399,6 @@ var WorldEntities = class {
17280
17399
  object4.position.fromArray(placement.position).multiplyScalar(16).addScalar(-8);
17281
17400
  object4.rotation.y = placement.yaw;
17282
17401
  await object4.init();
17283
- checkSources();
17284
17402
  if (!current()) return;
17285
17403
  placement.owner.add(object4);
17286
17404
  this.scene.add(object4);
@@ -17477,7 +17595,6 @@ var MineRenderWorld = class {
17477
17595
  }
17478
17596
  try {
17479
17597
  await this.clearChunkColumn(chunk.x, chunk.z);
17480
- let sliceStart = performance.now();
17481
17598
  for (const section of chunk.sections) {
17482
17599
  if (section.biomes) {
17483
17600
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17491,6 +17608,9 @@ var MineRenderWorld = class {
17491
17608
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17492
17609
  column.set(section.y, copy);
17493
17610
  }
17611
+ }
17612
+ let sliceStart = performance.now();
17613
+ for (const section of chunk.sections) {
17494
17614
  if (!executor) {
17495
17615
  if (!occupiedSections.has(section.y)) continue;
17496
17616
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17655,18 +17775,34 @@ var MineRenderWorld = class {
17655
17775
  const key = this.worldPosToChunkKey(pos);
17656
17776
  let chunk = this._chunks.get(key);
17657
17777
  if (typeof chunk === "undefined") {
17778
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17658
17779
  chunk = new Chunk(
17659
17780
  this.scene,
17660
- Math.floor(pos.x / 16),
17661
- Math.floor(pos.y / 16),
17662
- Math.floor(pos.z / 16),
17781
+ x,
17782
+ y,
17783
+ z,
17663
17784
  (positions) => this.updateCulling(positions),
17664
17785
  this.sectionModels,
17665
- (x, y, z) => {
17666
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17786
+ (x2, y2, z2) => {
17787
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17667
17788
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17668
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17789
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17669
17790
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17791
+ },
17792
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17793
+ async (biomes) => {
17794
+ if (this._chunks.get(key) !== chunk) return;
17795
+ const columnKey = `${x}_${z}`;
17796
+ let column = this.biomes.get(columnKey);
17797
+ if (biomes) {
17798
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17799
+ column.set(y, biomes);
17800
+ } else {
17801
+ column?.delete(y);
17802
+ if (!column?.size) this.biomes.delete(columnKey);
17803
+ }
17804
+ await chunk.refreshBiomes();
17805
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17670
17806
  }
17671
17807
  );
17672
17808
  this._chunks.set(key, chunk);
@@ -17721,6 +17857,7 @@ function buildSectionGeometry(input) {
17721
17857
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17722
17858
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17723
17859
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17860
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17724
17861
  for (const template of templateData) {
17725
17862
  const { quads } = template;
17726
17863
  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) {
@@ -17798,7 +17935,9 @@ function buildSectionGeometry(input) {
17798
17935
  height
17799
17936
  });
17800
17937
  }
17801
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
17938
+ const tintIndex = (x + z * 16 + y * 256) * 3;
17939
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
17940
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17802
17941
  }
17803
17942
  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));
17804
17943
  }
@@ -17847,7 +17986,7 @@ function buildSectionGeometry(input) {
17847
17986
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17848
17987
  }
17849
17988
  }
17850
- for (const { quads, x, y, z, data } of page.fluids) {
17989
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17851
17990
  const atlas = atlases[data.atlas];
17852
17991
  const placement = page.placements[0];
17853
17992
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17871,7 +18010,7 @@ function buildSectionGeometry(input) {
17871
18010
  mapU((rect[0] + rect[2]) / atlas.width),
17872
18011
  mapV(1 - rect[1] / atlas.height)
17873
18012
  ], vertex * 4);
17874
- colors.set(data.tint, vertex * 3);
18013
+ colors.set(tint, vertex * 3);
17875
18014
  }
17876
18015
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17877
18016
  indexOffset += 6;
@@ -18013,7 +18152,7 @@ function toInput(entries, maxAtlasSize, fluids) {
18013
18152
  input.cullMasks[i] = cullMask;
18014
18153
  }
18015
18154
  if (fluids) {
18016
- input.fluids = { cells: fluids.cells };
18155
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
18017
18156
  for (const kind of ["water", "lava"]) {
18018
18157
  const atlas = fluids[kind];
18019
18158
  if (!atlas) continue;
@@ -18114,11 +18253,14 @@ var SectionMesh = class _SectionMesh extends Group3 {
18114
18253
  init_FluidGeometry();
18115
18254
  init_FluidQuads();
18116
18255
  init_CubeFace();
18256
+ var WATER_TINT2 = new Color13(4159204).toArray();
18117
18257
  var Chunk = class _Chunk {
18118
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18258
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18119
18259
  this.onBlocksChanged = onBlocksChanged;
18120
18260
  this.sectionModels = sectionModels;
18121
18261
  this.neighborCell = neighborCell;
18262
+ this.biomeAt = biomeAt;
18263
+ this.onBiomesChanged = onBiomesChanged;
18122
18264
  this.data = new ChunkData();
18123
18265
  this.renderedBlocks = /* @__PURE__ */ new Map();
18124
18266
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18143,6 +18285,56 @@ var Chunk = class _Chunk {
18143
18285
  const index = _Chunk.chunkPosToBlockIndex(pos);
18144
18286
  return this.renderedBlocks.get(index);
18145
18287
  }
18288
+ getBiomeAt(posOrX, y, z) {
18289
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector318(posOrX, y, z));
18290
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector318(...posOrX));
18291
+ const pos = this.worldPosToChunkPos(posOrX);
18292
+ _Chunk.chunkPosToBlockIndex(pos);
18293
+ 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];
18294
+ }
18295
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18296
+ setBiomes(biomes) {
18297
+ const copy = biomes ? [...biomes] : void 0;
18298
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18299
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18300
+ }
18301
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18302
+ this.biomes = copy;
18303
+ return this.refreshBiomes();
18304
+ }
18305
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18306
+ async refreshBiomes() {
18307
+ const affected = /* @__PURE__ */ new Map();
18308
+ const blocks = [];
18309
+ for (const index of this.renderedBlocks.keys()) {
18310
+ const block = this.data.get(index);
18311
+ const stateKey = ChunkData.paletteKey(block);
18312
+ let matches = affected.get(stateKey);
18313
+ if (!matches) {
18314
+ const key = AssetKey.parse("blockstates", block.type);
18315
+ matches = BlockStates.get(key).then(async (state) => {
18316
+ const origin = state?.key ?? key;
18317
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18318
+ return !!source || fluid?.kind === "water";
18319
+ });
18320
+ affected.set(stateKey, matches);
18321
+ }
18322
+ if (await matches) blocks.push({ index, block });
18323
+ }
18324
+ if (!blocks.length) return;
18325
+ const hidden = new Set(this.hiddenBlocks);
18326
+ try {
18327
+ await this.placeBlocks(blocks);
18328
+ } finally {
18329
+ for (const index of hidden) {
18330
+ this.hiddenBlocks.add(index);
18331
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18332
+ }
18333
+ const positions = blocks.map(({ index }) => new Vector318(...this.positionAt(index)));
18334
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18335
+ else await this.rebuildSectionMesh();
18336
+ }
18337
+ }
18146
18338
  async setBlockAt(posOrX, yOrBlock, z, block) {
18147
18339
  if (typeof posOrX == "number") {
18148
18340
  return this.setBlockAt(new Vector318(posOrX, yOrBlock, z), block);
@@ -18181,14 +18373,15 @@ var Chunk = class _Chunk {
18181
18373
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18182
18374
  } : void 0
18183
18375
  }));
18184
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18185
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18186
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18187
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18188
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18189
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18190
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18191
- }));
18376
+ const states = /* @__PURE__ */ new Map();
18377
+ for (const { index, key, block } of placements) {
18378
+ if (key === void 0) continue;
18379
+ let state = states.get(key);
18380
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18381
+ state.indices.push(index);
18382
+ }
18383
+ const biomes = /* @__PURE__ */ new Map();
18384
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18192
18385
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18193
18386
  const positions = [];
18194
18387
  let failed = false, failure;
@@ -18212,7 +18405,8 @@ var Chunk = class _Chunk {
18212
18405
  if (!readBlock) continue;
18213
18406
  const resolved = resolutions.get(key);
18214
18407
  if (resolved.status === "rejected") throw resolved.reason;
18215
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18408
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18409
+ const context = contexts.get(index);
18216
18410
  if (!blockState3) {
18217
18411
  this.data.assign(index, 0);
18218
18412
  continue;
@@ -18220,9 +18414,10 @@ var Chunk = class _Chunk {
18220
18414
  const variantPosition = worldPos.toArray();
18221
18415
  if (fluid) {
18222
18416
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18417
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18223
18418
  this.meshDirty = true;
18224
18419
  }
18225
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18420
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18226
18421
  if (templates) {
18227
18422
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18228
18423
  this.meshDirty = true;
@@ -18232,7 +18427,9 @@ var Chunk = class _Chunk {
18232
18427
  instanceMeshes: true,
18233
18428
  maxInstanceCount: 2e3,
18234
18429
  initialState: stored.properties,
18235
- variantPosition
18430
+ variantPosition,
18431
+ biome: context?.biome,
18432
+ biomePosition: this.biomePosition(index, blockState3, stored)
18236
18433
  });
18237
18434
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18238
18435
  }
@@ -18255,24 +18452,21 @@ var Chunk = class _Chunk {
18255
18452
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18256
18453
  this.sectionBlocks.clear();
18257
18454
  this.fluidCells.fill(0);
18455
+ this.waterTints = void 0;
18258
18456
  this.renderedBlocks.clear();
18259
18457
  this.hiddenBlocks.clear();
18260
18458
  this.meshDirty = true;
18261
18459
  this.data.copyFrom(data);
18262
- const ids = /* @__PURE__ */ new Set();
18460
+ const ids = /* @__PURE__ */ new Map();
18263
18461
  for (let index = 0; index < 4096; index++) {
18264
18462
  const id = this.data.idAt(index);
18265
- if (id) ids.add(id);
18266
- }
18267
- const palette = [...ids];
18268
- const settled = await Promise.allSettled(palette.map(async (id) => {
18269
- const state = this.data.stateAt(id);
18270
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18271
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18272
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18273
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18274
- return { blockState: blockState3, perBlock, fluid, prepared };
18275
- }));
18463
+ if (!id) continue;
18464
+ let indices = ids.get(id);
18465
+ if (!indices) ids.set(id, indices = []);
18466
+ indices.push(index);
18467
+ }
18468
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18469
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18276
18470
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18277
18471
  const perBlocks = [];
18278
18472
  let failed = false, failure;
@@ -18282,24 +18476,28 @@ var Chunk = class _Chunk {
18282
18476
  try {
18283
18477
  const resolved = resolutions.get(id);
18284
18478
  if (resolved.status === "rejected") throw resolved.reason;
18285
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18479
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18480
+ const context = contexts.get(index);
18286
18481
  if (!blockState3) {
18287
18482
  this.data.assign(index, 0);
18288
18483
  this.fluidCells[index] = 0;
18289
18484
  continue;
18290
18485
  }
18291
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18486
+ if (fluid) {
18487
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18488
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18489
+ }
18292
18490
  const variantPosition = [
18293
18491
  this.x * 16 + index % 16,
18294
18492
  this.y * 16 + Math.floor(index / 256),
18295
18493
  this.z * 16 + Math.floor(index / 16) % 16
18296
18494
  ];
18297
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18495
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18298
18496
  if (templates) {
18299
18497
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18300
18498
  this.meshDirty = true;
18301
18499
  } else {
18302
- perBlocks.push({ index, blockState: blockState3 });
18500
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18303
18501
  }
18304
18502
  const readBlock = this.data.snapshot(index);
18305
18503
  this.renderedBlocks.set(index, { get block() {
@@ -18312,7 +18510,7 @@ var Chunk = class _Chunk {
18312
18510
  failed = true;
18313
18511
  }
18314
18512
  }
18315
- for (const { index, blockState: blockState3 } of perBlocks) {
18513
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18316
18514
  let object4;
18317
18515
  try {
18318
18516
  object4 = await this.scene.addBlock(blockState3, {
@@ -18320,11 +18518,9 @@ var Chunk = class _Chunk {
18320
18518
  instanceMeshes: true,
18321
18519
  maxInstanceCount: 2e3,
18322
18520
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18323
- variantPosition: [
18324
- this.x * 16 + index % 16,
18325
- this.y * 16 + Math.floor(index / 256),
18326
- this.z * 16 + Math.floor(index / 16) % 16
18327
- ]
18521
+ variantPosition: this.positionAt(index),
18522
+ biome,
18523
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18328
18524
  });
18329
18525
  object4.setPosition(new Vector318(
18330
18526
  this.x * 256 + index % 16 * 16,
@@ -18343,6 +18539,68 @@ var Chunk = class _Chunk {
18343
18539
  }
18344
18540
  if (failed) throw failure;
18345
18541
  }
18542
+ positionAt(index) {
18543
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18544
+ }
18545
+ biomePosition(index, blockState3, stored) {
18546
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18547
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18548
+ return position;
18549
+ }
18550
+ async resolveState(stored, indices, biomes) {
18551
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18552
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18553
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18554
+ 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;
18555
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18556
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18557
+ const contexts = /* @__PURE__ */ new Map();
18558
+ const groups = /* @__PURE__ */ new Map();
18559
+ if (blockState3 && (source || fluid?.kind === "water")) {
18560
+ for (const index of indices) {
18561
+ const position = this.biomePosition(index, blockState3, stored);
18562
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18563
+ if (!biome) continue;
18564
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18565
+ let group = groups.get(key);
18566
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18567
+ group.indices.push(index);
18568
+ }
18569
+ }
18570
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18571
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18572
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18573
+ const available = () => {
18574
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18575
+ let found = biomes.get(key);
18576
+ if (!found) {
18577
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18578
+ biomes.set(key, found);
18579
+ }
18580
+ return found;
18581
+ };
18582
+ const context = { biome };
18583
+ if (perBlock) {
18584
+ if (!await available()) context.biome = void 0;
18585
+ } else {
18586
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18587
+ if (fluid?.kind === "water" && await available()) {
18588
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18589
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
18590
+ }
18591
+ }
18592
+ for (const index of indices2) contexts.set(index, context);
18593
+ }));
18594
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18595
+ }
18596
+ setWaterTint(index, tint) {
18597
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18598
+ if (!this.waterTints) {
18599
+ this.waterTints = new Float32Array(4096 * 3);
18600
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18601
+ }
18602
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18603
+ }
18346
18604
  /** Changes visibility at world block coordinates while preserving the block's data. */
18347
18605
  async setBlockVisibleAt(pos, visible) {
18348
18606
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18446,7 +18704,7 @@ var Chunk = class _Chunk {
18446
18704
  if (block && !block.object) origins.set(kind, block.block.type);
18447
18705
  }
18448
18706
  if (origins.size) {
18449
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18707
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18450
18708
  for (let y = -1; y <= 16; y++) {
18451
18709
  for (let z = -1; z <= 16; z++) {
18452
18710
  for (let x = -1; x <= 16; x++) {
@@ -18488,8 +18746,13 @@ var Chunk = class _Chunk {
18488
18746
  this.renderedBlocks.clear();
18489
18747
  this.hiddenBlocks.clear();
18490
18748
  this.fluidCells.fill(0);
18749
+ this.waterTints = void 0;
18491
18750
  this.data.clear();
18492
- if (notify) await this.onBlocksChanged?.(positions);
18751
+ this.biomes = void 0;
18752
+ if (notify) {
18753
+ await this.onBiomesChanged?.(void 0);
18754
+ await this.onBlocksChanged?.(positions);
18755
+ }
18493
18756
  }
18494
18757
  async dispose() {
18495
18758
  await this.clear();
@@ -18874,6 +19137,7 @@ export {
18874
19137
  BannerPatterns,
18875
19138
  BasicAssetKey,
18876
19139
  BatchedExecutor,
19140
+ Biomes,
18877
19141
  BlockEntities,
18878
19142
  BlockObject,
18879
19143
  BlockStateResolver,