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.
@@ -280,7 +280,8 @@ var init_Caching = __esm({
280
280
  this.fontCache,
281
281
  this.bannerPatternCache,
282
282
  this.mineRenderDataCache,
283
- this.usernameUuidCache
283
+ this.usernameUuidCache,
284
+ this.biomeCache
284
285
  ];
285
286
  }
286
287
  /** Invalidates in-memory entries so later lookups reload them. Persisted entries are retained. */
@@ -331,6 +332,7 @@ var init_Caching = __esm({
331
332
  _Caching.bannerPatternCache = _Caching.createAssetCache();
332
333
  _Caching.mineRenderDataCache = _Caching.createAssetCache();
333
334
  _Caching.usernameUuidCache = import_loading_cache.Caches.builder().expireAfterWrite(import_time.Time.minutes(5)).expirationInterval(import_time.Time.seconds(30)).buildAsync();
335
+ _Caching.biomeCache = _Caching.createAssetCache();
334
336
  Caching = _Caching;
335
337
  }
336
338
  });
@@ -1605,7 +1607,8 @@ var init_MineRenderData = __esm({
1605
1607
  }
1606
1608
  /**
1607
1609
  * Uses the exact version when published. Otherwise, selects the nearest patch in the same
1608
- * release line (older on ties), then the latest earlier release. Unpublished snapshots reject.
1610
+ * release line (older on ties), then the latest earlier release or the oldest published release.
1611
+ * Unpublished snapshots reject.
1609
1612
  */
1610
1613
  static async resolve(root) {
1611
1614
  const requestRoot = root?.replace(/\/+$/, "");
@@ -1614,11 +1617,11 @@ var init_MineRenderData = __esm({
1614
1617
  const scope = AssetLoader.persistentScope;
1615
1618
  return this.cached(`resolve:${requestRoot === void 0 ? "sources" : "explicit"}:${root}`, scope, defaultRoot, async () => {
1616
1619
  const requestedVersion = root.slice(root.lastIndexOf("/") + 1);
1617
- let manifest = await this.manifest(root, release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1620
+ let manifest = await this.manifest(root, requestRoot !== void 0 && release(requestedVersion) ? requestedVersion : void 0, requestRoot);
1618
1621
  if (manifest) return { requestedVersion, version: manifest.minecraftVersion, root, manifest };
1619
1622
  const requested = release(requestedVersion);
1620
- if (!requested || compare(requested, [1, 16, 5]) < 0) {
1621
- throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release from 1.16.5 onward`);
1623
+ if (!requested) {
1624
+ throw new Error(`No MineRender data is published for ${requestedVersion}; automatic fallback requires a release`);
1622
1625
  }
1623
1626
  const base = root.slice(0, root.lastIndexOf("/"));
1624
1627
  const index = await this.cached(`versions:${base}`, scope, defaultRoot, async () => {
@@ -1628,9 +1631,9 @@ var init_MineRenderData = __esm({
1628
1631
  }
1629
1632
  return value.versions;
1630
1633
  });
1631
- 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));
1634
+ const versions = index.map((entry) => ({ name: entry.name, number: release(entry.name) })).filter((entry) => !!entry.number).sort((left, right) => compare(left.number, right.number));
1632
1635
  const sameLine = versions.filter((entry) => entry.number[0] === requested[0] && entry.number[1] === requested[1]);
1633
- 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();
1636
+ 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];
1634
1637
  if (!selected) throw new Error(`No compatible MineRender data is published for ${requestedVersion}`);
1635
1638
  const resolvedRoot = `${base}/${selected.name}`;
1636
1639
  manifest = await this.manifest(resolvedRoot, selected.name, resolvedRoot);
@@ -1677,7 +1680,7 @@ ${key}` : key, async () => {
1677
1680
  case "blockStates":
1678
1681
  return Object.values(value).every((property) => object(property) && "default" in property && Array.isArray(property.values));
1679
1682
  case "blockTints":
1680
- return ["constant", "grass", "stem", "redstone"].includes(value.source) && (value.source !== "constant" || Number.isInteger(value.color));
1683
+ 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));
1681
1684
  case "fluids":
1682
1685
  return ["water", "lava"].includes(value.kind) && typeof value.renderModel === "boolean" && (value.levelProperty === void 0 || typeof value.levelProperty === "string");
1683
1686
  case "dyes":
@@ -1746,12 +1749,7 @@ var init_BannerPatterns = __esm({
1746
1749
  static async getLayers(value, target, root) {
1747
1750
  if (value === void 0) return [];
1748
1751
  if (!Array.isArray(value)) throw new Error("banner_patterns must be an array");
1749
- const activeRoot = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
1750
- const checkSources = () => {
1751
- if (activeRoot !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading banner patterns; retry the request");
1752
- };
1753
1752
  const colors = value.length ? await this.getColors(root) : void 0;
1754
- checkSources();
1755
1753
  return Promise.all(value.slice(0, 16).map(async (layer) => {
1756
1754
  if (!layer || typeof layer !== "object" || Array.isArray(layer)) throw new Error("Each banner pattern requires a pattern and dye color");
1757
1755
  if (typeof layer.color !== "string" || !Object.prototype.hasOwnProperty.call(colors, layer.color)) throw new Error(`Unsupported dye color ${layer.color}`);
@@ -1761,7 +1759,6 @@ var init_BannerPatterns = __esm({
1761
1759
  const id = this.identifier(pattern2);
1762
1760
  const key = new AssetKey(id.namespace, id.getFullPath(), "banner_pattern", void 0, "data", ".json", root);
1763
1761
  pattern2 = await Caching.bannerPatternCache.get(key.serialize(), () => AssetLoader.get(key, "json" /* JSON */));
1764
- checkSources();
1765
1762
  if (!pattern2) throw new Error(`Missing banner pattern ${layer.pattern}`);
1766
1763
  }
1767
1764
  if (!pattern2 || typeof pattern2 !== "object" || Array.isArray(pattern2) || typeof pattern2.translation_key !== "string") throw new Error("Banner patterns require asset_id and translation_key");
@@ -1821,15 +1818,19 @@ var init_Colormaps = __esm({
1821
1818
  Colormaps = class {
1822
1819
  /** Samples the selected resource pack's grass colormap using Minecraft's climate coordinates. */
1823
1820
  static async grassColor(origin, temperature = 0.5, downfall = 1) {
1824
- const key = AssetKey.parse("textures", "minecraft:colormap/grass", origin);
1821
+ return this.getColor("grass", origin, temperature, downfall);
1822
+ }
1823
+ /** Samples a resource-pack colormap at the supplied climate coordinates. */
1824
+ static async getColor(kind, origin, temperature = 0.5, downfall = 1) {
1825
+ const key = AssetKey.parse("textures", `minecraft:colormap/${kind}`, origin);
1825
1826
  const temp = Math.fround(temperature);
1826
1827
  const rain = Math.fround(downfall) * temp;
1827
1828
  const x = Math.floor((1 - temp) * 255), y = Math.floor((1 - rain) * 255);
1828
1829
  return await Caching.blockTintCache.get(`${key.serialize()}|${x},${y}`, async () => {
1829
1830
  const image = await ModelTextures.get(key);
1830
- if (!image) throw new MineRenderError(`Missing grass colormap: ${key.toNamespacedString()}`);
1831
+ if (!image) throw new MineRenderError(`Missing ${kind} colormap: ${key.toNamespacedString()}`);
1831
1832
  if (image.width !== 256 || image.height !== 256) {
1832
- throw new MineRenderError("Grass colormaps must be 256 \xD7 256 pixels");
1833
+ throw new MineRenderError(`${kind} colormaps must be 256 \xD7 256 pixels`);
1833
1834
  }
1834
1835
  const [red, green, blue] = image.data.getImageData(x, y, 1, 1).data;
1835
1836
  return red << 16 | green << 8 | blue;
@@ -2042,30 +2043,24 @@ var init_Models = __esm({
2042
2043
  }).then((r) => r?.files ?? []);
2043
2044
  }
2044
2045
  static async loadAndMerge(key, context = {}) {
2045
- const checkSources = this.sourceCheck();
2046
2046
  const model = key.type === "item" ? await this.getItemModel(key, context) : await this.getRaw(key);
2047
2047
  if (!model) {
2048
2048
  return void 0;
2049
2049
  }
2050
2050
  const merged = await ModelMerger.mergeWithParents(model);
2051
- checkSources();
2052
2051
  return merged;
2053
2052
  }
2054
2053
  static async getItemModel(key, context = {}) {
2055
- const checkSources = this.sourceCheck();
2056
2054
  const preview = this.snapshotContext(key, context);
2057
2055
  const dataKey = await this.applyDefaults(key, preview);
2058
- checkSources();
2059
2056
  return this.getPreparedItemModel(key, preview, dataKey);
2060
2057
  }
2061
2058
  static async getPreparedItemModel(key, preview, dataKey) {
2062
- const checkSources = this.sourceCheck();
2063
2059
  const itemId = `${key.namespace}:${key.path}`;
2064
2060
  const itemKey2 = new AssetKey(key.namespace, key.path, "items", void 0, key.rootType, ".json", key.root);
2065
2061
  const cacheKey = itemKey2.serialize() + this.contextKey(preview) + dataKey;
2066
2062
  const model = await this.PERSISTENT_CACHE.getOrLoad(`${this.ITEM_CACHE_VERSION}:${AssetLoader.persistentKey(cacheKey)}`, async () => {
2067
2063
  const result = await AssetLoader.getFirst([itemKey2, key], "json" /* JSON */);
2068
- checkSources();
2069
2064
  if (!result) return void 0;
2070
2065
  if (result.key.assetType !== "items") return { ...result.asset, key, itemId, components: preview.components };
2071
2066
  const load = async (selected) => {
@@ -2084,10 +2079,8 @@ var init_Models = __esm({
2084
2079
  return { ...model3, itemId, components: preview.components, ...selected.special && { special: selected.special }, ...selected.tints && { tints: selected.tints } };
2085
2080
  };
2086
2081
  const model2 = await load(this.selectItemModel(result.asset.model, key, preview));
2087
- checkSources();
2088
2082
  return model2;
2089
2083
  });
2090
- checkSources();
2091
2084
  const restore = (model2) => ({
2092
2085
  ...model2,
2093
2086
  key: Object.assign(new AssetKey("", ""), model2.key),
@@ -2151,22 +2144,14 @@ var init_Models = __esm({
2151
2144
  }
2152
2145
  static async applyDefaults(key, preview) {
2153
2146
  if (key.type !== "item") return "";
2154
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2155
2147
  const resolved = await MineRenderData.resolve(key.root);
2156
2148
  const defaults = await ItemDefaults.get(`${key.namespace}:${key.path}`, key.root);
2157
2149
  preview.components = { ...defaults, ...preview.components };
2158
2150
  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)) {
2159
2151
  preview.components["minecraft:enchantment_glint_override"] = true;
2160
2152
  }
2161
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item defaults; retry the request");
2162
2153
  return `|data:${resolved.root}:${resolved.manifest.datasets.itemDefaults?.sha256}:${resolved.manifest.datasets.potionColors?.sha256}`;
2163
2154
  }
2164
- static sourceCheck() {
2165
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
2166
- return () => {
2167
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) throw new Error("Asset sources changed while loading item models; retry the request");
2168
- };
2169
- }
2170
2155
  static contextKey(context) {
2171
2156
  const itemReferences = Object.fromEntries(Object.entries(context.itemReferences).map(([id, key]) => [id, key.serialize()]));
2172
2157
  return `|${this.ITEM_CACHE_VERSION}:${JSON.stringify(this.snapshotJson({ ...context, itemReferences }))}`;
@@ -2353,7 +2338,6 @@ var init_Models = __esm({
2353
2338
  * @param key - Model key, for example `AssetKey.parse("models", "minecraft:item/diamond_sword")`.
2354
2339
  */
2355
2340
  static async getMerged(key, context = {}) {
2356
- const checkSources = this.sourceCheck();
2357
2341
  if (!key.assetType) {
2358
2342
  key.assetType = "models";
2359
2343
  }
@@ -2362,15 +2346,12 @@ var init_Models = __esm({
2362
2346
  }
2363
2347
  const preview = this.snapshotContext(key, context);
2364
2348
  const dataKey = await this.applyDefaults(key, preview);
2365
- checkSources();
2366
2349
  const keyStr = AssetLoader.persistentKey(key.serialize() + (key.type === "item" ? this.contextKey(preview) + dataKey : ""));
2367
2350
  const model = await Caching.mergedModelCache.get(keyStr, async () => {
2368
2351
  const raw = key.type === "item" ? await this.getPreparedItemModel(key, preview, dataKey) : await this.getRaw(key);
2369
2352
  const merged = raw ? await ModelMerger.mergeWithParents(raw) : void 0;
2370
- checkSources();
2371
2353
  return merged;
2372
2354
  });
2373
- checkSources();
2374
2355
  return model;
2375
2356
  }
2376
2357
  /** Alias for {@link getMerged}. */
@@ -7516,6 +7497,7 @@ __export(index_browser_exports, {
7516
7497
  BannerPatterns: () => BannerPatterns,
7517
7498
  BasicAssetKey: () => BasicAssetKey,
7518
7499
  BatchedExecutor: () => BatchedExecutor,
7500
+ Biomes: () => Biomes,
7519
7501
  BlockEntities: () => BlockEntities,
7520
7502
  BlockObject: () => BlockObject,
7521
7503
  BlockStateResolver: () => BlockStateResolver,
@@ -7905,6 +7887,116 @@ init_AssetLoader();
7905
7887
  init_Assets();
7906
7888
  init_BannerPatterns();
7907
7889
 
7890
+ // src/assets/Biomes.ts
7891
+ init_AssetKey();
7892
+ init_AssetLoader();
7893
+ init_AssetParsers();
7894
+ init_Caching();
7895
+ init_MineRenderError();
7896
+ init_Colormaps();
7897
+
7898
+ // src/assets/_biomes/SwampGrassColor.ts
7899
+ var permutation;
7900
+ 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]];
7901
+ var skew = (Math.sqrt(3) - 1) / 2;
7902
+ var unskew = (3 - Math.sqrt(3)) / 6;
7903
+ function createPermutation() {
7904
+ const multiplier = 0x5deece66dn, mask = (1n << 48n) - 1n;
7905
+ let seed = 2345n ^ multiplier;
7906
+ const next = (bits) => {
7907
+ seed = seed * multiplier + 11n & mask;
7908
+ return Number(seed >> BigInt(48 - bits));
7909
+ };
7910
+ for (let i = 0; i < 3; i++) {
7911
+ next(26);
7912
+ next(27);
7913
+ }
7914
+ const values = Uint8Array.from({ length: 256 }, (_, i) => i);
7915
+ for (let i = 0; i < 256; i++) {
7916
+ const bound = 256 - i;
7917
+ let offset;
7918
+ if ((bound & bound - 1) === 0) offset = Math.floor(bound * next(31) / 2147483648);
7919
+ else {
7920
+ let bits;
7921
+ do {
7922
+ bits = next(31);
7923
+ offset = bits % bound;
7924
+ } while (bits - offset + bound - 1 > 2147483647);
7925
+ }
7926
+ const other = i + offset;
7927
+ [values[i], values[other]] = [values[other], values[i]];
7928
+ }
7929
+ return values;
7930
+ }
7931
+ function swampGrassColor(x, z) {
7932
+ const p10 = permutation ?? (permutation = createPermutation());
7933
+ x *= 0.0225;
7934
+ z *= 0.0225;
7935
+ const s = (x + z) * skew;
7936
+ const i = Math.floor(x + s), j = Math.floor(z + s);
7937
+ const t = (i + j) * unskew;
7938
+ const dx = x - (i - t), dz = z - (j - t);
7939
+ const stepX = dx > dz ? 1 : 0, stepZ = 1 - stepX;
7940
+ const corner = (gx, gz, a, b) => {
7941
+ let falloff = 0.5 - a * a - b * b;
7942
+ if (falloff < 0) return 0;
7943
+ const gradient = gradients[p10[gx + p10[gz & 255] & 255] % 12];
7944
+ falloff *= falloff;
7945
+ return falloff * falloff * (gradient[0] * a + gradient[1] * b);
7946
+ };
7947
+ 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));
7948
+ return noise < -0.1 ? 5011004 : 6975545;
7949
+ }
7950
+
7951
+ // src/assets/Biomes.ts
7952
+ var Biomes = class {
7953
+ /** Loads a biome registry entry, or returns `undefined` when no source provides it. */
7954
+ static async get(id, origin) {
7955
+ if (!/^(?:[a-z0-9_.-]+:)?[a-z0-9_./-]+$/.test(id)) throw new MineRenderError(`Invalid biome identifier: ${id}`);
7956
+ const [namespace, path] = id.includes(":") ? id.split(":") : ["minecraft", id];
7957
+ const key = new AssetKey(namespace, path, "worldgen", "biome", "data", ".json", origin?.root);
7958
+ return Caching.biomeCache.get(key.serialize(), async () => {
7959
+ const asset2 = await AssetLoader.get(key, "json" /* JSON */);
7960
+ if (asset2 === void 0) return void 0;
7961
+ if (!asset2 || typeof asset2 !== "object" || Array.isArray(asset2) || !asset2.effects || typeof asset2.effects !== "object" || Array.isArray(asset2.effects)) {
7962
+ throw new MineRenderError(`Invalid biome effects: ${id}`);
7963
+ }
7964
+ if (typeof asset2.temperature !== "number" || !Number.isFinite(asset2.temperature) || typeof asset2.downfall !== "number" || !Number.isFinite(asset2.downfall)) {
7965
+ throw new MineRenderError(`Invalid biome climate: ${id}`);
7966
+ }
7967
+ for (const kind of ["grass", "foliage", "dry_foliage", "water"]) {
7968
+ const color2 = asset2.effects[`${kind}_color`];
7969
+ if (color2 !== void 0) this.parseColor(color2, id, kind);
7970
+ }
7971
+ const modifier = asset2.effects.grass_color_modifier;
7972
+ if (modifier !== void 0 && modifier !== "none" && modifier !== "dark_forest" && modifier !== "swamp") {
7973
+ throw new MineRenderError(`Invalid biome grass color modifier: ${id}`);
7974
+ }
7975
+ return asset2;
7976
+ });
7977
+ }
7978
+ /** Returns packed sRGB, or `undefined` when the biome or requested water color is missing. */
7979
+ static async getColor(id, kind, origin, x = 0, z = 0) {
7980
+ const biome = await this.get(id, origin);
7981
+ if (!biome) return void 0;
7982
+ const rawColor = biome.effects[`${kind}_color`];
7983
+ let color2 = rawColor === void 0 ? void 0 : this.parseColor(rawColor, id, kind);
7984
+ if (kind === "water") return color2;
7985
+ const modifier = kind === "grass" ? biome.effects.grass_color_modifier : void 0;
7986
+ if (modifier === "swamp") return swampGrassColor(x, z);
7987
+ if (color2 === void 0) {
7988
+ const clamp = (value) => Math.max(0, Math.min(1, Math.fround(value)));
7989
+ color2 = await Colormaps.getColor(kind, origin, clamp(biome.temperature), clamp(biome.downfall));
7990
+ }
7991
+ return modifier === "dark_forest" ? (color2 & 16711422) + 2634762 >> 1 : color2;
7992
+ }
7993
+ static parseColor(value, id, kind) {
7994
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) return parseInt(value.slice(1), 16);
7995
+ if (typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 16777215) return value;
7996
+ throw new MineRenderError(`Invalid biome ${kind} color: ${id}`);
7997
+ }
7998
+ };
7999
+
7908
8000
  // src/assets/BlockEntities.ts
7909
8001
  var import_three10 = require("three");
7910
8002
  init_AssetKey();
@@ -12343,8 +12435,20 @@ var BlockStateResolver = class _BlockStateResolver {
12343
12435
  init_Colormaps();
12344
12436
  init_MineRenderData();
12345
12437
  var BlockTints = class {
12346
- /** Resolves block preview colors without biome context; explicit tints take precedence. */
12347
- static async get(key, state, model, tints) {
12438
+ /** Returns a block's versioned biome color source, leaving fixed-color blocks unchanged. */
12439
+ static async biomeSource(key) {
12440
+ const rule = key && (await MineRenderData.get("blockTints", key.root))[key.toNamespacedString()];
12441
+ return rule ? rule.biome ?? (rule.source === "grass" ? "grass" : void 0) : void 0;
12442
+ }
12443
+ /** Resolves one block's biome tint at world block coordinates, without biome blending. */
12444
+ static async getBiomeColor(key, biome, position) {
12445
+ if (!biome) return void 0;
12446
+ const x = position?.[0], z = position?.[2];
12447
+ const source = await this.biomeSource(key);
12448
+ return source ? Biomes.getColor(biome, source, key, x, z) : void 0;
12449
+ }
12450
+ /** Resolves used tint indices from biome or preview colors; explicit tints take precedence. */
12451
+ static async get(key, state, model, tints, biomeColor) {
12348
12452
  if (!key) return tints;
12349
12453
  const candidates = (model.elements ?? []).flatMap((element) => Object.values(element.faces)).some((face) => face?.tintindex !== void 0 && face.tintindex >= 0 && tints?.[face.tintindex] === void 0);
12350
12454
  if (!candidates) return tints;
@@ -12360,8 +12464,12 @@ var BlockTints = class {
12360
12464
  }
12361
12465
  }
12362
12466
  if (!indices.size) return tints;
12467
+ const source = rule.biome ?? (rule.source === "grass" ? "grass" : void 0);
12468
+ const biome = source ? typeof biomeColor === "function" ? await biomeColor() : biomeColor : void 0;
12363
12469
  let color2;
12364
- switch (rule.source) {
12470
+ if (biome !== void 0) {
12471
+ color2 = biome;
12472
+ } else switch (rule.source) {
12365
12473
  case "constant":
12366
12474
  color2 = rule.color;
12367
12475
  break;
@@ -12398,7 +12506,6 @@ init_AssetKey();
12398
12506
  init_ModelCulling();
12399
12507
  init_FluidGeometry();
12400
12508
  init_MineRenderData();
12401
- init_AssetLoader();
12402
12509
  init_Entities();
12403
12510
  var _BlockObject = class _BlockObject extends SceneObject {
12404
12511
  constructor(blockState3, options) {
@@ -12420,21 +12527,15 @@ var _BlockObject = class _BlockObject extends SceneObject {
12420
12527
  this._entityGeneration = 0;
12421
12528
  this.options = (0, import_ts_deepmerge6.default)({}, _BlockObject.DEFAULT_OPTIONS, options ?? {});
12422
12529
  this.variantPosition = options?.variantPosition ? [...options.variantPosition] : void 0;
12530
+ this.biomePosition = options?.biomePosition ? [...options.biomePosition] : void 0;
12423
12531
  }
12424
12532
  async init() {
12425
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
12426
- const checkSources = () => {
12427
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading a block; retry the request");
12428
- };
12429
12533
  this._fluidRules = await MineRenderData.get("fluids", this.blockState.key?.root);
12430
- checkSources();
12431
12534
  if (this.options.applyDefaultState) {
12432
12535
  this._setState(await BlockStateResolver.defaults(this.blockState));
12433
- checkSources();
12434
12536
  }
12435
12537
  if (this.options.initialState !== void 0) this._setState(this.options.initialState);
12436
12538
  await this.recreateModels();
12437
- checkSources();
12438
12539
  }
12439
12540
  dispose() {
12440
12541
  this.clearModels();
@@ -12612,11 +12713,16 @@ var _BlockObject = class _BlockObject extends SceneObject {
12612
12713
  const { FluidModelObject: FluidModelObject2, sampleFluid: sampleFluid2 } = await Promise.resolve().then(() => (init_FluidModelObject(), FluidModelObject_exports));
12613
12714
  this._fluidSampler = sample;
12614
12715
  const surface = sampleFluid2(kind, (x, y, z) => x === 0 && y === 0 && z === 0 ? { fluid: kind, level: this.fluidLevel } : sample?.(x, y, z) ?? {});
12615
- if (this._fluidKey === surface.key) return;
12716
+ const tint = this.options.tints?.[0] ?? this.waterTint;
12717
+ const fluidKey = `${surface.key}|${tint ?? "default"}`;
12718
+ if (this._fluidKey === fluidKey) return;
12616
12719
  const key = new AssetKey("minecraft", `${kind}/${surface.key}`, "models", "fluid", "assets", ".json", this.blockState.key?.root);
12617
12720
  const replacement = await this.scene.addSceneObject(
12618
12721
  { key },
12619
- () => new FluidModelObject2(kind, surface.sample, this.blockState.key, this.options)
12722
+ () => new FluidModelObject2(kind, surface.sample, this.blockState.key, {
12723
+ ...this.options,
12724
+ tints: tint === void 0 ? this.options.tints : { ...this.options.tints, 0: tint }
12725
+ })
12620
12726
  );
12621
12727
  const previous = this._fluidModel;
12622
12728
  const matrix = previous ? this.getModelMatrix(previous) : new import_three34.Matrix4().makeTranslation(...this.position.toArray());
@@ -12625,7 +12731,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12625
12731
  this._models = this._models.filter((model) => model !== previous);
12626
12732
  this._models.push(replacement);
12627
12733
  this._fluidModel = replacement;
12628
- this._fluidKey = surface.key;
12734
+ this._fluidKey = fluidKey;
12629
12735
  this._isInstanced || (this._isInstanced = isInstanceReference(replacement));
12630
12736
  this._instanceCounter = this._isInstanced ? 1 : 0;
12631
12737
  if (previous) this.removeModels([previous]);
@@ -12648,6 +12754,7 @@ var _BlockObject = class _BlockObject extends SceneObject {
12648
12754
  }
12649
12755
  async recreateModels() {
12650
12756
  this.clearModels();
12757
+ 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;
12651
12758
  if (resolveBlockFluidState(this.blockState.key, this.state, this._fluidRules)?.renderModel === false) {
12652
12759
  await this.updateFluid(this._fluidSampler);
12653
12760
  return;
@@ -12682,7 +12789,13 @@ var _BlockObject = class _BlockObject extends SceneObject {
12682
12789
  const model = await Models.getMerged(modelKey);
12683
12790
  const options = {
12684
12791
  ...this.options,
12685
- tints: model ? await BlockTints.get(this.blockState.key, this.state, model, this.options.tints) : this.options.tints,
12792
+ tints: model ? await BlockTints.get(
12793
+ this.blockState.key,
12794
+ this.state,
12795
+ model,
12796
+ this.options.tints,
12797
+ () => BlockTints.getBiomeColor(this.blockState.key, this.options.biome, this.biomePosition)
12798
+ ) : this.options.tints,
12686
12799
  uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0,
12687
12800
  cullMask: this.options.displayPosition ? 0 : ModelCulling.toLocalMask(this._cullMask, rotation)
12688
12801
  };
@@ -16337,7 +16450,6 @@ init_util2();
16337
16450
  // src/world/AnvilParser.ts
16338
16451
  init_MineRenderError();
16339
16452
  init_MineRenderData();
16340
- init_AssetLoader();
16341
16453
  init_NBTHelper();
16342
16454
 
16343
16455
  // src/world/ChunkData.ts
@@ -16608,14 +16720,12 @@ var AnvilParser = class {
16608
16720
  /** Decodes all stored chunks. Gzip, zlib, LZ4, and uncompressed payloads are supported. */
16609
16721
  static async parse(data, options = {}) {
16610
16722
  options = { ...options };
16611
- const checkSources = this.assetGuard();
16612
16723
  options.signal?.throwIfAborted();
16613
16724
  const bytes = this.bytes(data);
16614
16725
  const chunks = [];
16615
16726
  for (const location of this.locations(bytes)) {
16616
16727
  chunks.push(await this.abortable(this.readChunk(bytes, location, options), options.signal));
16617
16728
  }
16618
- checkSources();
16619
16729
  return { chunks };
16620
16730
  }
16621
16731
  /**
@@ -16626,22 +16736,12 @@ var AnvilParser = class {
16626
16736
  */
16627
16737
  static async parseChunk(data, localX, localZ, options = {}) {
16628
16738
  options = { ...options };
16629
- const checkSources = this.assetGuard();
16630
16739
  options.signal?.throwIfAborted();
16631
16740
  const bytes = this.bytes(data);
16632
16741
  const location = this.location(bytes, localX, localZ);
16633
16742
  const chunk = location ? await this.abortable(this.readChunk(bytes, location, options), options.signal) : void 0;
16634
- checkSources();
16635
16743
  return chunk;
16636
16744
  }
16637
- static assetGuard() {
16638
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
16639
- return () => {
16640
- if (AssetLoader.ROOT !== root || AssetLoader.persistentScope !== scope) {
16641
- throw new MineRenderError("Asset sources changed while parsing Anvil data; retry the request");
16642
- }
16643
- };
16644
- }
16645
16745
  /**
16646
16746
  * Decodes one chunk from an `entities/r.x.z.mca` file without migrating its NBT.
16647
16747
  * @param localX - Chunk x within the region, from 0 to 31.
@@ -17163,24 +17263,48 @@ var SectionModels = class {
17163
17263
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("maxAtlasSize must be a positive integer");
17164
17264
  }
17165
17265
  /** Returns templates for the matched parts, or `undefined` when the block needs an individual render object. */
17166
- async get(blockState3, properties = {}, position) {
17167
- const variantPosition = position ? [...position] : void 0;
17168
- return (await this.prepareState(blockState3, properties))?.pick(variantPosition);
17266
+ async get(blockState3, properties = {}, position, biome) {
17267
+ const biomePosition = position ? [...position] : void 0;
17268
+ const prepared = await this.state(blockState3, properties);
17269
+ if (!prepared) return void 0;
17270
+ return Promise.all(prepared.groups.map(async (group) => {
17271
+ const variant = BlockStateResolver.choose(group, biomePosition);
17272
+ const choice = prepared.choices.get(variant);
17273
+ const tints = await BlockTints.get(
17274
+ blockState3.key,
17275
+ prepared.state,
17276
+ choice.model,
17277
+ void 0,
17278
+ () => BlockTints.getBiomeColor(blockState3.key, biome, biomePosition)
17279
+ );
17280
+ return this.template(variant, choice, tints);
17281
+ }));
17169
17282
  }
17170
- /** Resolves section templates once, with a synchronous weighted choice for each cell. */
17171
- async prepareState(blockState3, properties = {}) {
17283
+ /** Resolves section templates for one tint palette, with a synchronous weighted choice for each cell. */
17284
+ async prepareState(blockState3, properties = {}, biomeColor) {
17285
+ const prepared = await this.state(blockState3, properties);
17286
+ if (!prepared) return void 0;
17287
+ let color2;
17288
+ const resolveColor = typeof biomeColor === "function" ? () => color2 ?? (color2 = biomeColor()) : biomeColor;
17289
+ const templates = /* @__PURE__ */ new Map();
17290
+ await Promise.all([...prepared.choices].map(async ([variant, choice]) => {
17291
+ const tints = await BlockTints.get(blockState3.key, prepared.state, choice.model, void 0, resolveColor);
17292
+ templates.set(variant, await this.template(variant, choice, tints));
17293
+ }));
17294
+ return { pick: (position) => prepared.groups.map((group) => templates.get(BlockStateResolver.choose(group, position))) };
17295
+ }
17296
+ async state(blockState3, properties) {
17172
17297
  let states = this.states.get(blockState3);
17173
17298
  if (!states) this.states.set(blockState3, states = /* @__PURE__ */ new Map());
17174
17299
  const key = JSON.stringify(Object.entries(properties).sort(([a], [b]) => a.localeCompare(b)));
17175
- const prepared = await cached(states, key, () => this.prepare(blockState3, properties));
17176
- if (!prepared) return void 0;
17177
- return { pick: (position) => prepared.groups.map((group) => prepared.templates.get(BlockStateResolver.choose(group, position))) };
17300
+ return cached(states, key, () => this.prepare(blockState3, properties));
17178
17301
  }
17179
17302
  /** Returns the shared still/flow atlas used by per-block fluid objects. */
17180
17303
  fluidAtlas(kind, root) {
17181
- return cached(this.fluidAtlases, kind, async () => {
17304
+ const key = new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root);
17305
+ return cached(this.fluidAtlases, key.serialize(), async () => {
17182
17306
  const atlas = await UVMapper.getAtlas({
17183
- key: new AssetKey("minecraft", kind, "models", "fluid", "assets", ".json", root),
17307
+ key,
17184
17308
  textures: { still: `minecraft:block/${kind}_still`, flow: `minecraft:block/${kind}_flow` },
17185
17309
  elements: []
17186
17310
  });
@@ -17192,64 +17316,67 @@ var SectionModels = class {
17192
17316
  const state = { ...await BlockStateResolver.defaults(blockState3), ...properties };
17193
17317
  const groups = BlockStateResolver.matching(blockState3, state);
17194
17318
  if (!groups.length) return void 0;
17195
- const templates = /* @__PURE__ */ new Map();
17319
+ const prepared = /* @__PURE__ */ new Map();
17196
17320
  for (const group of groups) {
17197
17321
  const choices = Array.isArray(group) ? group : [group];
17198
17322
  if (!choices.length) return void 0;
17199
17323
  for (const variant of choices) {
17200
17324
  if (!variant.model) return void 0;
17201
- const model = await Models.getMerged(AssetKey.parse("models", variant.model));
17325
+ const modelKey = AssetKey.parse("models", variant.model);
17326
+ modelKey.root = blockState3.key?.root;
17327
+ const model = await Models.getMerged(modelKey);
17202
17328
  if (!model?.elements?.length) return void 0;
17203
17329
  const atlas = await UVMapper.getAtlas(model);
17204
17330
  const rotation = BlockStateResolver.rotation(variant);
17205
17331
  if (!atlas || atlas.image.width > this.maxAtlasSize || atlas.image.height > this.maxAtlasSize) return void 0;
17206
- const tints = await BlockTints.get(blockState3.key, state, model);
17207
- let models = this.models.get(model);
17208
- if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17209
- const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17210
- const template = await cached(models, key, async () => {
17211
- const object4 = new SectionModel(model, {
17212
- instanceMeshes: false,
17213
- mergeMeshes: true,
17214
- tints,
17215
- uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17216
- });
17217
- await object4.init();
17218
- let geometry = object4.children[0].geometry;
17219
- geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17220
- object4.clear();
17221
- const keep = [];
17222
- const cullFaces = [];
17223
- for (const element of atlas.model.elements) {
17224
- for (const face of CUBE_FACES) {
17225
- const data = element.faces[face];
17226
- keep.push(data !== void 0);
17227
- if (!data) continue;
17228
- const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17229
- if (local < 0) {
17230
- cullFaces.push(0);
17231
- continue;
17232
- }
17233
- const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17234
- 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);
17235
- cullFaces.push(world < 0 ? 0 : 1 << world);
17236
- }
17237
- }
17238
- geometry = compactQuads(geometry, keep);
17239
- const template2 = {
17240
- geometry,
17241
- atlas,
17242
- cullFaces: new Uint8Array(cullFaces),
17243
- occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17244
- layer: atlas.hasTranslucency ? 1 : 0
17245
- };
17246
- this.templates.add(template2);
17247
- return template2;
17248
- });
17249
- templates.set(variant, template);
17332
+ prepared.set(variant, { model, atlas, rotation });
17250
17333
  }
17251
17334
  }
17252
- return { groups, templates };
17335
+ return { state, groups, choices: prepared };
17336
+ }
17337
+ async template(variant, { model, atlas, rotation }, tints) {
17338
+ let models = this.models.get(model);
17339
+ if (!models) this.models.set(model, models = /* @__PURE__ */ new Map());
17340
+ const key = JSON.stringify([rotation.x, rotation.y, rotation.z, variant.uvlock, tints]);
17341
+ return cached(models, key, async () => {
17342
+ const object4 = new SectionModel(model, {
17343
+ instanceMeshes: false,
17344
+ mergeMeshes: true,
17345
+ tints,
17346
+ uvLockRotation: variant.uvlock && (rotation.x !== 0 || rotation.y !== 0) ? [rotation.x, rotation.y, rotation.z] : void 0
17347
+ });
17348
+ await object4.init();
17349
+ let geometry = object4.children[0].geometry;
17350
+ geometry.applyMatrix4(new import_three41.Matrix4().makeRotationFromEuler(rotation));
17351
+ object4.clear();
17352
+ const keep = [];
17353
+ const cullFaces = [];
17354
+ for (const element of atlas.model.elements) {
17355
+ for (const face of CUBE_FACES) {
17356
+ const data = element.faces[face];
17357
+ keep.push(data !== void 0);
17358
+ if (!data) continue;
17359
+ const local = CUBE_FACES.findIndex((name) => name === data.cullface);
17360
+ if (local < 0) {
17361
+ cullFaces.push(0);
17362
+ continue;
17363
+ }
17364
+ const normal = new import_three41.Vector3(...CUBE_FACE_OFFSETS[local]).applyEuler(rotation);
17365
+ 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);
17366
+ cullFaces.push(world < 0 ? 0 : 1 << world);
17367
+ }
17368
+ }
17369
+ geometry = compactQuads(geometry, keep);
17370
+ const template = {
17371
+ geometry,
17372
+ atlas,
17373
+ cullFaces: new Uint8Array(cullFaces),
17374
+ occludes: ModelCulling.isOpaqueFullCube(atlas) && ModelCulling.toLocalMask(63, rotation) === 63,
17375
+ layer: atlas.hasTranslucency ? 1 : 0
17376
+ };
17377
+ this.templates.add(template);
17378
+ return template;
17379
+ });
17253
17380
  }
17254
17381
  /** Disposes prepared geometry and clears this world's template caches. */
17255
17382
  clear() {
@@ -17335,7 +17462,6 @@ function resolveSavedEntity(entity) {
17335
17462
 
17336
17463
  // src/world/_entities/WorldEntities.ts
17337
17464
  init_BannerPatterns();
17338
- init_AssetLoader();
17339
17465
  var WorldEntities = class {
17340
17466
  constructor(scene) {
17341
17467
  this.scene = scene;
@@ -17343,10 +17469,6 @@ var WorldEntities = class {
17343
17469
  }
17344
17470
  /** Captures column ownership before asynchronous block or asset loading starts. */
17345
17471
  prepare(entities = [], column) {
17346
- const root = AssetLoader.ROOT, scope = AssetLoader.persistentScope;
17347
- const checkSources = () => {
17348
- if (root !== AssetLoader.ROOT || scope !== AssetLoader.persistentScope) throw new Error("Asset sources changed while loading saved entities; retry the request");
17349
- };
17350
17472
  const placements = entities.flatMap((entity) => {
17351
17473
  const resolved = resolveSavedEntity(entity);
17352
17474
  if (!resolved) return [];
@@ -17362,17 +17484,13 @@ var WorldEntities = class {
17362
17484
  let object4;
17363
17485
  try {
17364
17486
  if (!current()) return;
17365
- checkSources();
17366
17487
  const models = await Entities.getEntityList();
17367
- checkSources();
17368
17488
  if (!models.includes(placement.key.path) || !current()) return;
17369
17489
  const model = await Entities.getEntity(placement.key, placement.texture, { when: placement.when });
17370
- checkSources();
17371
17490
  if (!model || !current()) return;
17372
17491
  let tints = placement.tints;
17373
17492
  if (placement.woolDye) {
17374
17493
  const dye = (await BannerPatterns.getColors(placement.key.root))[placement.woolDye];
17375
- checkSources();
17376
17494
  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);
17377
17495
  tints = { ...tints, wool_color: tint };
17378
17496
  }
@@ -17382,7 +17500,6 @@ var WorldEntities = class {
17382
17500
  object4.position.fromArray(placement.position).multiplyScalar(16).addScalar(-8);
17383
17501
  object4.rotation.y = placement.yaw;
17384
17502
  await object4.init();
17385
- checkSources();
17386
17503
  if (!current()) return;
17387
17504
  placement.owner.add(object4);
17388
17505
  this.scene.add(object4);
@@ -17579,7 +17696,6 @@ var MineRenderWorld = class {
17579
17696
  }
17580
17697
  try {
17581
17698
  await this.clearChunkColumn(chunk.x, chunk.z);
17582
- let sliceStart = performance.now();
17583
17699
  for (const section of chunk.sections) {
17584
17700
  if (section.biomes) {
17585
17701
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17593,6 +17709,9 @@ var MineRenderWorld = class {
17593
17709
  if (!column) this.biomes.set(key, column = /* @__PURE__ */ new Map());
17594
17710
  column.set(section.y, copy);
17595
17711
  }
17712
+ }
17713
+ let sliceStart = performance.now();
17714
+ for (const section of chunk.sections) {
17596
17715
  if (!executor) {
17597
17716
  if (!occupiedSections.has(section.y)) continue;
17598
17717
  const position = new import_three42.Vector3(chunk.x * 16, section.y * 16, chunk.z * 16);
@@ -17757,18 +17876,34 @@ var MineRenderWorld = class {
17757
17876
  const key = this.worldPosToChunkKey(pos);
17758
17877
  let chunk = this._chunks.get(key);
17759
17878
  if (typeof chunk === "undefined") {
17879
+ const x = Math.floor(pos.x / 16), y = Math.floor(pos.y / 16), z = Math.floor(pos.z / 16);
17760
17880
  chunk = new Chunk(
17761
17881
  this.scene,
17762
- Math.floor(pos.x / 16),
17763
- Math.floor(pos.y / 16),
17764
- Math.floor(pos.z / 16),
17882
+ x,
17883
+ y,
17884
+ z,
17765
17885
  (positions) => this.updateCulling(positions),
17766
17886
  this.sectionModels,
17767
- (x, y, z) => {
17768
- const cx = Math.floor(x / 16), cy = Math.floor(y / 16), cz = Math.floor(z / 16);
17887
+ (x2, y2, z2) => {
17888
+ const cx = Math.floor(x2 / 16), cy = Math.floor(y2 / 16), cz = Math.floor(z2 / 16);
17769
17889
  const section = this._chunks.get(`${cx}_${cy}_${cz}`);
17770
- const index = (y - cy * 16) * 256 + (z - cz * 16) * 16 + x - cx * 16;
17890
+ const index = (y2 - cy * 16) * 256 + (z2 - cz * 16) * 16 + x2 - cx * 16;
17771
17891
  return (section?.fluidByteIndex(index) ?? 0) | (section?.isOccludingIndex(index) ? 64 : 0);
17892
+ },
17893
+ (x2, y2, z2) => this.getBiomeAt(x2, y2, z2),
17894
+ async (biomes) => {
17895
+ if (this._chunks.get(key) !== chunk) return;
17896
+ const columnKey = `${x}_${z}`;
17897
+ let column = this.biomes.get(columnKey);
17898
+ if (biomes) {
17899
+ if (!column) this.biomes.set(columnKey, column = /* @__PURE__ */ new Map());
17900
+ column.set(y, biomes);
17901
+ } else {
17902
+ column?.delete(y);
17903
+ if (!column?.size) this.biomes.delete(columnKey);
17904
+ }
17905
+ await chunk.refreshBiomes();
17906
+ await this._chunks.get(`${x}_${y + 1}_${z}`)?.refreshBiomes();
17772
17907
  }
17773
17908
  );
17774
17909
  this._chunks.set(key, chunk);
@@ -17823,6 +17958,7 @@ function buildSectionGeometry(input) {
17823
17958
  if (!Number.isInteger(maxAtlasSize) || maxAtlasSize < 1) throw new RangeError("Section atlas size must be a positive integer");
17824
17959
  if (count > indices.length || count > templates.length || count > cullMasks.length) throw new RangeError("Section entry count exceeds its arrays");
17825
17960
  if (fluids && fluids.cells.length !== 5832) throw new RangeError("Section fluid cells must cover an 18\xD718\xD718 neighborhood");
17961
+ if (fluids?.waterTints && fluids.waterTints.length !== 4096 * 3) throw new RangeError("Section water tints must contain RGB for 4096 cells");
17826
17962
  for (const template of templateData) {
17827
17963
  const { quads } = template;
17828
17964
  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) {
@@ -17900,7 +18036,9 @@ function buildSectionGeometry(input) {
17900
18036
  height
17901
18037
  });
17902
18038
  }
17903
- fluidPages.get(fluid).fluids.push({ quads, x, y, z, data });
18039
+ const tintIndex = (x + z * 16 + y * 256) * 3;
18040
+ const tint = fluid === "water" && fluids.waterTints ? fluids.waterTints.subarray(tintIndex, tintIndex + 3) : data.tint;
18041
+ fluidPages.get(fluid).fluids.push({ quads, x, y, z, data, tint });
17904
18042
  }
17905
18043
  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));
17906
18044
  }
@@ -17949,7 +18087,7 @@ function buildSectionGeometry(input) {
17949
18087
  for (let corner = 0; corner < 6; corner++) outputIndices[indexOffset++] = start + template.indices[quad * 6 + corner];
17950
18088
  }
17951
18089
  }
17952
- for (const { quads, x, y, z, data } of page.fluids) {
18090
+ for (const { quads, x, y, z, data, tint } of page.fluids) {
17953
18091
  const atlas = atlases[data.atlas];
17954
18092
  const placement = page.placements[0];
17955
18093
  const mapU = (u) => (placement.x + u * atlas.width) / page.width;
@@ -17973,7 +18111,7 @@ function buildSectionGeometry(input) {
17973
18111
  mapU((rect[0] + rect[2]) / atlas.width),
17974
18112
  mapV(1 - rect[1] / atlas.height)
17975
18113
  ], vertex * 4);
17976
- colors.set(data.tint, vertex * 3);
18114
+ colors.set(tint, vertex * 3);
17977
18115
  }
17978
18116
  outputIndices.set([start, start + 1, start + 2, start, start + 2, start + 3], indexOffset);
17979
18117
  indexOffset += 6;
@@ -18114,7 +18252,7 @@ function toInput(entries, maxAtlasSize, fluids) {
18114
18252
  input.cullMasks[i] = cullMask;
18115
18253
  }
18116
18254
  if (fluids) {
18117
- input.fluids = { cells: fluids.cells };
18255
+ input.fluids = { cells: fluids.cells, waterTints: fluids.waterTints };
18118
18256
  for (const kind of ["water", "lava"]) {
18119
18257
  const atlas = fluids[kind];
18120
18258
  if (!atlas) continue;
@@ -18215,11 +18353,14 @@ var SectionMesh = class _SectionMesh extends import_three43.Group {
18215
18353
  init_FluidGeometry();
18216
18354
  init_FluidQuads();
18217
18355
  init_CubeFace();
18356
+ var WATER_TINT2 = new import_three44.Color(4159204).toArray();
18218
18357
  var Chunk = class _Chunk {
18219
- constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell) {
18358
+ constructor(scene, x, y, z, onBlocksChanged, sectionModels, neighborCell, biomeAt, onBiomesChanged) {
18220
18359
  this.onBlocksChanged = onBlocksChanged;
18221
18360
  this.sectionModels = sectionModels;
18222
18361
  this.neighborCell = neighborCell;
18362
+ this.biomeAt = biomeAt;
18363
+ this.onBiomesChanged = onBiomesChanged;
18223
18364
  this.data = new ChunkData();
18224
18365
  this.renderedBlocks = /* @__PURE__ */ new Map();
18225
18366
  this.sectionBlocks = /* @__PURE__ */ new Map();
@@ -18244,6 +18385,56 @@ var Chunk = class _Chunk {
18244
18385
  const index = _Chunk.chunkPosToBlockIndex(pos);
18245
18386
  return this.renderedBlocks.get(index);
18246
18387
  }
18388
+ getBiomeAt(posOrX, y, z) {
18389
+ if (typeof posOrX === "number") return this.getBiomeAt(new import_three44.Vector3(posOrX, y, z));
18390
+ if (isTripleArray(posOrX)) return this.getBiomeAt(new import_three44.Vector3(...posOrX));
18391
+ const pos = this.worldPosToChunkPos(posOrX);
18392
+ _Chunk.chunkPosToBlockIndex(pos);
18393
+ 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];
18394
+ }
18395
+ /** Copies 64 biome IDs in x + z * 4 + y * 16 order and refreshes placed blocks. Omit the array to clear biome data. */
18396
+ setBiomes(biomes) {
18397
+ const copy = biomes ? [...biomes] : void 0;
18398
+ if (copy && (copy.length !== 64 || !copy.every((id) => typeof id === "string" && id.length > 0))) {
18399
+ throw new RangeError("Section biomes must contain 64 nonempty biome IDs");
18400
+ }
18401
+ if (this.onBiomesChanged) return this.onBiomesChanged(copy);
18402
+ this.biomes = copy;
18403
+ return this.refreshBiomes();
18404
+ }
18405
+ /** Rebuilds biome-tinted blocks while preserving hidden blocks and unrelated render objects. */
18406
+ async refreshBiomes() {
18407
+ const affected = /* @__PURE__ */ new Map();
18408
+ const blocks = [];
18409
+ for (const index of this.renderedBlocks.keys()) {
18410
+ const block = this.data.get(index);
18411
+ const stateKey = ChunkData.paletteKey(block);
18412
+ let matches = affected.get(stateKey);
18413
+ if (!matches) {
18414
+ const key = AssetKey.parse("blockstates", block.type);
18415
+ matches = BlockStates.get(key).then(async (state) => {
18416
+ const origin = state?.key ?? key;
18417
+ const [source, fluid] = await Promise.all([BlockTints.biomeSource(origin), getBlockFluidState(origin, block.properties)]);
18418
+ return !!source || fluid?.kind === "water";
18419
+ });
18420
+ affected.set(stateKey, matches);
18421
+ }
18422
+ if (await matches) blocks.push({ index, block });
18423
+ }
18424
+ if (!blocks.length) return;
18425
+ const hidden = new Set(this.hiddenBlocks);
18426
+ try {
18427
+ await this.placeBlocks(blocks);
18428
+ } finally {
18429
+ for (const index of hidden) {
18430
+ this.hiddenBlocks.add(index);
18431
+ this.renderedBlocks.get(index)?.object?.setVisible(false);
18432
+ }
18433
+ const positions = blocks.map(({ index }) => new import_three44.Vector3(...this.positionAt(index)));
18434
+ if (this.onBlocksChanged) await this.onBlocksChanged(positions);
18435
+ else await this.rebuildSectionMesh();
18436
+ }
18437
+ }
18247
18438
  async setBlockAt(posOrX, yOrBlock, z, block) {
18248
18439
  if (typeof posOrX == "number") {
18249
18440
  return this.setBlockAt(new import_three44.Vector3(posOrX, yOrBlock, z), block);
@@ -18282,14 +18473,15 @@ var Chunk = class _Chunk {
18282
18473
  nbt: block.nbt === void 0 ? void 0 : structuredClone(block.nbt)
18283
18474
  } : void 0
18284
18475
  }));
18285
- const states = new Map(placements.filter(({ key }) => key !== void 0).map(({ key, block }) => [key, block]));
18286
- const settled = await Promise.allSettled([...states.values()].map(async (stored) => {
18287
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18288
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18289
- const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18290
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, stored.properties);
18291
- return { blockState: blockState3, perBlock, fluid, prepared, stored };
18292
- }));
18476
+ const states = /* @__PURE__ */ new Map();
18477
+ for (const { index, key, block } of placements) {
18478
+ if (key === void 0) continue;
18479
+ let state = states.get(key);
18480
+ if (!state) states.set(key, state = { stored: block, indices: [] });
18481
+ state.indices.push(index);
18482
+ }
18483
+ const biomes = /* @__PURE__ */ new Map();
18484
+ const settled = await Promise.allSettled([...states.values()].map(({ stored, indices }) => this.resolveState(stored, indices, biomes)));
18293
18485
  const resolutions = new Map([...states.keys()].map((key, i) => [key, settled[i]]));
18294
18486
  const positions = [];
18295
18487
  let failed = false, failure;
@@ -18313,7 +18505,8 @@ var Chunk = class _Chunk {
18313
18505
  if (!readBlock) continue;
18314
18506
  const resolved = resolutions.get(key);
18315
18507
  if (resolved.status === "rejected") throw resolved.reason;
18316
- const { blockState: blockState3, perBlock, fluid, prepared, stored } = resolved.value;
18508
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts, stored } = resolved.value;
18509
+ const context = contexts.get(index);
18317
18510
  if (!blockState3) {
18318
18511
  this.data.assign(index, 0);
18319
18512
  continue;
@@ -18321,9 +18514,10 @@ var Chunk = class _Chunk {
18321
18514
  const variantPosition = worldPos.toArray();
18322
18515
  if (fluid) {
18323
18516
  this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18517
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18324
18518
  this.meshDirty = true;
18325
18519
  }
18326
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18520
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18327
18521
  if (templates) {
18328
18522
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18329
18523
  this.meshDirty = true;
@@ -18333,7 +18527,9 @@ var Chunk = class _Chunk {
18333
18527
  instanceMeshes: true,
18334
18528
  maxInstanceCount: 2e3,
18335
18529
  initialState: stored.properties,
18336
- variantPosition
18530
+ variantPosition,
18531
+ biome: context?.biome,
18532
+ biomePosition: this.biomePosition(index, blockState3, stored)
18337
18533
  });
18338
18534
  object4.setPosition(MineRenderWorld.worldToScenePosition(worldPos));
18339
18535
  }
@@ -18356,24 +18552,21 @@ var Chunk = class _Chunk {
18356
18552
  for (const info of this.renderedBlocks.values()) info.object?.removeFromScene();
18357
18553
  this.sectionBlocks.clear();
18358
18554
  this.fluidCells.fill(0);
18555
+ this.waterTints = void 0;
18359
18556
  this.renderedBlocks.clear();
18360
18557
  this.hiddenBlocks.clear();
18361
18558
  this.meshDirty = true;
18362
18559
  this.data.copyFrom(data);
18363
- const ids = /* @__PURE__ */ new Set();
18560
+ const ids = /* @__PURE__ */ new Map();
18364
18561
  for (let index = 0; index < 4096; index++) {
18365
18562
  const id = this.data.idAt(index);
18366
- if (id) ids.add(id);
18367
- }
18368
- const palette = [...ids];
18369
- const settled = await Promise.allSettled(palette.map(async (id) => {
18370
- const state = this.data.stateAt(id);
18371
- const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", state.type));
18372
- const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18373
- const fluid = await getBlockFluidState(blockState3?.key, state.properties);
18374
- const prepared = perBlock || fluid?.renderModel === false ? void 0 : await this.sectionModels.prepareState(blockState3, state.properties);
18375
- return { blockState: blockState3, perBlock, fluid, prepared };
18376
- }));
18563
+ if (!id) continue;
18564
+ let indices = ids.get(id);
18565
+ if (!indices) ids.set(id, indices = []);
18566
+ indices.push(index);
18567
+ }
18568
+ const palette = [...ids.keys()], biomes = /* @__PURE__ */ new Map();
18569
+ const settled = await Promise.allSettled(palette.map((id) => this.resolveState(this.data.stateAt(id), ids.get(id), biomes)));
18377
18570
  const resolutions = new Map(palette.map((id, i) => [id, settled[i]]));
18378
18571
  const perBlocks = [];
18379
18572
  let failed = false, failure;
@@ -18383,24 +18576,28 @@ var Chunk = class _Chunk {
18383
18576
  try {
18384
18577
  const resolved = resolutions.get(id);
18385
18578
  if (resolved.status === "rejected") throw resolved.reason;
18386
- const { blockState: blockState3, perBlock, fluid, prepared } = resolved.value;
18579
+ const { blockState: blockState3, perBlock, fluid, prepared, contexts } = resolved.value;
18580
+ const context = contexts.get(index);
18387
18581
  if (!blockState3) {
18388
18582
  this.data.assign(index, 0);
18389
18583
  this.fluidCells[index] = 0;
18390
18584
  continue;
18391
18585
  }
18392
- if (fluid) this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18586
+ if (fluid) {
18587
+ this.fluidCells[index] = (fluid.kind === "water" ? 16 : 32) | Math.min(fluid.level, 8);
18588
+ if (fluid.kind === "water") this.setWaterTint(index, context?.waterTint);
18589
+ }
18393
18590
  const variantPosition = [
18394
18591
  this.x * 16 + index % 16,
18395
18592
  this.y * 16 + Math.floor(index / 256),
18396
18593
  this.z * 16 + Math.floor(index / 16) % 16
18397
18594
  ];
18398
- const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : prepared?.pick(variantPosition);
18595
+ const templates = perBlock ? void 0 : fluid?.renderModel === false ? [] : (context?.prepared ?? prepared)?.pick(variantPosition);
18399
18596
  if (templates) {
18400
18597
  this.sectionBlocks.set(index, templates.map((template) => ({ index, template, cullMask: 0 })));
18401
18598
  this.meshDirty = true;
18402
18599
  } else {
18403
- perBlocks.push({ index, blockState: blockState3 });
18600
+ perBlocks.push({ index, blockState: blockState3, biome: context?.biome });
18404
18601
  }
18405
18602
  const readBlock = this.data.snapshot(index);
18406
18603
  this.renderedBlocks.set(index, { get block() {
@@ -18413,7 +18610,7 @@ var Chunk = class _Chunk {
18413
18610
  failed = true;
18414
18611
  }
18415
18612
  }
18416
- for (const { index, blockState: blockState3 } of perBlocks) {
18613
+ for (const { index, blockState: blockState3, biome } of perBlocks) {
18417
18614
  let object4;
18418
18615
  try {
18419
18616
  object4 = await this.scene.addBlock(blockState3, {
@@ -18421,11 +18618,9 @@ var Chunk = class _Chunk {
18421
18618
  instanceMeshes: true,
18422
18619
  maxInstanceCount: 2e3,
18423
18620
  initialState: this.data.stateAt(this.data.idAt(index)).properties,
18424
- variantPosition: [
18425
- this.x * 16 + index % 16,
18426
- this.y * 16 + Math.floor(index / 256),
18427
- this.z * 16 + Math.floor(index / 16) % 16
18428
- ]
18621
+ variantPosition: this.positionAt(index),
18622
+ biome,
18623
+ biomePosition: this.biomePosition(index, blockState3, this.data.stateAt(this.data.idAt(index)))
18429
18624
  });
18430
18625
  object4.setPosition(new import_three44.Vector3(
18431
18626
  this.x * 256 + index % 16 * 16,
@@ -18444,6 +18639,68 @@ var Chunk = class _Chunk {
18444
18639
  }
18445
18640
  if (failed) throw failure;
18446
18641
  }
18642
+ positionAt(index) {
18643
+ return [this.x * 16 + index % 16, this.y * 16 + Math.floor(index / 256), this.z * 16 + Math.floor(index / 16) % 16];
18644
+ }
18645
+ biomePosition(index, blockState3, stored) {
18646
+ const position = this.positionAt(index), name = blockState3.key?.toNamespacedString();
18647
+ if (stored.properties?.half === "upper" && (name === "minecraft:tall_grass" || name === "minecraft:large_fern")) position[1]--;
18648
+ return position;
18649
+ }
18650
+ async resolveState(stored, indices, biomes) {
18651
+ const blockState3 = await BlockStates.get(AssetKey.parse("blockstates", stored.type));
18652
+ const perBlock = !blockState3 || !this.sectionModels || !!(blockState3.key && BlockEntities.entry(await BlockEntities.getIndex(blockState3.key.root), blockState3.key.toNamespacedString()));
18653
+ const fluid = await getBlockFluidState(blockState3?.key, stored.properties);
18654
+ 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;
18655
+ const source = hasBiomes && blockState3 ? await BlockTints.biomeSource(blockState3.key) : void 0;
18656
+ const prepare = (color2) => perBlock || fluid?.renderModel === false ? void 0 : this.sectionModels.prepareState(blockState3, stored.properties, color2);
18657
+ const contexts = /* @__PURE__ */ new Map();
18658
+ const groups = /* @__PURE__ */ new Map();
18659
+ if (blockState3 && (source || fluid?.kind === "water")) {
18660
+ for (const index of indices) {
18661
+ const position = this.biomePosition(index, blockState3, stored);
18662
+ const biome = this.biomeAt ? this.biomeAt(...position) : position[1] >= this.y * 16 ? this.getBiomeAt(position) : void 0;
18663
+ if (!biome) continue;
18664
+ const key = JSON.stringify(source === "grass" ? [biome, position[0], position[2]] : [biome]);
18665
+ let group = groups.get(key);
18666
+ if (!group) groups.set(key, group = { biome, position, indices: [] });
18667
+ group.indices.push(index);
18668
+ }
18669
+ }
18670
+ const groupedCount = [...groups.values()].reduce((sum, group) => sum + group.indices.length, 0);
18671
+ const prepared = groupedCount < indices.length ? await prepare() : void 0;
18672
+ await Promise.all([...groups.values()].map(async ({ biome, position, indices: indices2 }) => {
18673
+ const available = () => {
18674
+ const key = JSON.stringify([biome, blockState3?.key?.root]);
18675
+ let found = biomes.get(key);
18676
+ if (!found) {
18677
+ found = Biomes.get(biome, blockState3?.key).then((value) => value !== void 0);
18678
+ biomes.set(key, found);
18679
+ }
18680
+ return found;
18681
+ };
18682
+ const context = { biome };
18683
+ if (perBlock) {
18684
+ if (!await available()) context.biome = void 0;
18685
+ } else {
18686
+ context.prepared = await prepare(async () => await available() ? BlockTints.getBiomeColor(blockState3?.key, biome, position) : void 0);
18687
+ if (fluid?.kind === "water" && await available()) {
18688
+ const color2 = await Biomes.getColor(biome, "water", blockState3?.key);
18689
+ if (color2 !== void 0 && color2 !== 4159204) context.waterTint = new import_three44.Color(color2).toArray();
18690
+ }
18691
+ }
18692
+ for (const index of indices2) contexts.set(index, context);
18693
+ }));
18694
+ return { blockState: blockState3, perBlock, fluid, prepared, contexts, stored };
18695
+ }
18696
+ setWaterTint(index, tint) {
18697
+ if (!this.sectionModels || !this.waterTints && tint === void 0) return;
18698
+ if (!this.waterTints) {
18699
+ this.waterTints = new Float32Array(4096 * 3);
18700
+ for (let cell = 0; cell < 4096; cell++) this.waterTints.set(WATER_TINT2, cell * 3);
18701
+ }
18702
+ this.waterTints.set(tint ?? WATER_TINT2, index * 3);
18703
+ }
18447
18704
  /** Changes visibility at world block coordinates while preserving the block's data. */
18448
18705
  async setBlockVisibleAt(pos, visible) {
18449
18706
  const index = _Chunk.chunkPosToBlockIndex(this.worldPosToChunkPos(pos));
@@ -18547,7 +18804,7 @@ var Chunk = class _Chunk {
18547
18804
  if (block && !block.object) origins.set(kind, block.block.type);
18548
18805
  }
18549
18806
  if (origins.size) {
18550
- fluids = { cells: new Uint8Array(18 * 18 * 18) };
18807
+ fluids = { cells: new Uint8Array(18 * 18 * 18), waterTints: this.waterTints?.slice() };
18551
18808
  for (let y = -1; y <= 16; y++) {
18552
18809
  for (let z = -1; z <= 16; z++) {
18553
18810
  for (let x = -1; x <= 16; x++) {
@@ -18589,8 +18846,13 @@ var Chunk = class _Chunk {
18589
18846
  this.renderedBlocks.clear();
18590
18847
  this.hiddenBlocks.clear();
18591
18848
  this.fluidCells.fill(0);
18849
+ this.waterTints = void 0;
18592
18850
  this.data.clear();
18593
- if (notify) await this.onBlocksChanged?.(positions);
18851
+ this.biomes = void 0;
18852
+ if (notify) {
18853
+ await this.onBiomesChanged?.(void 0);
18854
+ await this.onBlocksChanged?.(positions);
18855
+ }
18594
18856
  }
18595
18857
  async dispose() {
18596
18858
  await this.clear();