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.
@@ -279,7 +279,8 @@ var init_Caching = __esm({
279
279
  this.fontCache,
280
280
  this.bannerPatternCache,
281
281
  this.mineRenderDataCache,
282
- this.usernameUuidCache
282
+ this.usernameUuidCache,
283
+ this.biomeCache
283
284
  ];
284
285
  }
285
286
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -330,6 +331,7 @@ var init_Caching = __esm({
330
331
  _Caching.bannerPatternCache = _Caching.createAssetCache();
331
332
  _Caching.mineRenderDataCache = _Caching.createAssetCache();
332
333
  _Caching.usernameUuidCache = Caches.builder().expireAfterWrite(Time.minutes(5)).expirationInterval(Time.seconds(30)).buildAsync();
334
+ _Caching.biomeCache = _Caching.createAssetCache();
333
335
  Caching = _Caching;
334
336
  }
335
337
  });
@@ -1604,7 +1606,8 @@ var init_MineRenderData = __esm({
1604
1606
  }
1605
1607
  /**
1606
1608
  * Uses the exact version when published. Otherwise, selects the nearest patch in the same
1607
- * release line (older on ties), then the latest earlier release. Unpublished snapshots reject.
1609
+ * release line (older on ties), then the latest earlier release or the oldest published release.
1610
+ * Unpublished snapshots reject.
1608
1611
  */
1609
1612
  static async resolve(root) {
1610
1613
  const requestRoot = root?.replace(/\/+$/, "");
@@ -1613,11 +1616,11 @@ var init_MineRenderData = __esm({
1613
1616
  const scope = AssetLoader.persistentScope;
1614
1617
  return this.cached(`resolve:${requestRoot === void 0 ? "sources" : "explicit"}:${root}`, scope, defaultRoot, async () => {
1615
1618
  const requestedVersion = root.slice(root.lastIndexOf("/") + 1);
1616
- let manifest = await this.manifest(root, release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1619
+ let manifest = await this.manifest(root, requestRoot !== void 0 && release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1617
1620
  if (manifest) return { requestedVersion, version: manifest.minecraftVersion, root, manifest };
1618
1621
  const requested = release(requestedVersion);
1619
- if (!requested || compare(requested, [1, 16, 5]) < 0) {
1620
- throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release from 1.16.5 onward`);
1622
+ if (!requested) {
1623
+ throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release`);
1621
1624
  }
1622
1625
  const base = root.slice(0, root.lastIndexOf("/"));
1623
1626
  const index = await this.cached(`versions:${base}`, scope, defaultRoot, async () => {
@@ -1627,9 +1630,9 @@ var init_MineRenderData = __esm({
1627
1630
  }
1628
1631
  return value.versions;
1629
1632
  });
1630
- 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));
1633
+ const versions = index.map((entry) => ({ name: entry.name, number: release(entry.name) })).filter((entry) => !!entry.number).sort((left, right) => compare(left.number, right.number));
1631
1634
  const sameLine = versions.filter((entry) => entry.number[0] === requested[0] && entry.number[1] === requested[1]);
1632
- 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();
1635
+ 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];
1633
1636
  if (!selected) throw new Error(`No compatible MineRender data is published for ${requestedVersion}`);
1634
1637
  const resolvedRoot = `${base}/${selected.name}`;
1635
1638
  manifest = await this.manifest(resolvedRoot, selected.name, resolvedRoot);
@@ -1676,7 +1679,7 @@ ${key}` : key, async () => {
1676
1679
  case "blockStates":
1677
1680
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1678
1681
  case "blockTints":
1679
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1682
+ 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));
1680
1683
  case "fluids":
1681
1684
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1682
1685
  case "dyes":
@@ -1745,12 +1748,7 @@ var init_BannerPatterns = __esm({
1745
1748
  static async getLayers(value, target, root) {
1746
1749
  if (value === void 0) return [];
1747
1750
  if (!Array.isArray(value)) throw new Error("banner_patterns must be an array");
1748
- const activeRoot = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
1749
- const checkSources = () => {
1750
- if (activeRoot !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading banner patterns; retry the request");
1751
- };
1752
1751
  const colors = value.length ? await this.getColors(root) : void 0;
1753
- checkSources();
1754
1752
  return Promise.all(value.slice(0, 16).map(async (layer) => {
1755
1753
  if (!layer || typeof layer !== "object" || Array.isArray(layer)) throw new Error("Each banner pattern requires a pattern and dye color");
1756
1754
  if (typeof layer.color !== "string" || !Object.prototype.hasOwnProperty.call(colors, layer.color)) throw new Error(`Unsupported dye color ${layer.color}`);
@@ -1760,7 +1758,6 @@ var init_BannerPatterns = __esm({
1760
1758
  const id = this.identifier(pattern2);
1761
1759
  const key = new AssetKey(id.namespace, id.getFullPath(), "banner_pattern", void 0, "data", ".json", root);
1762
1760
  pattern2 = await Caching.bannerPatternCache.get(key.serialize(), () => AssetLoader.get(key, "json" /* JSON */));
1763
- checkSources();
1764
1761
  if (!pattern2) throw new Error(`Missing banner pattern ${layer.pattern}`);
1765
1762
  }
1766
1763
  if (!pattern2 || typeof pattern2 !== "object" || Array.isArray(pattern2) || typeof pattern2.translation_key !== "string") throw new Error("Banner patterns require asset_id and translation_key");
@@ -1820,15 +1817,19 @@ var init_Colormaps = __esm({
1820
1817
  Colormaps = class {
1821
1818
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1822
1819
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1823
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1820
+ return this.getColor("grass", origin, temperature, downfall);
1821
+ }
1822
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1823
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1824
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1824
1825
  const temp = Math.fround(temperature);
1825
1826
  const rain = Math.fround(downfall) * temp;
1826
1827
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1827
1828
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1828
1829
  const image = await ModelTextures.get(key);
1829
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1830
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1830
1831
  if (image.width !== 256 || image.height !== 256) {
1831
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1832
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1832
1833
  }
1833
1834
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1834
1835
  return red << 16 | green << 8 | blue;
@@ -2041,30 +2042,24 @@ var init_Models = __esm({
2041
2042
  }).then((r) => r?.files ?? []);
2042
2043
  }
2043
2044
  static async loadAndMerge(key, context = {}) {
2044
- const checkSources = this.sourceCheck();
2045
2045
  const model = key.type === "item" ? await this.getItemModel(key, context) : await this.getRaw(key);
2046
2046
  if (!model) {
2047
2047
  return void 0;
2048
2048
  }
2049
2049
  const merged = await ModelMerger.mergeWithParents(model);
2050
- checkSources();
2051
2050
  return merged;
2052
2051
  }
2053
2052
  static async getItemModel(key, context = {}) {
2054
- const checkSources = this.sourceCheck();
2055
2053
  const preview = this.snapshotContext(key, context);
2056
2054
  const dataKey = await this.applyDefaults(key, preview);
2057
- checkSources();
2058
2055
  return this.getPreparedItemModel(key, preview, dataKey);
2059
2056
  }
2060
2057
  static async getPreparedItemModel(key, preview, dataKey) {
2061
- const checkSources = this.sourceCheck();
2062
2058
  const itemId = `${key.namespace}:${key.path}`;
2063
2059
  const itemKey2 = new AssetKey(key.namespace, key.path, "items", void 0, key.rootType, ".json", key.root);
2064
2060
  const cacheKey = itemKey2.serialize() + this.contextKey(preview) + dataKey;
2065
2061
  const model = await this.PERSISTENT_CACHE.getOrLoad(`${this.ITEM_CACHE_VERSION}:${AssetLoader.persistentKey(cacheKey)}`, async () => {
2066
2062
  const result = await AssetLoader.getFirst([itemKey2, key], "json" /* JSON */);
2067
- checkSources();
2068
2063
  if (!result) return void 0;
2069
2064
  if (result.key.assetType !== "items") return { ...result.asset, key, itemId, components: preview.components };
2070
2065
  const load = async (selected) => {
@@ -2083,10 +2078,8 @@ var init_Models = __esm({
2083
2078
  return { ...model3, itemId, components: preview.components, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
2084
2079
  };
2085
2080
  const model2 = await load(this.selectItemModel(result.asset.model, key, preview));
2086
- checkSources();
2087
2081
  return model2;
2088
2082
  });
2089
- checkSources();
2090
2083
  const restore = (model2) => ({
2091
2084
  ...model2,
2092
2085
  key: Object.assign(new AssetKey("", ""), model2.key),
@@ -2150,22 +2143,14 @@ var init_Models = __esm({
2150
2143
  }
2151
2144
  static async applyDefaults(key, preview) {
2152
2145
  if (key.type !== "item") return "";
2153
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2154
2146
  const resolved = await MineRenderData.resolve(key.root);
2155
2147
  const defaults = await ItemDefaults.get(`${key.namespace}:${key.path}`, key.root);
2156
2148
  preview.components = { ...defaults, ...preview.components };
2157
2149
  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)) {
2158
2150
  preview.components["minecraft:enchantment_glint_override"] = true;
2159
2151
  }
2160
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item defaults; retry the request");
2161
2152
  return `|data:${resolved.root}:${resolved.manifest.datasets.itemDefaults?.sha256}:${resolved.manifest.datasets.potionColors?.sha256}`;
2162
2153
  }
2163
- static sourceCheck() {
2164
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2165
- return () => {
2166
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item models; retry the request");
2167
- };
2168
- }
2169
2154
  static contextKey(context) {
2170
2155
  const itemReferences = Object.fromEntries(Object.entries(context.itemReferences).map(([id, key]) => [id, key.serialize()]));
2171
2156
  return `|${this.ITEM_CACHE_VERSION}:${JSON.stringify(this.snapshotJson({ ...context, itemReferences }))}`;
@@ -2352,7 +2337,6 @@ var init_Models = __esm({
2352
2337
  * @param key - Model key, for example `AssetKey.parse("models", "minecraft:item/diamond_sword")`.
2353
2338
  */
2354
2339
  static async getMerged(key, context = {}) {
2355
- const checkSources = this.sourceCheck();
2356
2340
  if (!key.assetType) {
2357
2341
  key.assetType = "models";
2358
2342
  }
@@ -2361,15 +2345,12 @@ var init_Models = __esm({
2361
2345
  }
2362
2346
  const preview = this.snapshotContext(key, context);
2363
2347
  const dataKey = await this.applyDefaults(key, preview);
2364
- checkSources();
2365
2348
  const keyStr = AssetLoader.persistentKey(key.serialize() + (key.type === "item" ? this.contextKey(preview) + dataKey : ""));
2366
2349
  const model = await Caching.mergedModelCache.get(keyStr, async () => {
2367
2350
  const raw = key.type === "item" ? await this.getPreparedItemModel(key, preview, dataKey) : await this.getRaw(key);
2368
2351
  const merged = raw ? await ModelMerger.mergeWithParents(raw) : void 0;
2369
- checkSources();
2370
2352
  return merged;
2371
2353
  });
2372
- checkSources();
2373
2354
  return model;
2374
2355
  }
2375
2356
  /** Alias for {@link getMerged}. */
@@ -7699,6 +7680,116 @@ init_AssetLoader();
7699
7680
  init_Assets();
7700
7681
  init_BannerPatterns();
7701
7682
 
7683
+ // src/assets/Biomes.ts
7684
+ init_AssetKey();
7685
+ init_AssetLoader();
7686
+ init_AssetParsers();
7687
+ init_Caching();
7688
+ init_MineRenderError();
7689
+ init_Colormaps();
7690
+
7691
+ // src/assets/_biomes/SwampGrassColor.ts
7692
+ var permutation;
7693
+ 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]];
7694
+ var skew = (Math.sqrt(3) - 1) / 2;
7695
+ var unskew = (3 - Math.sqrt(3)) / 6;
7696
+ function createPermutation() {
7697
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7698
+ let seed = 2345n ^ multiplier;
7699
+ const next = (bits) => {
7700
+ seed = seed * multiplier + 11n & mask;
7701
+ return Number(seed >> BigInt(48 - bits));
7702
+ };
7703
+ for (let i = 0; i < 3; i++) {
7704
+ next(26);
7705
+ next(27);
7706
+ }
7707
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7708
+ for (let i = 0; i < 256; i++) {
7709
+ const bound = 256 - i;
7710
+ let offset;
7711
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7712
+ else {
7713
+ let bits;
7714
+ do {
7715
+ bits = next(31);
7716
+ offset = bits % bound;
7717
+ } while (bits - offset + bound - 1 > 2147483647);
7718
+ }
7719
+ const other = i + offset;
7720
+ [values[i], values[other]] = [values[other], values[i]];
7721
+ }
7722
+ return values;
7723
+ }
7724
+ function swampGrassColor(x, z) {
7725
+ const p10 = permutation ?? (permutation = createPermutation());
7726
+ x *= 0.0225;
7727
+ z *= 0.0225;
7728
+ const s = (x + z) * skew;
7729
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7730
+ const t = (i + j) * unskew;
7731
+ const dx = x - (i - t), dz = z - (j - t);
7732
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7733
+ const corner = (gx, gz, a, b) => {
7734
+ let falloff = 0.5 - a * a - b * b;
7735
+ if (falloff < 0) return 0;
7736
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7737
+ falloff *= falloff;
7738
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7739
+ };
7740
+ 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));
7741
+ return noise < -0.1 ? 5011004 : 6975545;
7742
+ }
7743
+
7744
+ // src/assets/Biomes.ts
7745
+ var Biomes = class {
7746
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7747
+ static async get(id, origin) {
7748
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7749
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7750
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7751
+ return Caching.biomeCache.get(key.serialize(), async () => {
7752
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7753
+ if (asset2 === void 0) return void 0;
7754
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7755
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7756
+ }
7757
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7758
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7759
+ }
7760
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7761
+ const color2 = asset2.effects[`${kind}_color`];
7762
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7763
+ }
7764
+ const modifier = asset2.effects.grass_color_modifier;
7765
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7766
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7767
+ }
7768
+ return asset2;
7769
+ });
7770
+ }
7771
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7772
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7773
+ const biome = await this.get(id, origin);
7774
+ if (!biome) return void 0;
7775
+ const rawColor = biome.effects[`${kind}_color`];
7776
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7777
+ if (kind === "water") return color2;
7778
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7779
+ if (modifier === "swamp") return swampGrassColor(x, z);
7780
+ if (color2 === void 0) {
7781
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7782
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7783
+ }
7784
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7785
+ }
7786
+ static parseColor(value, id, kind) {
7787
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7788
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7789
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7790
+ }
7791
+ };
7792
+
7702
7793
  // src/assets/BlockEntities.ts
7703
7794
  init_AssetKey();
7704
7795
  init_AssetLoader();
@@ -12178,8 +12269,20 @@ var BlockStateResolver = class _BlockStateResolver {
12178
12269
  init_Colormaps();
12179
12270
  init_MineRenderData();
12180
12271
  var BlockTints = class {
12181
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12182
- static async get(key, state, model, tints) {
12272
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12273
+ static async biomeSource(key) {
12274
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12275
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12276
+ }
12277
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12278
+ static async getBiomeColor(key, biome, position) {
12279
+ if (!biome) return void 0;
12280
+ const x = position?.[0], z = position?.[2];
12281
+ const source = await this.biomeSource(key);
12282
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12283
+ }
12284
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12285
+ static async get(key, state, model, tints, biomeColor) {
12183
12286
  if (!key) return tints;
12184
12287
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12185
12288
  if (!candidates) return tints;
@@ -12195,8 +12298,12 @@ var BlockTints = class {
12195
12298
  }
12196
12299
  }
12197
12300
  if (!indices.size) return tints;
12301
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12302
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12198
12303
  let color2;
12199
- switch (rule.source) {
12304
+ if (biome !== void 0) {
12305
+ color2 = biome;
12306
+ } else switch (rule.source) {
12200
12307
  case "constant":
12201
12308
  color2 = rule.color;
12202
12309
  break;
@@ -12233,7 +12340,6 @@ import { Euler as Euler10, Matrix4 as Matrix411, Quaternion as Quaternion5, Vect
12233
12340
  init_ModelCulling();
12234
12341
  init_FluidGeometry();
12235
12342
  init_MineRenderData();
12236
- init_AssetLoader();
12237
12343
  init_Entities();
12238
12344
  var _BlockObject = class _BlockObject extends SceneObject {
12239
12345
  constructor(blockState3, options) {
@@ -12255,21 +12361,15 @@ var _BlockObject = class _BlockObject extends SceneObject {
12255
12361
  this._entityGeneration = 0;
12256
12362
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12257
12363
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12364
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12258
12365
  }
12259
12366
  async init() {
12260
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
12261
- const checkSources = () => {
12262
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading a block; retry the request");
12263
- };
12264
12367
  this._fluidRules = await MineRenderData.get("fluids", this.blockState.key?.root);
12265
- checkSources();
12266
12368
  if (this.options.applyDefaultState) {
12267
12369
  this._setState(await BlockStateResolver.defaults(this.blockState));
12268
- checkSources();
12269
12370
  }
12270
12371
  if (this.options.initialState !== void 0) this._setState(this.options.initialState);
12271
12372
  await this.recreateModels();
12272
- checkSources();
12273
12373
  }
12274
12374
  dispose() {
12275
12375
  this.clearModels();
@@ -12447,11 +12547,16 @@ var _BlockObject = class _BlockObject extends SceneObject {
12447
12547
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12448
12548
  this._fluidSampler = sample;
12449
12549
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12450
- if (this._fluidKey === surface.key) return;
12550
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12551
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12552
+ if (this._fluidKey === fluidKey) return;
12451
12553
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12452
12554
  const replacement = await this.scene.addSceneObject(
12453
12555
  { key },
12454
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12556
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12557
+ ...this.options,
12558
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12559
+ })
12455
12560
  );
12456
12561
  const previous = this._fluidModel;
12457
12562
  const matrix = previous ? this.getModelMatrix(previous) : new Matrix411().makeTranslation(...this.position.toArray());
@@ -12460,7 +12565,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12460
12565
  this._models = this._models.filter((model) => model !== previous);
12461
12566
  this._models.push(replacement);
12462
12567
  this._fluidModel = replacement;
12463
- this._fluidKey = surface.key;
12568
+ this._fluidKey = fluidKey;
12464
12569
  this._isInstanced || (this._isInstanced = isInstanceReference(replacement));
12465
12570
  this._instanceCounter = this._isInstanced ? 1 : 0;
12466
12571
  if (previous) this.removeModels([previous]);
@@ -12483,6 +12588,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12483
12588
  }
12484
12589
  async recreateModels() {
12485
12590
  this.clearModels();
12591
+ 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;
12486
12592
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12487
12593
  await this.updateFluid(this._fluidSampler);
12488
12594
  return;
@@ -12517,7 +12623,13 @@ var _BlockObject = class _BlockObject extends SceneObject {
12517
12623
  const model = await Models.getMerged(modelKey);
12518
12624
  const options = {
12519
12625
  ...this.options,
12520
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12626
+ tints: model ? await BlockTints.get(
12627
+ this.blockState.key,
12628
+ this.state,
12629
+ model,
12630
+ this.options.tints,
12631
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12632
+ ) : this.options.tints,
12521
12633
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12522
12634
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12523
12635
  };
@@ -16180,7 +16292,6 @@ init_util2();
16180
16292
  // src/world/AnvilParser.ts
16181
16293
  init_MineRenderError();
16182
16294
  init_MineRenderData();
16183
- init_AssetLoader();
16184
16295
  init_NBTHelper();
16185
16296
 
16186
16297
  // src/world/ChunkData.ts
@@ -16451,14 +16562,12 @@ var AnvilParser = class {
16451
16562
  /** Decodes all stored chunks. Gzip, zlib, LZ4, and uncompressed payloads are supported. */
16452
16563
  static async parse(data, options = {}) {
16453
16564
  options = { ...options };
16454
- const checkSources = this.assetGuard();
16455
16565
  options.signal?.throwIfAborted();
16456
16566
  const bytes = this.bytes(data);
16457
16567
  const chunks = [];
16458
16568
  for (const location of this.locations(bytes)) {
16459
16569
  chunks.push(await this.abortable(this.readChunk(bytes, location, options), options.signal));
16460
16570
  }
16461
- checkSources();
16462
16571
  return { chunks };
16463
16572
  }
16464
16573
  /**
@@ -16469,22 +16578,12 @@ var AnvilParser = class {
16469
16578
  */
16470
16579
  static async parseChunk(data, localX, localZ, options = {}) {
16471
16580
  options = { ...options };
16472
- const checkSources = this.assetGuard();
16473
16581
  options.signal?.throwIfAborted();
16474
16582
  const bytes = this.bytes(data);
16475
16583
  const location = this.location(bytes, localX, localZ);
16476
16584
  const chunk = location ? await this.abortable(this.readChunk(bytes, location, options), options.signal) : void 0;
16477
- checkSources();
16478
16585
  return chunk;
16479
16586
  }
16480
- static assetGuard() {
16481
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
16482
- return () => {
16483
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) {
16484
- throw new MineRenderError("Asset sources changed while parsing Anvil data; retry the request");
16485
- }
16486
- };
16487
- }
16488
16587
  /**
16489
16588
  * Decodes one chunk from an `entities/r.x.z.mca` file without migrating its NBT.
16490
16589
  * @param localX - Chunk x within the region, from 0 to 31.
@@ -16936,7 +17035,7 @@ var AnvilWorldSource = class {
16936
17035
  };
16937
17036
 
16938
17037
  // src/world/Chunk.ts
16939
- import { Vector3 as Vector318 } from "three";
17038
+ import { Color as Color13, Vector3 as Vector318 } from "three";
16940
17039
  init_BlockStates();
16941
17040
  init_AssetKey();
16942
17041
 
@@ -17006,24 +17105,48 @@ var SectionModels = class {
17006
17105
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17007
17106
  }
17008
17107
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17009
- async get(blockState3, properties = {}, position) {
17010
- const variantPosition = position ? [...position] : void 0;
17011
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17108
+ async get(blockState3, properties = {}, position, biome) {
17109
+ const biomePosition = position ? [...position] : void 0;
17110
+ const prepared = await this.state(blockState3, properties);
17111
+ if (!prepared) return void 0;
17112
+ return Promise.all(prepared.groups.map(async (group) => {
17113
+ const variant = BlockStateResolver.choose(group, biomePosition);
17114
+ const choice = prepared.choices.get(variant);
17115
+ const tints = await BlockTints.get(
17116
+ blockState3.key,
17117
+ prepared.state,
17118
+ choice.model,
17119
+ void 0,
17120
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17121
+ );
17122
+ return this.template(variant, choice, tints);
17123
+ }));
17012
17124
  }
17013
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17014
- async prepareState(blockState3, properties = {}) {
17125
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17126
+ async prepareState(blockState3, properties = {}, biomeColor) {
17127
+ const prepared = await this.state(blockState3, properties);
17128
+ if (!prepared) return void 0;
17129
+ let color2;
17130
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17131
+ const templates = /* @__PURE__ */ new Map();
17132
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17133
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17134
+ templates.set(variant, await this.template(variant, choice, tints));
17135
+ }));
17136
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17137
+ }
17138
+ async state(blockState3, properties) {
17015
17139
  let states = this.states.get(blockState3);
17016
17140
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17017
17141
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17018
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17019
- if (!prepared) return void 0;
17020
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17142
+ return cached(states, key, () => this.prepare(blockState3, properties));
17021
17143
  }
17022
17144
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17023
17145
  fluidAtlas(kind, root) {
17024
- return cached(this.fluidAtlases, kind, async () => {
17146
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17147
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17025
17148
  const atlas = await UVMapper.getAtlas({
17026
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17149
+ key,
17027
17150
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17028
17151
  elements: []
17029
17152
  });
@@ -17035,64 +17158,67 @@ var SectionModels = class {
17035
17158
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17036
17159
  const groups = BlockStateResolver.matching(blockState3, state);
17037
17160
  if (!groups.length) return void 0;
17038
- const templates = /* @__PURE__ */ new Map();
17161
+ const prepared = /* @__PURE__ */ new Map();
17039
17162
  for (const group of groups) {
17040
17163
  const choices = Array.isArray(group) ? group : [group];
17041
17164
  if (!choices.length) return void 0;
17042
17165
  for (const variant of choices) {
17043
17166
  if (!variant.model) return void 0;
17044
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17167
+ const modelKey = AssetKey.parse("models", variant.model);
17168
+ modelKey.root = blockState3.key?.root;
17169
+ const model = await Models.getMerged(modelKey);
17045
17170
  if (!model?.elements?.length) return void 0;
17046
17171
  const atlas = await UVMapper.getAtlas(model);
17047
17172
  const rotation = BlockStateResolver.rotation(variant);
17048
17173
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17049
- const tints = await BlockTints.get(blockState3.key, state, model);
17050
- let models = this.models.get(model);
17051
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17052
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17053
- const template = await cached(models, key, async () => {
17054
- const object4 = new SectionModel(model, {
17055
- instanceMeshes: false,
17056
- mergeMeshes: true,
17057
- tints,
17058
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17059
- });
17060
- await object4.init();
17061
- let geometry = object4.children[0].geometry;
17062
- geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17063
- object4.clear();
17064
- const keep = [];
17065
- const cullFaces = [];
17066
- for (const element of atlas.model.elements) {
17067
- for (const face of CUBE_FACES) {
17068
- const data = element.faces[face];
17069
- keep.push(data !== void 0);
17070
- if (!data) continue;
17071
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17072
- if (local < 0) {
17073
- cullFaces.push(0);
17074
- continue;
17075
- }
17076
- const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17077
- 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);
17078
- cullFaces.push(world < 0 ? 0 : 1 << world);
17079
- }
17080
- }
17081
- geometry = compactQuads(geometry, keep);
17082
- const template2 = {
17083
- geometry,
17084
- atlas,
17085
- cullFaces: new Uint8Array(cullFaces),
17086
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17087
- layer: atlas.hasTranslucency ? 1 : 0
17088
- };
17089
- this.templates.add(template2);
17090
- return template2;
17091
- });
17092
- templates.set(variant, template);
17174
+ prepared.set(variant, { model, atlas, rotation });
17093
17175
  }
17094
17176
  }
17095
- return { groups, templates };
17177
+ return { state, groups, choices: prepared };
17178
+ }
17179
+ async template(variant, { model, atlas, rotation }, tints) {
17180
+ let models = this.models.get(model);
17181
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17182
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17183
+ return cached(models, key, async () => {
17184
+ const object4 = new SectionModel(model, {
17185
+ instanceMeshes: false,
17186
+ mergeMeshes: true,
17187
+ tints,
17188
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17189
+ });
17190
+ await object4.init();
17191
+ let geometry = object4.children[0].geometry;
17192
+ geometry.applyMatrix4(new Matrix412().makeRotationFromEuler(rotation));
17193
+ object4.clear();
17194
+ const keep = [];
17195
+ const cullFaces = [];
17196
+ for (const element of atlas.model.elements) {
17197
+ for (const face of CUBE_FACES) {
17198
+ const data = element.faces[face];
17199
+ keep.push(data !== void 0);
17200
+ if (!data) continue;
17201
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17202
+ if (local < 0) {
17203
+ cullFaces.push(0);
17204
+ continue;
17205
+ }
17206
+ const normal = new Vector316(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17207
+ 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);
17208
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17209
+ }
17210
+ }
17211
+ geometry = compactQuads(geometry, keep);
17212
+ const template = {
17213
+ geometry,
17214
+ atlas,
17215
+ cullFaces: new Uint8Array(cullFaces),
17216
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17217
+ layer: atlas.hasTranslucency ? 1 : 0
17218
+ };
17219
+ this.templates.add(template);
17220
+ return template;
17221
+ });
17096
17222
  }
17097
17223
  /** Disposes prepared geometry and clears this world's template caches. */
17098
17224
  clear() {
@@ -17178,7 +17304,6 @@ function resolveSavedEntity(entity) {
17178
17304
 
17179
17305
  // src/world/_entities/WorldEntities.ts
17180
17306
  init_BannerPatterns();
17181
- init_AssetLoader();
17182
17307
  var WorldEntities = class {
17183
17308
  constructor(scene) {
17184
17309
  this.scene = scene;
@@ -17186,10 +17311,6 @@ var WorldEntities = class {
17186
17311
  }
17187
17312
  /** Captures column ownership before asynchronous block or asset loading starts. */
17188
17313
  prepare(entities = [], column) {
17189
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
17190
- const checkSources = () => {
17191
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading saved entities; retry the request");
17192
- };
17193
17314
  const placements = entities.flatMap((entity) => {
17194
17315
  const resolved = resolveSavedEntity(entity);
17195
17316
  if (!resolved) return [];
@@ -17205,17 +17326,13 @@ var WorldEntities = class {
17205
17326
  let object4;
17206
17327
  try {
17207
17328
  if (!current()) return;
17208
- checkSources();
17209
17329
  const models = await Entities.getEntityList();
17210
- checkSources();
17211
17330
  if (!models.includes(placement.key.path) || !current()) return;
17212
17331
  const model = await Entities.getEntity(placement.key, placement.texture, { when: placement.when });
17213
- checkSources();
17214
17332
  if (!model || !current()) return;
17215
17333
  let tints = placement.tints;
17216
17334
  if (placement.woolDye) {
17217
17335
  const dye = (await BannerPatterns.getColors(placement.key.root))[placement.woolDye];
17218
- checkSources();
17219
17336
  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);
17220
17337
  tints = { ...tints, wool_color: tint };
17221
17338
  }
@@ -17225,7 +17342,6 @@ var WorldEntities = class {
17225
17342
  object4.position.fromArray(placement.position).multiplyScalar(16).addScalar(-8);
17226
17343
  object4.rotation.y = placement.yaw;
17227
17344
  await object4.init();
17228
- checkSources();
17229
17345
  if (!current()) return;
17230
17346
  placement.owner.add(object4);
17231
17347
  this.scene.add(object4);
@@ -17422,7 +17538,6 @@ var MineRenderWorld = class {
17422
17538
  }
17423
17539
  try {
17424
17540
  await this.clearChunkColumn(chunk.x, chunk.z);
17425
- let sliceStart = performance.now();
17426
17541
  for (const section of chunk.sections) {
17427
17542
  if (section.biomes) {
17428
17543
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17436,6 +17551,9 @@ var MineRenderWorld = class {
17436
17551
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17437
17552
  column.set(section.y, copy);
17438
17553
  }
17554
+ }
17555
+ let sliceStart = performance.now();
17556
+ for (const section of chunk.sections) {
17439
17557
  if (!executor) {
17440
17558
  if (!occupiedSections.has(section.y)) continue;
17441
17559
  const position = new Vector317(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17600,18 +17718,34 @@ var MineRenderWorld = class {
17600
17718
  const key = this.worldPosToChunkKey(pos);
17601
17719
  let chunk = this._chunks.get(key);
17602
17720
  if (typeof chunk === "undefined") {
17721
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17603
17722
  chunk = new Chunk(
17604
17723
  this.scene,
17605
- Math.floor(pos.x / 16),
17606
- Math.floor(pos.y / 16),
17607
- Math.floor(pos.z / 16),
17724
+ x,
17725
+ y,
17726
+ z,
17608
17727
  (positions) => this.updateCulling(positions),
17609
17728
  this.sectionModels,
17610
- (x, y, z) => {
17611
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17729
+ (x2, y2, z2) => {
17730
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17612
17731
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17613
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17732
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17614
17733
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17734
+ },
17735
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17736
+ async (biomes) => {
17737
+ if (this._chunks.get(key) !== chunk) return;
17738
+ const columnKey = `${x}_${z}`;
17739
+ let column = this.biomes.get(columnKey);
17740
+ if (biomes) {
17741
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17742
+ column.set(y, biomes);
17743
+ } else {
17744
+ column?.delete(y);
17745
+ if (!column?.size) this.biomes.delete(columnKey);
17746
+ }
17747
+ await chunk.refreshBiomes();
17748
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17615
17749
  }
17616
17750
  );
17617
17751
  this._chunks.set(key, chunk);
@@ -17666,6 +17800,7 @@ function buildSectionGeometry(input) {
17666
17800
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17667
17801
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17668
17802
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17803
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17669
17804
  for (const template of templateData) {
17670
17805
  const { quads } = template;
17671
17806
  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) {
@@ -17743,7 +17878,9 @@ function buildSectionGeometry(input) {
17743
17878
  height
17744
17879
  });
17745
17880
  }
17746
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
17881
+ const tintIndex = (x + z * 16 + y * 256) * 3;
17882
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
17883
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17747
17884
  }
17748
17885
  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));
17749
17886
  }
@@ -17792,7 +17929,7 @@ function buildSectionGeometry(input) {
17792
17929
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17793
17930
  }
17794
17931
  }
17795
- for (const { quads, x, y, z, data } of page.fluids) {
17932
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17796
17933
  const atlas = atlases[data.atlas];
17797
17934
  const placement = page.placements[0];
17798
17935
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17816,7 +17953,7 @@ function buildSectionGeometry(input) {
17816
17953
  mapU((rect[0] + rect[2]) / atlas.width),
17817
17954
  mapV(1 - rect[1] / atlas.height)
17818
17955
  ], vertex * 4);
17819
- colors.set(data.tint, vertex * 3);
17956
+ colors.set(tint, vertex * 3);
17820
17957
  }
17821
17958
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17822
17959
  indexOffset += 6;
@@ -17957,7 +18094,7 @@ function toInput(entries, maxAtlasSize, fluids) {
17957
18094
  input.cullMasks[i] = cullMask;
17958
18095
  }
17959
18096
  if (fluids) {
17960
- input.fluids = { cells: fluids.cells };
18097
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
17961
18098
  for (const kind of ["water", "lava"]) {
17962
18099
  const atlas = fluids[kind];
17963
18100
  if (!atlas) continue;
@@ -18058,11 +18195,14 @@ var SectionMesh = class _SectionMesh extends Group3 {
18058
18195
  init_FluidGeometry();
18059
18196
  init_FluidQuads();
18060
18197
  init_CubeFace();
18198
+ var WATER_TINT2 = new Color13(4159204).toArray();
18061
18199
  var Chunk = class _Chunk {
18062
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18200
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18063
18201
  this.onBlocksChanged = onBlocksChanged;
18064
18202
  this.sectionModels = sectionModels;
18065
18203
  this.neighborCell = neighborCell;
18204
+ this.biomeAt = biomeAt;
18205
+ this.onBiomesChanged = onBiomesChanged;
18066
18206
  this.data = new ChunkData();
18067
18207
  this.renderedBlocks = /* @__PURE__ */ new Map();
18068
18208
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18087,6 +18227,56 @@ var Chunk = class _Chunk {
18087
18227
  const index = _Chunk.chunkPosToBlockIndex(pos);
18088
18228
  return this.renderedBlocks.get(index);
18089
18229
  }
18230
+ getBiomeAt(posOrX, y, z) {
18231
+ if (typeof posOrX === "number") return this.getBiomeAt(new Vector318(posOrX, y, z));
18232
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new Vector318(...posOrX));
18233
+ const pos = this.worldPosToChunkPos(posOrX);
18234
+ _Chunk.chunkPosToBlockIndex(pos);
18235
+ 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];
18236
+ }
18237
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18238
+ setBiomes(biomes) {
18239
+ const copy = biomes ? [...biomes] : void 0;
18240
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18241
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18242
+ }
18243
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18244
+ this.biomes = copy;
18245
+ return this.refreshBiomes();
18246
+ }
18247
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18248
+ async refreshBiomes() {
18249
+ const affected = /* @__PURE__ */ new Map();
18250
+ const blocks = [];
18251
+ for (const index of this.renderedBlocks.keys()) {
18252
+ const block = this.data.get(index);
18253
+ const stateKey = ChunkData.paletteKey(block);
18254
+ let matches = affected.get(stateKey);
18255
+ if (!matches) {
18256
+ const key = AssetKey.parse("blockstates", block.type);
18257
+ matches = BlockStates.get(key).then(async (state) => {
18258
+ const origin = state?.key ?? key;
18259
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18260
+ return !!source || fluid?.kind === "water";
18261
+ });
18262
+ affected.set(stateKey, matches);
18263
+ }
18264
+ if (await matches) blocks.push({ index, block });
18265
+ }
18266
+ if (!blocks.length) return;
18267
+ const hidden = new Set(this.hiddenBlocks);
18268
+ try {
18269
+ await this.placeBlocks(blocks);
18270
+ } finally {
18271
+ for (const index of hidden) {
18272
+ this.hiddenBlocks.add(index);
18273
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18274
+ }
18275
+ const positions = blocks.map(({ index }) => new Vector318(...this.positionAt(index)));
18276
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18277
+ else await this.rebuildSectionMesh();
18278
+ }
18279
+ }
18090
18280
  async setBlockAt(posOrX, yOrBlock, z, block) {
18091
18281
  if (typeof posOrX == "number") {
18092
18282
  return this.setBlockAt(new Vector318(posOrX, yOrBlock, z), block);
@@ -18125,14 +18315,15 @@ var Chunk = class _Chunk {
18125
18315
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18126
18316
  } : void 0
18127
18317
  }));
18128
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18129
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18130
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18131
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18132
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18133
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18134
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18135
- }));
18318
+ const states = /* @__PURE__ */ new Map();
18319
+ for (const { index, key, block } of placements) {
18320
+ if (key === void 0) continue;
18321
+ let state = states.get(key);
18322
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18323
+ state.indices.push(index);
18324
+ }
18325
+ const biomes = /* @__PURE__ */ new Map();
18326
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18136
18327
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18137
18328
  const positions = [];
18138
18329
  let failed = false, failure;
@@ -18156,7 +18347,8 @@ var Chunk = class _Chunk {
18156
18347
  if (!readBlock) continue;
18157
18348
  const resolved = resolutions.get(key);
18158
18349
  if (resolved.status === "rejected") throw resolved.reason;
18159
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18350
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18351
+ const context = contexts.get(index);
18160
18352
  if (!blockState3) {
18161
18353
  this.data.assign(index, 0);
18162
18354
  continue;
@@ -18164,9 +18356,10 @@ var Chunk = class _Chunk {
18164
18356
  const variantPosition = worldPos.toArray();
18165
18357
  if (fluid) {
18166
18358
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18359
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18167
18360
  this.meshDirty = true;
18168
18361
  }
18169
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18362
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18170
18363
  if (templates) {
18171
18364
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18172
18365
  this.meshDirty = true;
@@ -18176,7 +18369,9 @@ var Chunk = class _Chunk {
18176
18369
  instanceMeshes: true,
18177
18370
  maxInstanceCount: 2e3,
18178
18371
  initialState: stored.properties,
18179
- variantPosition
18372
+ variantPosition,
18373
+ biome: context?.biome,
18374
+ biomePosition: this.biomePosition(index, blockState3, stored)
18180
18375
  });
18181
18376
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18182
18377
  }
@@ -18199,24 +18394,21 @@ var Chunk = class _Chunk {
18199
18394
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18200
18395
  this.sectionBlocks.clear();
18201
18396
  this.fluidCells.fill(0);
18397
+ this.waterTints = void 0;
18202
18398
  this.renderedBlocks.clear();
18203
18399
  this.hiddenBlocks.clear();
18204
18400
  this.meshDirty = true;
18205
18401
  this.data.copyFrom(data);
18206
- const ids = /* @__PURE__ */ new Set();
18402
+ const ids = /* @__PURE__ */ new Map();
18207
18403
  for (let index = 0; index < 4096; index++) {
18208
18404
  const id = this.data.idAt(index);
18209
- if (id) ids.add(id);
18210
- }
18211
- const palette = [...ids];
18212
- const settled = await Promise.allSettled(palette.map(async (id) => {
18213
- const state = this.data.stateAt(id);
18214
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18215
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18216
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18217
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18218
- return { blockState: blockState3, perBlock, fluid, prepared };
18219
- }));
18405
+ if (!id) continue;
18406
+ let indices = ids.get(id);
18407
+ if (!indices) ids.set(id, indices = []);
18408
+ indices.push(index);
18409
+ }
18410
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18411
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18220
18412
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18221
18413
  const perBlocks = [];
18222
18414
  let failed = false, failure;
@@ -18226,24 +18418,28 @@ var Chunk = class _Chunk {
18226
18418
  try {
18227
18419
  const resolved = resolutions.get(id);
18228
18420
  if (resolved.status === "rejected") throw resolved.reason;
18229
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18421
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18422
+ const context = contexts.get(index);
18230
18423
  if (!blockState3) {
18231
18424
  this.data.assign(index, 0);
18232
18425
  this.fluidCells[index] = 0;
18233
18426
  continue;
18234
18427
  }
18235
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18428
+ if (fluid) {
18429
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18430
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18431
+ }
18236
18432
  const variantPosition = [
18237
18433
  this.x * 16 + index % 16,
18238
18434
  this.y * 16 + Math.floor(index / 256),
18239
18435
  this.z * 16 + Math.floor(index / 16) % 16
18240
18436
  ];
18241
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18437
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18242
18438
  if (templates) {
18243
18439
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18244
18440
  this.meshDirty = true;
18245
18441
  } else {
18246
- perBlocks.push({ index, blockState: blockState3 });
18442
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18247
18443
  }
18248
18444
  const readBlock = this.data.snapshot(index);
18249
18445
  this.renderedBlocks.set(index, { get block() {
@@ -18256,7 +18452,7 @@ var Chunk = class _Chunk {
18256
18452
  failed = true;
18257
18453
  }
18258
18454
  }
18259
- for (const { index, blockState: blockState3 } of perBlocks) {
18455
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18260
18456
  let object4;
18261
18457
  try {
18262
18458
  object4 = await this.scene.addBlock(blockState3, {
@@ -18264,11 +18460,9 @@ var Chunk = class _Chunk {
18264
18460
  instanceMeshes: true,
18265
18461
  maxInstanceCount: 2e3,
18266
18462
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18267
- variantPosition: [
18268
- this.x * 16 + index % 16,
18269
- this.y * 16 + Math.floor(index / 256),
18270
- this.z * 16 + Math.floor(index / 16) % 16
18271
- ]
18463
+ variantPosition: this.positionAt(index),
18464
+ biome,
18465
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18272
18466
  });
18273
18467
  object4.setPosition(new Vector318(
18274
18468
  this.x * 256 + index % 16 * 16,
@@ -18287,6 +18481,68 @@ var Chunk = class _Chunk {
18287
18481
  }
18288
18482
  if (failed) throw failure;
18289
18483
  }
18484
+ positionAt(index) {
18485
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18486
+ }
18487
+ biomePosition(index, blockState3, stored) {
18488
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18489
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18490
+ return position;
18491
+ }
18492
+ async resolveState(stored, indices, biomes) {
18493
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18494
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18495
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18496
+ 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;
18497
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18498
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18499
+ const contexts = /* @__PURE__ */ new Map();
18500
+ const groups = /* @__PURE__ */ new Map();
18501
+ if (blockState3 && (source || fluid?.kind === "water")) {
18502
+ for (const index of indices) {
18503
+ const position = this.biomePosition(index, blockState3, stored);
18504
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18505
+ if (!biome) continue;
18506
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18507
+ let group = groups.get(key);
18508
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18509
+ group.indices.push(index);
18510
+ }
18511
+ }
18512
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18513
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18514
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18515
+ const available = () => {
18516
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18517
+ let found = biomes.get(key);
18518
+ if (!found) {
18519
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18520
+ biomes.set(key, found);
18521
+ }
18522
+ return found;
18523
+ };
18524
+ const context = { biome };
18525
+ if (perBlock) {
18526
+ if (!await available()) context.biome = void 0;
18527
+ } else {
18528
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18529
+ if (fluid?.kind === "water" && await available()) {
18530
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18531
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new Color13(color2).toArray();
18532
+ }
18533
+ }
18534
+ for (const index of indices2) contexts.set(index, context);
18535
+ }));
18536
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18537
+ }
18538
+ setWaterTint(index, tint) {
18539
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18540
+ if (!this.waterTints) {
18541
+ this.waterTints = new Float32Array(4096 * 3);
18542
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18543
+ }
18544
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18545
+ }
18290
18546
  /** Changes visibility at world block coordinates while preserving the block's data. */
18291
18547
  async setBlockVisibleAt(pos, visible) {
18292
18548
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18390,7 +18646,7 @@ var Chunk = class _Chunk {
18390
18646
  if (block && !block.object) origins.set(kind, block.block.type);
18391
18647
  }
18392
18648
  if (origins.size) {
18393
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18649
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18394
18650
  for (let y = -1; y <= 16; y++) {
18395
18651
  for (let z = -1; z <= 16; z++) {
18396
18652
  for (let x = -1; x <= 16; x++) {
@@ -18432,8 +18688,13 @@ var Chunk = class _Chunk {
18432
18688
  this.renderedBlocks.clear();
18433
18689
  this.hiddenBlocks.clear();
18434
18690
  this.fluidCells.fill(0);
18691
+ this.waterTints = void 0;
18435
18692
  this.data.clear();
18436
- if (notify) await this.onBlocksChanged?.(positions);
18693
+ this.biomes = void 0;
18694
+ if (notify) {
18695
+ await this.onBiomesChanged?.(void 0);
18696
+ await this.onBlocksChanged?.(positions);
18697
+ }
18437
18698
  }
18438
18699
  async dispose() {
18439
18700
  await this.clear();
@@ -18654,6 +18915,7 @@ export {
18654
18915
  BannerPatterns,
18655
18916
  BasicAssetKey,
18656
18917
  BatchedExecutor,
18918
+ Biomes,
18657
18919
  BlockEntities,
18658
18920
  BlockObject,
18659
18921
  BlockStateResolver,