Polytone: read targets and biome/dimension attribute modifiers

A new crate, horizon-polytone, reads the Polytone resource pack format
clean-room from the public wiki: modifier targets (implicit path id, ids,
tags, regex, required) and the attributes_modifiers layers of biome and
dimension modifiers (groups, REMOVE, values, modifier objects, colors,
colormap references, expressions kept as source). Nothing uses it yet.

Part of #11486

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Vinícius MoreiraandClaude Opus 5.5 committed 2026-11-06 21:17:25 -03:00
1 parent 13f70ad482
commit c80eac9506
8 files changed
+617

No files matched your search

+1
View File
@@ -65,6 +65,7 @@ Game workspace (root `Cargo.toml`); `launcher/` and `crates/horizon-telemetry-se
| `horizon-mods`, `cargo-horizon-mod` | Content mods (`.hmod`: manifest, content fence, no code), the registry index and `.hpack`, and `cargo horizon-mod` (`new`, `build`, `run`, `check`, `registry`) ([docs/mods.md](docs/mods.md)). Code mods are native, built by the launcher ([docs/native-mods.md](docs/native-mods.md)) |
| `horizon-port-pack` | Porting Java modpacks ([docs/modpacks.md](docs/modpacks.md)) |
| `horizon-optifine` | OptiFine-format pack files below every feature that reads them: `.properties`, the name/range/block matchers, paths and `optifine/` folders, and custom entity models (`cem/`: `.jem`/`.jpm` parsing and baking) |
| `horizon-polytone` | Polytone-format pack files (`assets/*/polytone/`), read clean-room from the public wiki: targets, biome and dimension modifiers and their environment attribute layers (#11486) |
| `horizon-shader-api` | Horizon's own WGSL shader packs ([docs/api/shaders.md](docs/api/shaders.md)): `horizon-shaders.toml`, the `.hshader` package; shared by the game, the launcher and the registry |
| `horizon-shaders` | Shader packs (the `shaders/` folder format), rebuilt clean-room from a written spec of the format; no wgpu |
| `horizon-theme` | Design tokens and fonts for the launcher, the game's own menus and the website |
Generated
+8
View File
@@ -1703,6 +1703,14 @@ dependencies = [
"regex",
]
[[package]]
name = "horizon-polytone"
version = "0.1.0"
dependencies = [
"regex",
"serde_json",
]
[[package]]
name = "horizon-port-pack"
version = "0.1.0"
+1
View File
@@ -37,6 +37,7 @@ members = [
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-optifine",
"crates/horizon-polytone",
"crates/horizon-shader-api",
"crates/horizon-release-sign",
"crates/horizon-mod-deps",
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "horizon-polytone"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "Polytone-format resource pack files: targets, and the biome and dimension modifiers' environment attribute layers"
[dependencies]
regex = { version = "1", default-features = false, features = ["std", "unicode-perl"] }
serde_json = "1"
[lints]
workspace = true
+237
View File
@@ -0,0 +1,237 @@
//! Environment attribute layers (wiki "Environment Attributes"): a map from attribute id to a
//! value, which sets the attribute, a `{argument, modifier}` object, which modifies the value the
//! earlier layers gave, or `REMOVE`, which drops the game's own layer for it.
use serde_json::{Map, Value};
use std::collections::BTreeMap;
/// The vanilla `modifier` operations.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Modifier {
Override,
Add,
Subtract,
Multiply,
Minimum,
Maximum,
AlphaBlend,
BlendToGray,
And,
Nand,
Or,
Nor,
Xor,
Xnor,
}
impl Modifier {
/// The `modifier` field, in any case (the wiki writes both `add` and `ADD`).
fn parse(s: &str) -> Option<Self> {
Some(match s.to_ascii_lowercase().as_str() {
"override" => Self::Override,
"add" => Self::Add,
"subtract" => Self::Subtract,
"multiply" => Self::Multiply,
"minimum" => Self::Minimum,
"maximum" => Self::Maximum,
"alpha_blend" => Self::AlphaBlend,
"blend_to_gray" => Self::BlendToGray,
"and" => Self::And,
"nand" => Self::Nand,
"or" => Self::Or,
"nor" => Self::Nor,
"xor" => Self::Xor,
"xnor" => Self::Xnor,
_ => return None,
})
}
}
/// A layer's value or `argument`.
#[derive(Clone, Debug, PartialEq)]
pub enum Argument {
/// A number: a float, an angle, or a packed color, as the attribute's type reads it.
Number(f64),
Bool(bool),
/// A `#rrggbb` / `#aarrggbb` string (the `#` optional), packed ARGB; opaque without alpha.
Color(u32),
/// A `namespace:path` string: a colormap sampled for the value.
Colormap(String),
/// Any other string: a scripting expression, kept as source.
Expression(String),
/// Vanilla's structured arguments, such as `alpha_blend`'s `{value, alpha}`.
Object(Map<String, Value>),
}
impl Argument {
fn parse(v: &Value) -> Result<Self, String> {
Ok(match v {
Value::Number(n) => Self::Number(n.as_f64().unwrap_or_default()),
Value::Bool(b) => Self::Bool(*b),
Value::Object(o) => Self::Object(o.clone()),
Value::String(s) => {
let s = s.trim();
let hex = s.strip_prefix('#').unwrap_or(s);
let color =
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit());
if let (true, Ok(packed)) = (color, u32::from_str_radix(hex, 16)) {
Self::Color(if hex.len() == 6 {
0xff00_0000 | packed
} else {
packed
})
} else if s.contains(':') && crate::target::parse_id(s).is_some() {
Self::Colormap(s.to_owned())
} else {
Self::Expression(s.to_owned())
}
}
_ => return Err(format!("{v} is no attribute value")),
})
}
}
/// What one modifier file does to one attribute.
#[derive(Clone, Debug, PartialEq)]
pub enum Layer {
/// `REMOVE`: the game's own layer for the attribute in this group is dropped.
Remove,
/// A value (`Override`) or a `{argument, modifier}` object. `biome_blend` and `time_blend`
/// (default `true`) say whether a computed value is mixed across biome borders and faded
/// over time when it jumps.
Apply {
modifier: Modifier,
argument: Argument,
biome_blend: bool,
time_blend: bool,
},
}
impl Layer {
fn parse(v: &Value) -> Result<Self, String> {
if v.as_str().is_some_and(|s| s.trim() == "REMOVE") {
return Ok(Self::Remove);
}
let object = v.as_object().filter(|o| o.contains_key("modifier"));
let Some(o) = object else {
return Ok(Self::Apply {
modifier: Modifier::Override,
argument: Argument::parse(v)?,
biome_blend: true,
time_blend: true,
});
};
let name = o["modifier"].as_str().ok_or("modifier is not a string")?;
let flag = |key: &str| o.get(key).and_then(Value::as_bool).unwrap_or(true);
Ok(Self::Apply {
modifier: Modifier::parse(name).ok_or_else(|| format!("unknown modifier {name:?}"))?,
argument: Argument::parse(o.get("argument").ok_or("modifier without an argument")?)?,
biome_blend: flag("biome_blend"),
time_blend: flag("time_blend"),
})
}
}
/// `visual/fog_color` and `minecraft:visual/fog_color` are the same attribute.
pub fn normalize_attribute(id: &str) -> String {
if id.contains(':') {
id.to_owned()
} else {
format!("minecraft:{id}")
}
}
/// One group's layers, by attribute id (with its namespace).
pub type Attributes = BTreeMap<String, Layer>;
pub(crate) fn parse_attributes(v: &Value) -> Result<Attributes, String> {
let map = v.as_object().ok_or("attributes are not an object")?;
map.iter()
.map(|(id, layer)| {
let layer = Layer::parse(layer).map_err(|e| format!("{id}: {e}"))?;
Ok((normalize_attribute(id), layer))
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn layer(v: Value) -> Layer {
Layer::parse(&v).unwrap()
}
fn set(argument: Argument) -> Layer {
Layer::Apply {
modifier: Modifier::Override,
argument,
biome_blend: true,
time_blend: true,
}
}
#[test]
fn plain_values_set_the_attribute() {
assert_eq!(layer(json!(192.5)), set(Argument::Number(192.5)));
assert_eq!(layer(json!(false)), set(Argument::Bool(false)));
assert_eq!(layer(json!("REMOVE")), Layer::Remove);
}
#[test]
fn color_strings_pack_to_argb_with_or_without_the_hash() {
assert_eq!(layer(json!("4cb1a8")), set(Argument::Color(0xff4c_b1a8)));
assert_eq!(layer(json!("#332211")), set(Argument::Color(0xff33_2211)));
assert_eq!(layer(json!("#867a7aff")), set(Argument::Color(0x867a_7aff)));
assert_eq!(layer(json!("#00000000")), set(Argument::Color(0)));
}
#[test]
fn ids_name_colormaps_and_other_strings_are_expressions() {
let colormap = Argument::Colormap("my_pack:swamp_sky_color".into());
assert_eq!(layer(json!("my_pack:swamp_sky_color")), set(colormap));
let expression = Argument::Expression("r.rand() + 0.1".into());
assert_eq!(layer(json!("r.rand() + 0.1")), set(expression));
// Not hex, so no color, though six letters long.
let word = Argument::Expression("g.rain".into());
assert_eq!(layer(json!("g.rain")), set(word));
}
#[test]
fn modifier_objects_read_any_case_and_the_blend_flags() {
let l = layer(json!({"argument": "0.2 * g.rain", "modifier": "ADD", "biome_blend": false}));
assert_eq!(
l,
Layer::Apply {
modifier: Modifier::Add,
argument: Argument::Expression("0.2 * g.rain".into()),
biome_blend: false,
time_blend: true,
}
);
let blend =
layer(json!({"argument": {"value": 4.0, "alpha": 0.5}, "modifier": "alpha_blend"}));
assert!(matches!(
blend,
Layer::Apply {
modifier: Modifier::AlphaBlend,
argument: Argument::Object(_),
..
}
));
}
#[test]
fn unknown_modifiers_and_missing_arguments_are_errors() {
assert!(Layer::parse(&json!({"argument": 1, "modifier": "divide"})).is_err());
assert!(Layer::parse(&json!({"modifier": "add"})).is_err());
assert!(Layer::parse(&json!(null)).is_err());
}
#[test]
fn attribute_ids_get_the_minecraft_namespace() {
let a =
parse_attributes(&json!({"visual/fog_end_distance": 64, "mod:visual/x": 1})).unwrap();
let ids: Vec<&str> = a.keys().map(String::as_str).collect();
assert_eq!(ids, ["minecraft:visual/fog_end_distance", "mod:visual/x"]);
}
}
+17
View File
@@ -0,0 +1,17 @@
//! Polytone-format resource pack files (`assets/<namespace>/polytone/...`), the format of the
//! Polytone mod's packs (#11486).
//!
//! Clean room: written from the public wiki (<https://github.com/MehVahdJukaar/polytone/wiki>) and
//! from observing packs, never from the mod's GPL source. This crate only reads the files: no GPU,
//! no gameplay. The game applies what it reads; nothing changes without a Polytone pack.
//!
//! Read so far: [`Targets`] (what a modifier aims at), and the [`BiomeModifier`] and
//! [`DimensionModifier`] environment attribute layers. Expressions stay source text
//! ([`Argument::Expression`]) until the expression language lands.
mod attributes;
mod modifiers;
mod target;
pub use attributes::{Argument, Attributes, Layer, Modifier, normalize_attribute};
pub use modifiers::{BiomeModifier, DimensionModifier};
pub use target::{Target, Targets, implicit_id};
+189
View File
@@ -0,0 +1,189 @@
//! Biome modifiers (`polytone/biome_modifiers/`) and dimension modifiers
//! (`polytone/dimension_modifiers/`): their targets, priority and `attributes_modifiers` groups.
use crate::attributes::{Attributes, parse_attributes};
use crate::target::Targets;
use serde_json::Value;
/// The fields every modifier shares. `None` when the file is switched off (`polytone_ignore`).
fn common(json: &Value, implicit: Option<&str>) -> Result<Option<(Targets, i32)>, String> {
if json.get("polytone_ignore").and_then(Value::as_bool) == Some(true) {
return Ok(None);
}
let targets = Targets::parse(json.get("targets"), implicit)?;
let priority = json.get("priority").and_then(Value::as_i64).unwrap_or(0);
let priority = i32::try_from(priority).map_err(|_| "priority out of range")?;
Ok(Some((targets, priority)))
}
/// `attributes_modifiers` split into its groups. Without any group key, the whole object is the
/// `base` group.
fn groups<const N: usize>(json: &Value, names: [&str; N]) -> Result<[Attributes; N], String> {
let mut out = std::array::from_fn(|_| Attributes::new());
let Some(field) = json.get("attributes_modifiers") else {
return Ok(out);
};
let object = field
.as_object()
.ok_or("attributes_modifiers is not an object")?;
if !object.keys().any(|k| names.contains(&k.as_str())) {
out[0] = parse_attributes(field)?;
return Ok(out);
}
for (key, value) in object {
let i = names
.iter()
.position(|n| n == key)
.ok_or_else(|| format!("{key:?} is no attribute group (expected one of {names:?})"))?;
out[i] = parse_attributes(value).map_err(|e| format!("{key}: {e}"))?;
}
Ok(out)
}
/// A biome modifier's attribute layers: `base` applies in the biome group, before the dimension
/// timeline and the weather; `post_process` after them.
#[derive(Clone, Debug)]
pub struct BiomeModifier {
pub targets: Targets,
/// Higher wins where several modifiers apply.
pub priority: i32,
pub base: Attributes,
pub post_process: Attributes,
}
impl BiomeModifier {
/// `implicit` is the id the file's path names ([`crate::implicit_id`]).
pub fn parse(json: &Value, implicit: Option<&str>) -> Result<Option<Self>, String> {
let Some((targets, priority)) = common(json, implicit)? else {
return Ok(None);
};
let [base, post_process] = groups(json, ["base", "post_process"])?;
Ok(Some(Self {
targets,
priority,
base,
post_process,
}))
}
}
/// A dimension modifier's attribute layers: `base` in the dimension group (first of all), `rain`
/// and `thunder` in the weather groups, `post_process` last.
#[derive(Clone, Debug)]
pub struct DimensionModifier {
pub targets: Targets,
pub priority: i32,
pub base: Attributes,
pub rain: Attributes,
pub thunder: Attributes,
pub post_process: Attributes,
}
impl DimensionModifier {
pub fn parse(json: &Value, implicit: Option<&str>) -> Result<Option<Self>, String> {
let Some((targets, priority)) = common(json, implicit)? else {
return Ok(None);
};
let [base, rain, thunder, post_process] =
groups(json, ["base", "rain", "thunder", "post_process"])?;
Ok(Some(Self {
targets,
priority,
base,
rain,
thunder,
post_process,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::attributes::{Argument, Layer, Modifier};
use serde_json::json;
const FOG: &str = "minecraft:visual/fog_color";
#[test]
fn a_flat_biome_modifier_fills_the_base_group() {
let json = json!({"attributes_modifiers": {"visual/fog_color": "#102030"}});
let m = BiomeModifier::parse(&json, Some("minecraft:forest"))
.unwrap()
.unwrap();
assert!(m.targets.matches("minecraft:forest", |_| false));
assert_eq!(m.priority, 0);
assert!(m.post_process.is_empty());
assert!(matches!(
m.base[FOG],
Layer::Apply {
modifier: Modifier::Override,
argument: Argument::Color(0xff10_2030),
..
}
));
}
#[test]
fn a_grouped_biome_modifier_keeps_its_groups_apart() {
let json = json!({
"targets": ["minecraft:swamp", "minecraft:mangrove_swamp"],
"priority": -2,
"attributes_modifiers": {
"base": {"minecraft:visual/sky_color": "my_pack:sky"},
"post_process": {"visual/sunrise_sunset_color": "REMOVE"}
}
});
let m = BiomeModifier::parse(&json, None).unwrap().unwrap();
assert_eq!(m.priority, -2);
assert!(m.targets.matches("minecraft:mangrove_swamp", |_| false));
assert_eq!(m.base.len(), 1);
assert_eq!(
m.post_process["minecraft:visual/sunrise_sunset_color"],
Layer::Remove
);
}
#[test]
fn dimension_modifiers_read_the_weather_groups() {
let json = json!({
"targets": "minecraft:overworld",
"attributes_modifiers": {
"base": {"visual/fog_start_distance": "REMOVE"},
"rain": {"visual/cloud_color": "REMOVE", "visual/fog_color": "#3f3f4c"},
"thunder": {"visual/sky_color": {"argument": 0.5, "modifier": "multiply"}}
}
});
let m = DimensionModifier::parse(&json, None).unwrap().unwrap();
assert_eq!(m.base.len(), 1);
assert_eq!(m.rain.len(), 2);
assert!(matches!(
m.thunder["minecraft:visual/sky_color"],
Layer::Apply {
modifier: Modifier::Multiply,
argument: Argument::Number(0.5),
..
}
));
assert!(m.post_process.is_empty());
// Without groups, a dimension modifier's attributes are its base group too.
let flat = json!({"targets": "minecraft:overworld", "attributes_modifiers": {"visual/sky_color": -1}});
let flat = DimensionModifier::parse(&flat, None).unwrap().unwrap();
assert_eq!(flat.base.len(), 1);
assert!(flat.rain.is_empty());
}
#[test]
fn ignored_files_load_nothing_and_broken_ones_fail() {
let ignored = json!({"polytone_ignore": true, "attributes_modifiers": {"x": "REMOVE"}});
assert!(
BiomeModifier::parse(&ignored, Some("minecraft:forest"))
.unwrap()
.is_none()
);
// `rain` is a dimension group, not a biome one.
let wrong_group = json!({"attributes_modifiers": {"base": {}, "rain": {}}});
assert!(BiomeModifier::parse(&wrong_group, Some("minecraft:forest")).is_err());
let bad_layer = json!({"attributes_modifiers": {"visual/fog_color": null}});
assert!(DimensionModifier::parse(&bad_layer, Some("minecraft:overworld")).is_err());
}
}
+150
View File
@@ -0,0 +1,150 @@
//! Targeting (wiki "Shared Concepts"): a modifier aims at the id its file path names, or at the
//! ids, `#tags` and regular expressions of its `targets` field.
use regex::Regex;
use serde_json::Value;
/// One entry of `targets`.
#[derive(Clone, Debug)]
pub enum Target {
/// A literal id, `minecraft` when the namespace is left out.
Id(String),
/// `#namespace:path`, stored without the `#`.
Tag(String),
/// Anything else: a regular expression the whole id must match.
Pattern(Regex),
}
/// What a modifier applies to. Each entry carries `required` (default `true`): whether a missing
/// target is an error.
#[derive(Clone, Debug, Default)]
pub struct Targets(pub Vec<(Target, bool)>);
/// `minecraft:` for an id without a namespace; `None` when it holds characters ids can't
/// (`a-z 0-9 . _ -`, and `/` in the path).
pub(crate) fn parse_id(s: &str) -> Option<String> {
let (namespace, path) = s.split_once(':').unwrap_or(("minecraft", s));
let valid = |part: &str, slash: bool| {
!part.is_empty()
&& part.chars().all(|c| {
matches!(c, 'a'..='z' | '0'..='9' | '.' | '_' | '-') || (slash && c == '/')
})
};
(valid(namespace, false) && valid(path, true)).then(|| format!("{namespace}:{path}"))
}
/// The id a file's path targets when it has no `targets` field:
/// `assets/byg/polytone/biome_modifiers/redwood_forest.json` is `byg:redwood_forest` for the kind
/// `biome_modifiers`.
pub fn implicit_id(path: &str, kind: &str) -> Option<String> {
let path = path.replace('\\', "/");
let after = &path[path.find("assets/")? + "assets/".len()..];
let (namespace, rest) = after.split_once('/')?;
let name = rest.strip_prefix("polytone/")?.strip_prefix(kind)?;
let name = name.strip_prefix('/')?.strip_suffix(".json")?;
parse_id(&format!("{namespace}:{name}"))
}
impl Target {
/// A string is an id first, then a `#tag`, then a regular expression.
fn parse(s: &str) -> Result<Self, String> {
if let Some(id) = parse_id(s) {
return Ok(Self::Id(id));
}
if let Some(tag) = s.strip_prefix('#').and_then(parse_id) {
return Ok(Self::Tag(tag));
}
Regex::new(&format!("^(?:{s})$"))
.map(Self::Pattern)
.map_err(|e| format!("target {s:?} is no id, tag or pattern: {e}"))
}
}
impl Targets {
/// The `targets` field (a string, an object or a list of them), else the file's implicit id.
pub fn parse(field: Option<&Value>, implicit: Option<&str>) -> Result<Self, String> {
let entry = |v: &Value| match v {
Value::String(s) => Ok((Target::parse(s)?, true)),
Value::Object(o) => {
let id = o
.get("id")
.and_then(Value::as_str)
.ok_or("target without an id")?;
let required = o.get("required").and_then(Value::as_bool).unwrap_or(true);
Ok((Target::parse(id)?, required))
}
_ => Err(format!("target {v} is neither a string nor an object")),
};
match field {
Some(Value::Array(list)) => list.iter().map(entry).collect::<Result<_, _>>().map(Self),
Some(v) => Ok(Self(vec![entry(v)?])),
None => {
let id = implicit.ok_or("no targets field and no implicit target")?;
Ok(Self(vec![(Target::parse(id)?, true)]))
}
}
}
/// Whether `id` (`namespace:path`) is a target; `in_tag` answers tag membership.
pub fn matches(&self, id: &str, in_tag: impl Fn(&str) -> bool) -> bool {
self.0.iter().any(|(target, _)| match target {
Target::Id(t) => t == id,
Target::Tag(tag) => in_tag(tag),
Target::Pattern(re) => re.is_match(id),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn targets(v: Value) -> Targets {
Targets::parse(Some(&v), None).unwrap()
}
#[test]
fn the_file_path_names_the_target_without_a_targets_field() {
let path = "pack/assets/byg/polytone/biome_modifiers/redwood_forest.json";
assert_eq!(
implicit_id(path, "biome_modifiers").as_deref(),
Some("byg:redwood_forest")
);
assert_eq!(implicit_id(path, "dimension_modifiers"), None);
let t = Targets::parse(None, Some("byg:redwood_forest")).unwrap();
assert!(t.matches("byg:redwood_forest", |_| false));
assert!(!t.matches("minecraft:forest", |_| false));
}
#[test]
fn ids_default_to_minecraft_and_match_literally() {
let t = targets(json!(["stone", "create:brass_block"]));
assert!(t.matches("minecraft:stone", |_| false));
assert!(t.matches("create:brass_block", |_| false));
// `.` is an id character: no pattern, so it doesn't match other ids.
assert!(!targets(json!("minecraft:a.b")).matches("minecraft:axb", |_| false));
}
#[test]
fn strings_that_are_no_id_are_tags_then_patterns() {
let t = targets(json!(["#minecraft:is_overworld", "create:.*"]));
assert!(t.matches("create:anything", |_| false));
assert!(!t.matches("minecraft:plains", |_| false));
assert!(t.matches("minecraft:plains", |tag| tag == "minecraft:is_overworld"));
}
#[test]
fn the_long_form_carries_required() {
let t =
targets(json!([{"id": "minecraft:pale_oak_leaves", "required": false}, "oak_leaves"]));
let required: Vec<bool> = t.0.iter().map(|e| e.1).collect();
assert_eq!(required, [false, true]);
assert!(t.matches("minecraft:pale_oak_leaves", |_| false));
}
#[test]
fn a_bad_pattern_is_an_error() {
assert!(Targets::parse(Some(&json!("broken(")), None).is_err());
assert!(Targets::parse(None, None).is_err());
}
}