Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 33c7315b8b Custom uniforms: the renderer drives them through horizon-render-pipeline's CustomUniforms
Val and the CustomUniforms trait move to the engine crate; horizon-shaders'
Custom implements it and keeps its parser and evaluator. PackPost holds the
evaluator as a Box<dyn CustomUniforms>: one call a frame, on a pack's path only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:51:59 +01:00
Horizon agentandClaude Opus 5.5 27740e2e8f geometry.rs: GbuffersInputs only in its tests; lockfiles: the pipeline crate takes naga
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:46:06 +01:00
Horizon agentandClaude Opus 5.5 a7d211a1fc Translated programs' types move to horizon-render-pipeline
A translated program as the renderer builds pipelines from it (Program, ComputeProgram,
Interface, Uniform, Sampler, Image, Target, Error, DRAW_UNIFORMS) and the vertex inputs the
renderer offers it (GbuffersInputs, GeometryInputs, AlphaTest, WATER_MASK, WATER_PATCHES) move
unchanged from horizon-shaders' translator, which re-exports them. The renderer's library code
takes them from the engine crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:42:43 +01:00
Ludovic Marie 7ae28b9d3e Merge branch 'split/render-pipeline-stages' into split/render-pipeline-textures 2026-11-09 16:35:58 +01:00
Ludovic Marie 0b7d3f8105 Merge branch 'split/render-pipeline-config' into split/render-pipeline-stages 2026-11-09 16:35:02 +01:00
Horizon agentandClaude Opus 5.5 2f124cc0d2 pack_post.rs: import BUFFERS instead of re-declaring it, so the file doesn't grow
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:34:13 +01:00
Horizon agentandClaude Opus 5.5 950c368a3c Custom textures and render stages move to horizon-render-pipeline
The textures a pipeline's programs read (CustomTexture, Texture, Texels, TextureSource, Dim,
EVERY_STAGE) and the render stages they are told they draw in (RENDER_STAGES, render_stage)
move unchanged from horizon-shaders, which still reads them from a pack and re-exports them.
The renderer takes them from the engine crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:24:07 +01:00
Horizon agentandClaude Opus 5.5 a6093def90 launcher and registry lockfiles: horizon-shader-api depends on horizon-render-pipeline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:21:43 +01:00
Horizon agentandClaude Opus 5.5 80aa1a5b06 Cargo.lock: horizon-shader-api takes bytemuck through the pipeline crate
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:20:23 +01:00
Ludovic Marie 8da02c530e Merge branch 'split/render-pipeline-config' into split/render-pipeline-stages 2026-11-09 16:20:23 +01:00
Horizon agentandClaude Opus 5.5 7081c411dd Cargo.lock: horizon-render-pipeline
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 16:19:52 +01:00
Horizon agentandClaude Opus 5.5 a23f327048 WGSL stage interface moves to horizon-render-pipeline
The stages Horizon's own packs fill, their preludes, bindings and entry points (stages.rs),
the frame uniform the renderer uploads (inputs.rs) and the API lines (Api) are the renderer's
interface, so they move unchanged into the engine's pipeline crate. horizon-shader-api keeps
reading, composing and checking packs against them and re-exports them under the same paths
(launcher and registry unchanged). The renderer takes them from the engine crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:58:12 +01:00
Horizon agentandClaude Opus 5.5 5052d73cd4 horizon-render-pipeline: the pipeline's buffers and shadow map leave horizon-shaders
A new engine crate describes what an external shader pipeline gives the renderer. Its first
part is the colortex buffers and the shadow map (Format, Kind, Buffer, Shadow, Config, the
buffer names): moved unchanged from horizon-shaders' config.rs, which keeps reading them from a
pack's stage files (ReadConfig) and re-exports them. The renderer takes them from the new
crate. Layer plan M6.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:44:45 +01:00
Ludovic Marie ad67d515ad Merge pull request #13916 from horizon-rust/decisions/java-bridge-docs
🤖 Agent: docs only; nothing of the bridge is on main, the offer prompt and benchmark protocol are planned in #13914.
2026-11-09 15:41:40 +01:00
Ludovic Marie 08c8433e3c Merge pull request #13912 from horizon-rust/decisions/unreviewed-translations
🤖 Agent: the review record keeps only text a person checked, so every new machine string starts unreviewed without anyone marking it; seeded from git blame, erring towards unreviewed.
2026-11-09 15:41:36 +01:00
Horizon agentandClaude Opus 5.5 06fba30acc Docs: the Java bridge is a separate optional download, benchmarked against Java before promotion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:26:54 +01:00
Horizon agentandClaude Opus 5.5 b147d89df6 Translations: an unreviewed mark on machine-written strings
The project decided on 2026-11-09 that machine translations carry an
"unreviewed" mark that a person who speaks the language clears.
tools/lang_review/{game,launcher}/<code>.json records the text a person
wrote or checked; every other entry, or one changed since, is unreviewed.
lang_coverage.py counts them, lists them (--unreviewed) and records
checked entries (--mark-reviewed). docs/translations.md has the reviewer
workflow through #translations on Discord. The first record keeps the
1,917 entries git blame traces to single-language human contributors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:21:31 +01:00
Ludovic Marie 1be2c30f53 Merge pull request #13011 from horizon-rust/layers/plan
🤖 Agent: docs only; NikitaMGrimm's hud_layout.rs correction folded in (engine; the widget framework is hud_kit), M3/M4a marked merged, M5/M6 started 2026-11-09 by project decision.
2026-11-09 15:14:42 +01:00
Horizon agent d00cc3aa14 Merge remote-tracking branch 'origin/main' into layers/plan 2026-11-09 15:12:15 +01:00
Horizon agentandClaude Opus 5.5 a9e7940df2 docs/layers.md: M3 and M4's first part merged, M5 and M6 started
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:02:59 +01:00
Horizon agentandClaude Opus 5.5 9f4ef776d4 docs/layers.md: hud_layout.rs is engine; the HUD widget framework is hud_kit
NikitaMGrimm's correction on #13011.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 15:02:22 +01:00
Ludovic Marie 21a750533e Merge pull request #13866 from NikitaMGrimm/windows-pr-ready
🤖 Agent: test-only and tooling changes. The one Rust change opens the folder with FILE_FLAG_BACKUP_SEMANTICS only on Windows, so other platforms run the same test as before
2026-11-09 14:54:56 +01:00
Ludovic Marie 1341217fdf Merge pull request #13847 from NikitaMGrimm/tooling/file-size-skip-test-modules
🤖 Agent: when in doubt the module counts: a test module is skipped only when the brace count and rustfmt's layout agree on its end, so real code can't hide in one
2026-11-09 14:54:52 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 42a15b0561 Python tools and tests, and a mods test: the same results on Windows
- check_file_size and stamp_native_hooks read git's output and their files as UTF-8, not
  in the code page (cp1252 can't decode 0x9d, in UTF-8 such as "”").
- test_optional_release_assets runs the bash on PATH, as test_pr_ready does: a bare "bash"
  is WSL's on Windows, which gets none of the steps' env.
- The live-switch verdict test opens the mod folder so Windows can set its time.

Fixes #13858

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 13:57:12 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 3ff3720bc5 File size check: a test module whose braces close before its last line counts
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 13:34:26 +01:00
Nikita Maximilian GrimmandClaude Opus 5.5 e8474ed6eb File size check: inline test modules don't count
A normal build doesn't compile `#[cfg(test)] mod name { ... }` modules, so they no longer count toward the
3000-line limit, on the base or now. The failure message names both ways out: a sibling `<file>_tests.rs`, or a
pure-move PR first.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 13:27:09 +01:00
Horizon agentandClaude Opus 5.5 73b0a91c13 docs/layers.md: the rule's wording, replays split, the licence note
- duckybertduck's placement rule in their words (tooling excepted; the
  rest can be built on what's below), plus: a feature often splits, the
  part others build on is platform, the finished feature Horizon.
- Replays: recording and playback (.mcpr) are platform; the Studio
  (camera paths, timeline) is Horizon or a native mod.
- Licence: if this repository goes GPL, the linked Horizon layer must be
  GPL-compatible (no closed native code); WASM mods could stay closed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 03:10:39 +01:00
Horizon agentandClaude Opus 5.5 e547357ae7 docs/layers.md: controllers, splitscreen, Replay Studio, UI pages; the placement rule
Merges origin/main (keeps the rewritten page over #12993's edit of the
first version, which it already covers) and, from #contributors:
- duckybertduck's placement rule opens "Where a new feature goes".
- The compositor is engine (part of how the renderer presents a frame,
  idle on vanilla's one-view path), as Martipops and duckybertduck argued.
- Controllers, splitscreen, Replay Studio, the HUD widget framework and a
  UI page API are platform, on engine hooks named in a new section
  (actions and held state today; move vector, look delta and menu focus
  as an input source; screens). PvP presets and widgets stay Horizon.
- Plan states updated (#12983, #12990, #13015, #12993, #13002, #13058
  merged; #13060, #13094), M6 lists the input-source and page work.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 03:08:24 +01:00
Horizon agentandClaude Opus 5.5 b7b5cda0ad docs/layers.md: linked Horizon crates, portable crates, HDR in M6
From NikitaMGrimm's review on #13012:
- The Horizon layer's in-game parts are crates linked into the official
  game build in CI through platform hooks, never compiled by players. The
  official executable is a thin crate in the Horizon repository; this
  repository keeps a plain one.
- The portable crates (core, world, protocol, items, command, client-core,
  assets) take no desktop library; ports beyond the desktop build on them.
  #12997's text folded in; #13002 adds the check. Step P: gameplay into
  them over time.
- HDR's part of M6 is planned with NikitaMGrimm after HDR step 5.
- Credits name NikitaMGrimm (DuckyBertDuck on Discord).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 01:45:36 +01:00
Horizon agent 53776cf792 Merge remote-tracking branch 'origin/main' into layers/plan 2026-11-09 01:45:30 +01:00
Horizon agentandClaude Opus 5.5 7da0bdf46d docs/layers.md: the file-size check is #13015
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 00:58:02 +01:00
Horizon agentandClaude Opus 5.5 991a9b5451 docs/layers.md: the target (Horizon layer in its own repo) and the step plan
Ludovic, 2026-11-09: this repository keeps the engine and the platform,
a complete vanilla-like game; the Horizon layer (its extras, cosmetics,
shop, telemetry, sponsors, the launcher) moves to its own repository
after the split and ships with every official launcher download. So
Horizon uses only the platform's public API. Adds the seam, the
official download paths, the stage's asks (file-size rule, one address
list, sponsors in Horizon, a platform exposing what vanilla uses,
extras as native mods), the 47 exceptions and the M1-M6 plan with its
review gate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 00:54:03 +01:00
72 changed files with 4935 additions and 2063 deletions

No files matched your search

+6 -2
View File
@@ -25,8 +25,10 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
first line `> 🤖 Opened by Ludovic's AI agent (Claude), not by Ludovic himself.`, commits
`Co-Authored-By: Claude <model> <[email protected]>`, last line
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`.
- **Layers** ([docs/layers.md](docs/layers.md)): engine < platform < Horizon; a crate depends only on its own layer or a
lower one (`tools/check_layers.py`, crate layers in `tools/layers.json`). New Horizon code goes behind `horizon-extras`.
- **Layers** ([docs/layers.md](docs/layers.md)): engine uses engine; platform uses engine and platform; Horizon (its extras,
cosmetics, shop, telemetry, launcher) uses platform and Horizon only, never the engine: it moves to its own repository
later. `tools/check_layers.py` checks it. New Horizon code goes behind `horizon-extras`; built-in addresses go in
`horizon-addresses`.
- Everything players run is Rust. Python/Java only for dev tools and harness drivers in `tools/`.
- Horizon-only features follow [docs/design-system.md](docs/design-system.md) and are off by default where
they would change the vanilla look.
@@ -39,6 +41,8 @@ Feature → files → harness for the most-changed areas: [docs/architecture.md]
issue is taken, and so is one labelled `in progress` (claimed on Discord): pick another. `/unclaim` when you stop;
a claim with no open PR linking it is released after 5 days. The issues free to claim, without reading threads:
`gh issue list --search 'is:open no:assignee -label:"in progress" -label:tracker -label:proposal -label:"not claimable" -linked:pr'`
- **Machine translations stay unreviewed.** `tools/lang_review/` records only text a person who speaks the language
wrote or checked; never `lang_coverage.py --mark-reviewed` machine text ([docs/translations.md](docs/translations.md)).
- **Controllers reach what registers.** Draw controls with a registering helper (`ui::button`, `settings_shell::foot_button`); `graphics_style::button` and `screen_draw::button` only paint, and registering one part of a screen ends its crosshair, so register all of it.
## Crates
+15 -4
View File
@@ -45,9 +45,13 @@ and compares their outputs.
[The parity workflow](docs/parity-workflow.md) shows how harnesses run; copy a nearby one.
- **Horizon's own features** (the launcher, the new menus) follow [the design system](docs/design-system.md) and are
off by default wherever they would change the vanilla look.
- **Three layers: engine, platform, Horizon.** Vanilla code never depends on the platform (mods, shader packs,
Horizon-only settings) or on Horizon (cosmetics, the shop, official servers, telemetry), and the platform never
depends on Horizon. [docs/layers.md](docs/layers.md) says where a new feature goes; CI checks the dependencies.
- **Three layers: engine, platform, Horizon.**
- Vanilla code never depends on the platform (mods, shader packs, HDR, the settings system) or on Horizon
(its extras, cosmetics, the shop, official servers, telemetry).
- The platform never depends on Horizon.
- Horizon uses only the platform, never the engine: it will live in its own repository.
[docs/layers.md](docs/layers.md) says where a new feature goes; CI checks the dependencies.
- **Everything players run is Rust.** Python and Java are fine for dev tools and harness drivers in `tools/`, never
for player-facing code.
- **No game files in the repo.** Never commit game files, decompiled source or prepared resources (`.local/`):
@@ -93,7 +97,8 @@ names its sources: the sections of that specification, the documentation pages a
that downloads NVIDIA's files itself, on the player's machine. No NVIDIA binaries, headers or SDK files in the
repository.
- **The JVM bridge** (running Java mods) is only ever a separate, optional download, never installed or on by
default. The game may offer to turn it on when it finds a Java mod; the player decides.
default. The game may offer to turn it on when it finds a Java mod; the player decides. It is promoted out of beta
only after MSPT and RAM benchmarks against the Java client with the same mods ([#13914](https://github.com/horizon-rust/horizon/issues/13914)).
## Before asking for review
@@ -113,6 +118,12 @@ including examples. The dependency scope includes optional, platform, build and
library, binary, integration and doc tests without building unrelated example executables. Shared Cargo
configuration and toolchain changes select the whole workspace; CI also keeps its workspace-wide checks.
### Translations
Most of Horizon's translations were written by machine and stay marked unreviewed until someone who speaks the
language checks them. To report a wrong string or review your language, see [docs/translations.md](docs/translations.md)
and #translations on Horizon's Discord. Never mark machine-written text as reviewed.
## Claim before you start
Work is tracked in GitHub issues, and an issue that is assigned is taken (so is one with the `in progress` label:
Generated
+11 -1
View File
@@ -1593,6 +1593,7 @@ dependencies = [
"horizon-optifine",
"horizon-polytone",
"horizon-protocol",
"horizon-render-pipeline",
"horizon-shader-api",
"horizon-shaders",
"horizon-system",
@@ -1852,6 +1853,14 @@ dependencies = [
"ring",
]
[[package]]
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
]
[[package]]
name = "horizon-server"
version = "0.1.0"
@@ -1900,7 +1909,7 @@ dependencies = [
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"bytemuck",
"horizon-render-pipeline",
"naga",
"png",
"serde",
@@ -1916,6 +1925,7 @@ version = "0.1.0"
dependencies = [
"half",
"horizon-core",
"horizon-render-pipeline",
"naga",
"png",
"zip",
+1
View File
@@ -39,6 +39,7 @@ members = [
"crates/cargo-horizon-mod",
"crates/horizon-port-pack",
"crates/horizon-shaders",
"crates/horizon-render-pipeline",
"crates/horizon-optifine",
"crates/horizon-polytone",
"crates/horizon-shader-api",
+1
View File
@@ -24,6 +24,7 @@ horizon-narrator = { path = "../horizon-narrator" }
horizon-optifine = { path = "../horizon-optifine" }
horizon-polytone = { path = "../horizon-polytone" }
horizon-protocol = { path = "../horizon-protocol" }
horizon-render-pipeline = { path = "../horizon-render-pipeline" }
horizon-shader-api = { path = "../horizon-shader-api" }
horizon-shaders = { path = "../horizon-shaders" }
horizon-system = { path = "../horizon-system" }
+10 -10
View File
@@ -7661,7 +7661,7 @@ impl Renderer {
) -> std::result::Result<Vec<String>, String> {
let linear = pack
.as_ref()
.is_some_and(|p| p.0.manifest.api == horizon_shader_api::Api::V0_2);
.is_some_and(|p| p.0.manifest.api == horizon_render_pipeline::Api::V0_2);
if (linear || self.output.frame.format == LINEAR_FRAME)
&& linear != (self.output.frame.format == LINEAR_FRAME)
&& !std::mem::replace(&mut self.frame_kept_logged, true)
@@ -7714,7 +7714,7 @@ impl Renderer {
inputs.declarations.push_str(
"layout(set = 5, binding = 0) readonly buffer HzIndices { uint hz_indices[]; };\n",
);
horizon_shaders::GeometryInputs {
horizon_render_pipeline::GeometryInputs {
inputs,
pull: pack_atlas::PULL.to_owned(),
index: "hz_indices[@]".to_owned(),
@@ -7737,7 +7737,7 @@ impl Renderer {
}
}
fn pack_terrain_inputs(&mut self) -> Option<horizon_shaders::GbuffersInputs> {
fn pack_terrain_inputs(&mut self) -> Option<horizon_render_pipeline::GbuffersInputs> {
// Flat draws whenever the device has them (`render` keeps them on with a pack).
let flat = self.flat.is_some() && self.flat_on;
// Flat draws read each sprite's place from a table: the atlas is stitched as the game's.
@@ -10410,7 +10410,7 @@ impl Renderer {
self.own_timing.as_mut(),
);
pass.set_bind_group(0, &self.group, &[]);
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
if flat_frame
&& let Some(flat) = &self.flat
&& let Some(pipeline) = &shadow.flat
@@ -10443,7 +10443,7 @@ impl Renderer {
self.own_timing.as_mut(),
);
pass.set_bind_group(0, &self.group, &[]);
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
if flat_frame
&& let Some(flat) = &self.flat
&& let Some(pipeline) = &shadow.flat_translucent
@@ -10475,7 +10475,7 @@ impl Renderer {
.begin_entities(&mut encoder, "Native shader pack entity shadow");
pass.set_pipeline(&shadow.entity);
pass.set_bind_group(0, &self.entity_group, &[]);
pass.set_bind_group(horizon_shader_api::stages::GROUP, &shadow.group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, &shadow.group, &[]);
pass.set_vertex_buffer(0, self.entity_buffer.slice(..));
pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint32);
pass.draw_indexed(0..entity_quads * 6, 0, 0..1);
@@ -10696,7 +10696,7 @@ impl Renderer {
let gpu_occluding = gpu_frame && self.gpu_culling.as_ref().is_some_and(|g| g.occluding());
let pack_frame = |pass: &mut wgpu::RenderPass<'_>| {
if let Some((_, group)) = material {
pass.set_bind_group(horizon_shader_api::stages::GROUP, group, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, group, &[]);
}
};
// A pack's solid terrain that calls `set_surface` also writes the surface target, in
@@ -12084,7 +12084,7 @@ impl Renderer {
&& self
.native
.as_ref()
.is_none_or(|n| n.frame.api == horizon_shader_api::Api::V0_1);
.is_none_or(|n| n.frame.api == horizon_render_pipeline::Api::V0_1);
match (&self.hdr, &surface_frame) {
(Some(hdr), Some(f)) => hdr.present(
&self.queue,
@@ -15830,11 +15830,11 @@ mod shadow_caster_tests {
#[test]
fn the_shadow_pass_draws_terrain_outside_the_pack_s_planes() {
let kept = |near: Option<f32>, far: Option<f32>, y: f64, min: [f32; 3], depth: bool| {
let shadow = horizon_shaders::Shadow {
let shadow = horizon_render_pipeline::Shadow {
distance: 128.0,
near,
far,
..horizon_shaders::Shadow::default()
..horizon_render_pipeline::Shadow::default()
};
let camera = DVec3::new(13.5, y + 1.62, -13.5);
let view = pack_post::ShadowView {
@@ -8,7 +8,8 @@
//! colour. They read the terrain's group 0, the flat draws' group 1 where those run, and the
//! pack's frame at group 3.
use horizon_shader_api::{check::CheckedStage, stages::GROUP};
use horizon_render_pipeline::stages::GROUP;
use horizon_shader_api::check::CheckedStage;
use super::native_post::{NativeFrame, present_constants};
use super::wgsl_prefix::{TERRAIN, prefixed, section};
@@ -369,7 +370,7 @@ fn shader_source(
tags: &[(u32, u32)],
materials: &[(u32, u32)],
(vertex, displaced): (bool, bool),
api: horizon_shader_api::Api,
api: horizon_render_pipeline::Api,
) -> String {
format!(
"{source}\n// Horizon's terrain (terrain.wgsl)\n{}{}{}{}{}{ENTRIES}{}{}",
@@ -398,7 +399,7 @@ fn module(
materials: &[(u32, u32)],
vertex: bool,
displaced: bool,
api: horizon_shader_api::Api,
api: horizon_render_pipeline::Api,
) -> Result<naga::Module, String> {
let text = shader_source(&stage.source, tags, materials, (vertex, displaced), api);
naga::front::wgsl::parse_str(&text)
@@ -417,7 +418,7 @@ impl NativeMaterial {
vertex: bool,
displaced: Option<&DisplacedShape<'_>>,
) -> Result<Self, String> {
let writes_surface = horizon_shader_api::stages::calls_set_surface(&stage.source);
let writes_surface = horizon_render_pipeline::stages::calls_set_surface(&stage.source);
let module = module(
stage,
shape.sprite_tags,
@@ -586,7 +587,7 @@ impl NativeMaterial {
}
#[cfg(test)]
pub(super) fn coverage_test_shader(api: horizon_shader_api::Api) -> String {
pub(super) fn coverage_test_shader(api: horizon_render_pipeline::Api) -> String {
use horizon_shader_api::{MANIFEST, Pack, StageKind, check};
let manifest = format!(
"schema = 1\nid = \"coverage\"\nname = \"Coverage\"\nversion = \"1\"\napi = \"{api}\"\n[material]\nfile = \"material.wgsl\"\n[displacement]\n"
@@ -829,7 +830,7 @@ mod tests {
.into();
let report = check(&Pack::from_files(files).unwrap()).unwrap();
let source = report.stages[0].source.clone()
+ horizon_shader_api::stages::PBR_STUB
+ horizon_render_pipeline::stages::PBR_STUB
+ super::super::native_post::line_helpers(api)
+ entry;
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -1,7 +1,8 @@
//! One of Horizon's own shader packs while it runs (docs/shaders.md): the frame its stages
//! read, its sky, material and post stages, and the timer of their passes.
use horizon_shader_api::{Report, StageKind};
use horizon_render_pipeline::StageKind;
use horizon_shader_api::Report;
use super::displace::Displacement;
use super::native_material::{DisplacedShape, NativeMaterial, TerrainShape};
@@ -253,7 +254,7 @@ impl NativePack {
.transpose()?;
// A 0.2 pack always has one: its decode and tonemap run without a chain too.
let tonemap = others.iter().find(|s| s.kind == StageKind::Tonemap);
let post = (!post.is_empty() || api == horizon_shader_api::Api::V0_2)
let post = (!post.is_empty() || api == horizon_render_pipeline::Api::V0_2)
.then(|| {
let manifest = &report.manifest;
NativePost::new(device, post, manifest, constants, format, &frame, tonemap)
@@ -7,9 +7,9 @@
//! (`NativeFrame`).
use glam::{Mat4, Vec3};
use horizon_shader_api::{
Api, Frame as FrameInputs, StageKind, check::CheckedStage, stages::GROUP,
};
use horizon_render_pipeline::stages::{GROUP, POST_TARGET_BINDING};
use horizon_render_pipeline::{Api, Frame as FrameInputs, StageKind};
use horizon_shader_api::check::CheckedStage;
use super::Frame;
@@ -338,7 +338,7 @@ pub struct NativeFrame {
reflection_sampler: wgpu::Sampler,
/// The shadow map the stages read, and the camera that draws it when the pack asks for one.
pub shadow_map: super::native_shadow::ShadowMap,
shadow_camera: Option<horizon_shaders::Shadow>,
shadow_camera: Option<horizon_render_pipeline::Shadow>,
/// Whether the pack's material reads `reflection` (`NativePack::new`): else no mirror
/// is drawn and `has_reflection` stays 0.
pub reflects: bool,
@@ -1014,10 +1014,9 @@ impl NativePost {
texture_entry(11, wgpu::TextureSampleType::Uint),
];
entries.extend(pack_texture_entries(frame.textures.len()));
entries
.extend((0..targets.len() as u32).map(|j| {
texture_entry(horizon_shader_api::stages::POST_TARGET_BINDING + j, float)
}));
entries.extend(
(0..targets.len() as u32).map(|j| texture_entry(POST_TARGET_BINDING + j, float)),
);
let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Native shader pack post"),
entries: &entries,
@@ -1346,7 +1345,7 @@ impl NativePost {
view
};
wgpu::BindGroupEntry {
binding: horizon_shader_api::stages::POST_TARGET_BINDING + j as u32,
binding: POST_TARGET_BINDING + j as u32,
resource: wgpu::BindingResource::TextureView(view),
}
}));
@@ -1497,7 +1496,7 @@ pub fn frame_inputs(
size: [u32; 2],
seconds: f32,
index: u32,
shadow: Option<&horizon_shaders::Shadow>,
shadow: Option<&horizon_render_pipeline::Shadow>,
) -> FrameInputs {
let aspect = size[0] as f32 / size[1].max(1) as f32;
let view = crate::camera::view_matrix(frame.yaw, frame.pitch);
@@ -1655,7 +1654,7 @@ pub fn pack_textures(
/// Layout entries of `count` pack textures and their samplers.
pub(super) fn pack_texture_entries(count: usize) -> Vec<wgpu::BindGroupLayoutEntry> {
use horizon_shader_api::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
use horizon_render_pipeline::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
(0..count as u32)
.flat_map(|i| {
[
@@ -1684,7 +1683,7 @@ pub(super) fn pack_texture_entries(count: usize) -> Vec<wgpu::BindGroupLayoutEnt
pub(super) fn pack_texture_group_entries(
textures: &[(wgpu::TextureView, wgpu::Sampler)],
) -> Vec<wgpu::BindGroupEntry<'_>> {
use horizon_shader_api::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
use horizon_render_pipeline::stages::{TEXTURE_BINDING, TEXTURE_SAMPLER_BINDING};
textures
.iter()
.enumerate()
@@ -6,8 +6,9 @@
//! draw lists (`ShadowView`) serve both; the map's texels are distorted toward the camera as the
//! prelude's `shadow_position` undoes.
use horizon_shader_api::{Api, Frame as FrameInputs, manifest::Shadow as Settings, stages::GROUP};
use horizon_shaders::Shadow;
use horizon_render_pipeline::Shadow;
use horizon_render_pipeline::{Api, Frame as FrameInputs, stages::GROUP};
use horizon_shader_api::manifest::Shadow as Settings;
use super::native_material::TerrainShape;
use super::wgsl_prefix::{TERRAIN, prefixed, section};
@@ -387,7 +388,7 @@ fn entity_source() -> String {
format!(
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}{ENTITY_ENTRIES}",
FrameInputs::WGSL.wgsl(),
horizon_shader_api::stages::SHADOW_HELPERS,
horizon_render_pipeline::stages::SHADOW_HELPERS,
)
}
@@ -486,7 +487,7 @@ fn shader_source(
(Some(material), _) => (
format!(
"{material}{}{table}{}{}{GRID_ENTRIES}",
horizon_shader_api::stages::PBR_STUB,
horizon_render_pipeline::stages::PBR_STUB,
super::displace::RELIEF,
super::displace::GRID,
),
@@ -504,7 +505,7 @@ fn shader_source(
format!(
"{}@group({GROUP}) @binding(0) var<uniform> frame: Frame;\n{}{UNTAKEN}",
FrameInputs::WGSL.wgsl(),
horizon_shader_api::stages::SHADOW_HELPERS,
horizon_render_pipeline::stages::SHADOW_HELPERS,
),
super::native_material::DISPLACE_VANILLA,
),
@@ -6,7 +6,8 @@
//! names). It reads the renderer's globals (group 0) and the pack's frame (group 3), and
//! draws where vanilla's sky draws; the sun, moon, stars and the dark disc are drawn over it.
use horizon_shader_api::{check::CheckedStage, stages::GROUP};
use horizon_render_pipeline::stages::GROUP;
use horizon_shader_api::check::CheckedStage;
use super::native_post::{NativeFrame, present_constants};
use super::wgsl_prefix::{TERRAIN, prefixed, section};
@@ -40,7 +41,7 @@ fn vanilla_sky() -> String {
}
/// The sky shader for the pack's sky stage `source`.
fn shader_source(source: &str, api: horizon_shader_api::Api) -> String {
fn shader_source(source: &str, api: horizon_render_pipeline::Api) -> String {
format!(
"{source}\n// Horizon's sky (terrain.wgsl)\n{}{}{PACK_ENTRY}",
vanilla_sky(),
@@ -198,7 +199,7 @@ mod tests {
// A pack may use the renderer's names for its own things.
let own = "override gain: f32;\nconst PI: f32 = 3.0;\nfn view_ray() -> f32 { return PI; }\n\
fn sky(input: SkyInput) -> vec4<f32> { return input.color * gain * view_ray(); }\n";
let text = shader_source(&stage(own).source, horizon_shader_api::Api::V0_1);
let text = shader_source(&stage(own).source, horizon_render_pipeline::Api::V0_1);
let module = naga::front::wgsl::parse_str(&text).unwrap_or_else(|e| {
panic!("{}", e.emit_to_string(&text));
});
@@ -261,7 +262,7 @@ mod tests {
Vec::new(),
Default::default(),
None,
horizon_shader_api::Api::V0_1,
horizon_render_pipeline::Api::V0_1,
);
queue.write_buffer(
&frame.buffer,
@@ -597,9 +597,9 @@ const FACE: &str = " uint hz_q = (uint(gl_VertexIndex) & ~3u) * 6u;\n \
/// quad lands whole in one part. The colour's alpha can't decide it, as with `separateAo` it
/// holds each vertex's own ambient occlusion.
pub fn terrain_parts(
terrain: &horizon_shaders::GbuffersInputs,
terrain: &horizon_render_pipeline::GbuffersInputs,
lava: &[u16],
) -> [horizon_shaders::GbuffersInputs; 2] {
) -> [horizon_render_pipeline::GbuffersInputs; 2] {
let opaque = crate::mesher::OPAQUE_LAYER;
let lava: String = lava
.iter()
@@ -619,7 +619,7 @@ pub fn terrain_parts(
impl PackAtlas {
/// The vertex inputs of a gbuffers program drawing terrain (`TerrainVertex`, terrain.wgsl's
/// `vs_terrain` or, for `flat` draws, `vs_terrain_flat`), in atlas coordinates.
pub fn terrain_inputs(&self, flat: bool) -> horizon_shaders::GbuffersInputs {
pub fn terrain_inputs(&self, flat: bool) -> horizon_render_pipeline::GbuffersInputs {
let mut declarations = String::from(
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in uvec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_layer;\nlayout(location = 3) in vec4 hz_v_color;\n\
@@ -753,7 +753,7 @@ impl PackAtlas {
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -2,7 +2,7 @@
//! translator's `DRAW_UNIFORMS`): one slot per render stage of a uniform buffer, picked with a
//! dynamic offset at each draw.
use horizon_shaders::{RENDER_STAGES, render_stage};
use horizon_render_pipeline::{RENDER_STAGES, render_stage};
/// Where a run of `Frame::entities` begins, and what it is (slice 7): the run lasts until the
/// next span. Recorded only while a shader pack draws entities, so Horizon's own frames carry
@@ -6,7 +6,7 @@
use std::ops::Range;
use horizon_shaders::render_stage;
use horizon_render_pipeline::render_stage;
use super::Vertex;
use super::pack_draw::EntitySpan;
@@ -31,7 +31,7 @@ pub fn buffers() -> [Option<wgpu::VertexBufferLayout<'static>>; 1] {
/// position, the shared atlas's UV, colour, light (block, sky in 16ths) and the world-oriented
/// normal. The quad's corners, read from the same vertices at set 1, give the face normal where
/// the vertex has none (custom geometry) and `at_tangent` along the texture's u, as for terrain.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let words = std::mem::size_of::<Vertex>() / 4;
let mut declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
@@ -112,7 +112,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
// bright.
("horizonTier", "float((hz_v_overlay >> 16u) & 255u)"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -121,7 +121,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
.collect(),
after: String::new(),
// Cutout, as vanilla's entity types mostly are (OQ-23): the loader's test at 0.1.
alpha_test: Some(horizon_shaders::AlphaTest {
alpha_test: Some(horizon_render_pipeline::AlphaTest {
op: ">",
reference: 0.1,
}),
@@ -138,7 +138,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
/// inputs of [`inputs`], each vertex read from set 1 (`hz_entity_vertices`), as `Vertex` lays
/// it out. The draw's corners walk its quads two triangles each, as `quad_indices` does: corner
/// `c` of a draw from vertex `first` (a whole quad) is drawn as vertex `first / 4 * 6 + c`.
pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
pub fn geometry_inputs() -> horizon_render_pipeline::GeometryInputs {
let words = std::mem::size_of::<Vertex>() / 4;
let pull = format!(
" uint hz_pw = hz_vertex_index * {words}u;\n \
@@ -150,7 +150,7 @@ pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
hz_v_light = uvec4(hz_pl & 255u, (hz_pl >> 8u) & 255u, (hz_pl >> 16u) & 255u, hz_pl >> 24u);\n \
hz_v_normal = unpackSnorm4x8(hz_entity_vertices[hz_pw + 8u]);\n"
);
horizon_shaders::GeometryInputs {
horizon_render_pipeline::GeometryInputs {
inputs: inputs(),
pull,
// Quad `c / 6`, corner 0, 1, 2, 0, 2, 3 of it.
@@ -163,7 +163,7 @@ pub fn geometry_inputs() -> horizon_shaders::GeometryInputs {
/// What `gbuffers_clouds` reads from the clouds' mesh (entity vertices, `vs_cloud`): the
/// position moved by the clouds' offset, the colour of the face's shade from their palette,
/// full light, the face's normal. Set 1 holds the clouds' uniform (`clouds::Uniform`).
pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
pub fn cloud_inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations = "layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
layout(location = 4) in uvec4 hz_v_light;\nlayout(location = 5) in vec4 hz_v_normal;\n\
@@ -183,7 +183,7 @@ pub fn cloud_inputs() -> horizon_shaders::GbuffersInputs {
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -17,7 +17,7 @@ const UNIFORM: u64 = 2 * 64;
const SLOT: u64 = 32;
/// What `gbuffers_hand` reads: the entity inputs, and the hand's projection from set 1.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let mut inputs = super::pack_entities::inputs();
// No last frame's hand to measure from: the hand's `at_velocity` reads zero (and its set 1
// has no previous vertices).
@@ -270,7 +270,7 @@ fn translucent_quad(quad: &[Vertex]) -> bool {
/// outside block entities, and `currentRenderedItemId`.
fn draw_slot(id: i32, translucent: bool) -> [u8; SLOT as usize] {
let mut bytes = [0u8; SLOT as usize];
let stage = horizon_shaders::render_stage(if translucent {
let stage = horizon_render_pipeline::render_stage(if translucent {
"HAND_TRANSLUCENT"
} else {
"HAND_SOLID"
@@ -4,7 +4,8 @@
use std::collections::BTreeMap;
use horizon_shaders::{BufferObject, CustomImage, Format, Kind};
use horizon_render_pipeline::{Format, Kind};
use horizon_shaders::{BufferObject, CustomImage};
pub(super) struct PackImage {
pub(super) def: CustomImage,
@@ -8,7 +8,7 @@
/// width in pixels. The strip's corners 0 and 1 are at the start, 2 and 3 at the end (six
/// vertices per edge: pack pipelines draw triangle lists); the
/// width is applied in screen space after the pack's transform.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations =
"layout(location = 0) in vec3 hz_l_start;\nlayout(location = 1) in vec3 hz_l_end;\n\
layout(location = 2) in vec4 hz_l_argb;\nlayout(location = 3) in float hz_l_width;\n\
@@ -41,7 +41,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
if ((hz_l_c & 1) != 0) hz_l_o = -hz_l_o;\n \
gl_Position.xy += hz_l_o * gl_Position.w;\n }\n"
.to_owned();
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -18,7 +18,8 @@
use std::collections::HashMap;
use glam::{DMat4, DVec3};
use horizon_shaders::{Format, Group, Kind, Post, SHADOW_COLORS};
use horizon_render_pipeline::{BUFFERS, Format, Kind, SHADOW_COLORS};
use horizon_shaders::{Group, Post};
use smallvec::SmallVec;
use super::pack_draw::DrawUniforms;
@@ -28,8 +29,6 @@ use super::pack_sky::PackSky;
use super::pack_textures;
use super::pack_uniforms::{self, FrameUniforms, PackInputs};
const BUFFERS: usize = horizon_shaders::BUFFERS;
mod packed;
#[cfg(test)]
@@ -126,7 +125,7 @@ enum Source {
ShadowDepth(usize, bool),
/// `shadowcolor0/1` read outside the shadow programs (in them they are `Color`).
ShadowColor(usize),
/// The boats' water patches' depth (`horizon_shaders::WATER_MASK`), for translucent terrain.
/// The boats' water patches' depth (`horizon_render_pipeline::WATER_MASK`), for translucent terrain.
WaterMask,
}
@@ -691,7 +690,7 @@ pub struct PackPost {
/// `depthtex0` at the screen's centre, read back for `centerDepthSmooth`.
center_depth: Option<super::pack_center_depth::CenterDepth>,
/// `shaders.properties`' custom uniforms, after the frame's in `uniforms.values`.
custom: horizon_shaders::Custom,
custom: Box<dyn horizon_render_pipeline::CustomUniforms>,
atlas: Option<wgpu::TextureView>,
material_maps: Option<[wgpu::TextureView; 2]>,
/// The shared atlas's normal and specular paged maps (`set_paged_maps`).
@@ -884,13 +883,13 @@ fn fits(ty: &str, sample_type: wgpu::TextureSampleType) -> bool {
/// `hz_depth_linear` for a program: 1 for each of shadowtex0, shadowtex1, `shadow` and
/// `watershadow` whose depth filters (`linear`, bit n: shadowtex n). `shadow` is shadowtex1 in a
/// program that also declares `watershadow` (6.3).
fn depth_linear(interface: &horizon_shaders::Interface, linear: u8) -> [f32; 4] {
fn depth_linear(interface: &horizon_render_pipeline::Interface, linear: u8) -> [f32; 4] {
let watershadow = interface.samplers.iter().any(|s| s.name == "watershadow");
[0, 1, usize::from(watershadow), 0].map(|n| f32::from(linear >> n & 1))
}
fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool) -> Source {
let shadow_color = horizon_shaders::shadow_color(name);
let shadow_color = horizon_render_pipeline::shadow_color(name);
let shadow_depth = match name {
"shadowtex0" | "watershadow" | "shadowtex0HW" => Some(0),
"shadowtex1" | "shadowtex1HW" => Some(1),
@@ -909,7 +908,7 @@ fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool)
};
}
}
if shadow_space && horizon_shaders::buffer_index(name).is_some() {
if shadow_space && horizon_render_pipeline::buffer_index(name).is_some() {
// colortex buffers are not this pass's.
return Source::Dummy;
}
@@ -922,8 +921,8 @@ fn source(name: &str, shadow_space: bool, shadow_pass: bool, watershadow: bool)
"lightmap" => Source::Lightmap,
"normals" => Source::Normals,
"specular" => Source::Specular,
horizon_shaders::WATER_MASK => Source::WaterMask,
n => horizon_shaders::buffer_index(n).map_or(Source::Dummy, Source::Color),
horizon_render_pipeline::WATER_MASK => Source::WaterMask,
n => horizon_render_pipeline::buffer_index(n).map_or(Source::Dummy, Source::Color),
}
}
@@ -1504,7 +1503,7 @@ fn owned_buffers(
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
fn bindings(
features: wgpu::Features,
interface: &horizon_shaders::Interface,
interface: &horizon_render_pipeline::Interface,
name: &str,
group: Group,
buffers: &[u32],
@@ -1575,12 +1574,12 @@ fn bindings(
// pack of ours (#9064) bound it over `gaux2` and `noisetex` but not over names of its
// own, where a raw texture was bound; this rule is a fit to those cases.
let unbound = t.png
&& t.stage != horizon_shaders::EVERY_STAGE
&& t.stage != horizon_render_pipeline::textures::EVERY_STAGE
&& source(&s.name, false, true, false) == Source::Dummy;
!unbound
&& (t.stage != pack_textures::stage(group)
|| released.0 != t.stage
|| horizon_shaders::buffer_index(&t.name)
|| horizon_render_pipeline::buffer_index(&t.name)
.is_none_or(|b| released.1 & 1 << b == 0))
});
let image = pack
@@ -2284,7 +2283,7 @@ fn build_compute(
device: &wgpu::Device,
name: String,
group: Group,
c: horizon_shaders::ComputeProgram,
c: horizon_render_pipeline::ComputeProgram,
formats: &[wgpu::TextureFormat; BUFFERS],
shadow: Option<&[wgpu::TextureFormat; SHADOW_COLORS]>,
notes: &mut Vec<String>,
@@ -2522,24 +2521,22 @@ impl PackPost {
let shadow_config = post.config.shadow.clone();
let noise_size = post.config.noise_resolution.unwrap_or(256);
let half_lives = post.config.half_lives;
for (name, ticks) in horizon_shaders::HALF_LIVES.iter().zip(half_lives) {
for (name, ticks) in horizon_render_pipeline::HALF_LIVES.iter().zip(half_lives) {
if let Some(t) = ticks {
notes.push(format!("`{name}` is {t} ticks"));
}
}
// shadowcolor2 to 7 only for a pack that names `HIGHER_SHADOWCOLOR`.
let shadow_colors = (1u16 << post.shadow_colors) - 1;
let mut custom = post.custom;
let custom_names: Vec<(String, String)> = (custom.uniforms())
.map(|(name, ty)| (name.to_owned(), ty.to_owned()))
.collect();
let mut custom: Box<dyn horizon_render_pipeline::CustomUniforms> = Box::new(post.custom);
let custom_names = custom.names();
// A first run finds the names neither the frame nor the pack defines.
custom.eval(0.0, &pack_uniforms::zero);
// Its zeros are no start for `smooth`: the first frame's values are, as in Iris.
custom.restart_smoothing();
if !custom.notes.is_empty() {
notes.push(format!("custom uniforms: {}", custom.notes.join("; ")));
custom.notes.clear();
let custom_notes = custom.take_notes();
if !custom_notes.is_empty() {
notes.push(format!("custom uniforms: {}", custom_notes.join("; ")));
}
let mask =
|flips: &[(u32, bool)]| flips.iter().filter(|f| f.1).fold(0u16, |m, f| m | 1 << f.0);
@@ -5395,7 +5392,7 @@ impl PackPost {
self.uniforms.update(frame, inputs, size);
// `smooth` starts on the first frame that knows the camera's biome.
self.custom.world_frame(inputs.biome_known);
self.uniforms.eval_custom(&mut self.custom);
self.uniforms.eval_custom(self.custom.as_mut());
if self.targets.is_none() {
return;
}
@@ -6066,7 +6063,7 @@ mod tests {
texture("composite", "tr", false),
texture("composite", "gaux2", true),
texture("composite", "noisetex", true),
texture(horizon_shaders::EVERY_STAGE, "cp", true),
texture(horizon_render_pipeline::textures::EVERY_STAGE, "cp", true),
];
let names = ["tp", "tr", "gaux2", "noisetex", "cp"];
let interface = horizon_shaders::Interface {
@@ -10,7 +10,7 @@
//! a fit to one driver's values, not a rule Iris states. `RGBA4` and `RGBA2` read back the `RGBA8`
//! bytes in the same probe, as Horizon's 8-bit buffers already do.
use horizon_shaders::{Format, Kind};
use horizon_render_pipeline::{Format, Kind};
use naga::{BinaryOperator, Expression, Literal, MathFunction, Statement, VectorSize};
/// The bits each channel keeps in a buffer of format `f`, for the formats that keep fewer than
@@ -3,7 +3,7 @@
//! with two sides that `shadowcomp` programs flip as composite programs flip colortex (6.1.6).
//! Square, `shadowMapResolution` pixels.
use horizon_shaders::SHADOW_COLORS;
use horizon_render_pipeline::SHADOW_COLORS;
type Ops<T> = wgpu::Operations<T>;
type Color = wgpu::Color;
@@ -97,7 +97,11 @@ pub(super) fn stand_in(device: &wgpu::Device) -> wgpu::TextureView {
}
impl ShadowTargets {
pub fn reset_history(&mut self, device: &wgpu::Device, shadow: &horizon_shaders::Shadow) {
pub fn reset_history(
&mut self,
device: &wgpu::Device,
shadow: &horizon_render_pipeline::Shadow,
) {
let mut colors = 0;
let mut doubled = 0;
let formats = std::array::from_fn(|n| match &self.color[n] {
@@ -125,7 +129,7 @@ impl ShadowTargets {
/// resolution is kept within the device's limit.
pub fn new(
device: &wgpu::Device,
shadow: &horizon_shaders::Shadow,
shadow: &horizon_render_pipeline::Shadow,
formats: [wgpu::TextureFormat; SHADOW_COLORS],
colors: u16,
doubled: u16,
@@ -58,7 +58,7 @@ pub(super) const ATTRIBUTES: [wgpu::VertexAttribute; 2] =
wgpu::vertex_attr_array![0 => Float32x3, 1 => Uint32];
/// What `gbuffers_skybasic` reads from these vertices.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations =
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in uint hz_v_kind;\n\
layout(set = 1, binding = 0) uniform HzSky { mat4 hz_sky_stars; vec4 hz_sky_color; \
@@ -90,7 +90,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
hz_MultiTexCoord2 = hz_MultiTexCoord1;\n \
hz_Normal = vec3(0.0, 0.0, 1.0);\n"
);
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: vec![
@@ -111,8 +111,8 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
}
/// What `gbuffers_skytextured` reads from the sun and moon's vertices.
pub fn celestial_inputs() -> horizon_shaders::GbuffersInputs {
horizon_shaders::GbuffersInputs {
pub fn celestial_inputs() -> horizon_render_pipeline::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations:
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in vec4 hz_v_color;\n"
@@ -1,7 +1,11 @@
//! A shader pack's custom textures (`texture.<stage>.<name>`, `texture.noise`) on the GPU, and
//! which of them a program's sampler reads.
use horizon_shaders::{CustomTexture, Group, Kind, Texels, Texture, TextureDim, TextureSource};
use horizon_render_pipeline::Kind;
use horizon_render_pipeline::textures::{
CustomTexture, Dim as TextureDim, EVERY_STAGE, Texels, Texture, TextureSource,
};
use horizon_shaders::Group;
/// What a custom texture is once uploaded: an image of ours, or one the game already has.
pub(super) enum Image {
@@ -118,24 +122,24 @@ pub(super) fn find(textures: &[PackTexture], group: Group, ty: &str, name: &str)
} else {
wgpu::TextureViewDimension::D2
};
let buffer = horizon_shaders::buffer_index(name);
let buffer = horizon_render_pipeline::buffer_index(name);
let staged = textures.iter().position(|t| {
t.stage == stage(group)
&& t.dimension == dimension
&& (t.name == name
|| buffer.is_some() && horizon_shaders::buffer_index(&t.name) == buffer)
|| buffer.is_some() && horizon_render_pipeline::buffer_index(&t.name) == buffer)
});
staged.or_else(|| {
textures.iter().position(|t| {
t.stage == horizon_shaders::EVERY_STAGE && t.dimension == dimension && t.name == name
})
textures
.iter()
.position(|t| t.stage == EVERY_STAGE && t.dimension == dimension && t.name == name)
})
}
/// The `shaders.properties` key a texture came from, for notes.
fn key(stage: &str, name: &str) -> String {
match stage {
horizon_shaders::EVERY_STAGE => format!("customTexture.{name}"),
EVERY_STAGE => format!("customTexture.{name}"),
_ => format!("texture.{stage}.{name}"),
}
}
@@ -350,7 +354,7 @@ mod tests {
#[test]
fn custom_textures_reach_every_program() {
use wgpu::TextureViewDimension as D;
let every = horizon_shaders::EVERY_STAGE;
let every = EVERY_STAGE;
let textures = [
lightmap(every, "texClouds", D::D3),
lightmap("composite", "texClouds", D::D3),
@@ -28,7 +28,7 @@
//! [-w, w], and the depth buffers packs read hold GL window depth.
use glam::{DMat4, DVec3, Mat4, Vec3, Vec4};
use horizon_shaders::Shadow;
use horizon_render_pipeline::{CustomUniforms, Shadow, Val};
use super::Frame;
@@ -440,7 +440,7 @@ pub struct FrameUniforms {
center_depth: Option<f32>,
/// The pack's shadow settings (7.1).
pub shadow: Shadow,
/// The pack's smoothing half-lives in ticks (`horizon_shaders::HALF_LIVES`).
/// The pack's smoothing half-lives in ticks (`horizon_render_pipeline::HALF_LIVES`).
pub half_lives: [Option<f32>; 4],
}
@@ -1050,8 +1050,8 @@ impl FrameUniforms {
impl FrameUniforms {
/// Evaluates the pack's custom uniforms with this frame's values and puts them after
/// `NAMES`' values, in `custom.uniforms()` order.
pub fn eval_custom(&mut self, custom: &mut horizon_shaders::Custom) {
/// `NAMES`' values, in `custom.names()` order.
pub fn eval_custom(&mut self, custom: &mut dyn CustomUniforms) {
if let Some(tick) = self.custom_restart {
custom.restart_at(tick);
}
@@ -1061,8 +1061,8 @@ impl FrameUniforms {
let v = values.get(index(name)?)?;
let n = components(name);
Some(match *v {
Value::F(f) => horizon_shaders::Val::vector(&f.map(f64::from)[..n]),
Value::I(i) => horizon_shaders::Val::vector(&i.map(f64::from)[..n.min(3)]),
Value::F(f) => Val::vector(&f.map(f64::from)[..n]),
Value::I(i) => Val::vector(&i.map(f64::from)[..n.min(3)]),
})
};
let dt = match index("frameTime").and_then(|i| values.get(i)) {
@@ -1144,9 +1144,9 @@ fn components(name: &str) -> usize {
/// What `name` gives custom uniforms before the first frame: zeros, at its size, so their
/// shapes check out as on a frame (#9086).
pub fn zero(name: &str) -> Option<horizon_shaders::Val> {
pub fn zero(name: &str) -> Option<Val> {
index(name)?;
Some(horizon_shaders::Val::vector(&[0.0; 16][..components(name)]))
Some(Val::vector(&[0.0; 16][..components(name)]))
}
/// The position of `name` in [`NAMES`].
@@ -2717,7 +2717,7 @@ pub(super) mod initial_history_tests {
/// `horizonWaterPatch<k>`'s `k`: patch `k / 2`, its corner and y range (even) or edges (odd).
fn water_patch_index(name: &str) -> Option<usize> {
let k: usize = name.strip_prefix("horizonWaterPatch")?.parse().ok()?;
(k < 2 * horizon_shaders::WATER_PATCHES).then_some(k)
(k < 2 * horizon_render_pipeline::WATER_PATCHES).then_some(k)
}
/// The boats' water patches among the frame's entity quads (`WATER_MASK_OVERLAY`), the first
@@ -2729,7 +2729,7 @@ pub(super) fn water_patches(entities: &[super::Vertex]) -> Vec<[[f32; 4]; 2]> {
let mut points: Vec<[f32; 3]> = Vec::new();
let flush = |points: &mut Vec<[f32; 3]>, out: &mut Vec<[[f32; 4]; 2]>| {
if let Some(b) = patch_box(points)
&& out.len() < horizon_shaders::WATER_PATCHES
&& out.len() < horizon_render_pipeline::WATER_PATCHES
{
out.push(b);
}
@@ -11,7 +11,7 @@ use super::pack_post::Weather;
/// What `gbuffers_weather` reads from a weather vertex: camera-relative position, the UV in
/// repeats of the sprite, colour (with vanilla's fade) and light. Weather has no normal; the
/// pack sees straight up.
pub fn inputs() -> horizon_shaders::GbuffersInputs {
pub fn inputs() -> horizon_render_pipeline::GbuffersInputs {
let declarations =
"layout(location = 0) in vec3 hz_v_pos;\nlayout(location = 1) in vec2 hz_v_uv;\n\
layout(location = 2) in uint hz_v_overlay;\nlayout(location = 3) in vec4 hz_v_color;\n\
@@ -31,7 +31,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
("vaColor", "hz_Color"),
("vaNormal", "hz_Normal"),
];
horizon_shaders::GbuffersInputs {
horizon_render_pipeline::GbuffersInputs {
declarations,
body,
attributes: attributes
@@ -40,7 +40,7 @@ pub fn inputs() -> horizon_shaders::GbuffersInputs {
.collect(),
after: String::new(),
// Vanilla's weather drops texels below 0.1, as Horizon's own weather shader does.
alpha_test: Some(horizon_shaders::AlphaTest {
alpha_test: Some(horizon_render_pipeline::AlphaTest {
op: ">",
reference: 0.1,
}),
@@ -158,7 +158,7 @@ impl Weather {
match native {
Some((pipeline, frame)) => {
pass.set_pipeline(pipeline);
pass.set_bind_group(horizon_shader_api::stages::GROUP, frame, &[]);
pass.set_bind_group(horizon_render_pipeline::stages::GROUP, frame, &[]);
}
None => pass.set_pipeline(&self.pipeline),
}
@@ -225,7 +225,7 @@ struct HorizonWeatherOut {
";
/// The shader of a native pack's weather stage `source`.
fn native_source(source: &str, api: horizon_shader_api::Api) -> String {
fn native_source(source: &str, api: horizon_render_pipeline::Api) -> String {
format!(
"{source}\n// Horizon's weather (entity.wgsl)\n{}{}{NATIVE_ENTRIES}",
super::wgsl_prefix::prefixed(&super::entity_shader(), "horizon_entity_"),
@@ -245,7 +245,7 @@ pub(super) fn native_pipeline(
stage: &horizon_shader_api::check::CheckedStage,
constants: &[(String, f64)],
) -> Result<wgpu::RenderPipeline, String> {
use horizon_shader_api::stages::GROUP;
use horizon_render_pipeline::stages::GROUP;
let text = native_source(&stage.source, frame.api);
let module = naga::front::wgsl::parse_str(&text)
.map_err(|e| format!("{}: {}", stage.file, e.emit_to_string(&text)))?;
@@ -322,7 +322,7 @@ mod tests {
#[test]
fn a_native_weather_stage_validates() {
use horizon_shader_api::{Api, stages::StageKind};
use horizon_render_pipeline::{Api, StageKind};
for api in [Api::V0_1, Api::V0_2] {
let source = StageKind::Weather.prelude(api)
+ "fn weather(input: WeatherInput) -> vec4<f32> {\n \
+10 -4
View File
@@ -969,10 +969,16 @@ mod tests {
assert_eq!(verdict(&ruby, &manifest, true), switches_on_restart);
assert_eq!(verdict(&ruby, &manifest, false), Some(Ok(())));
let later = std::time::SystemTime::now() + std::time::Duration::from_secs(5);
std::fs::File::open(&ruby)
.unwrap()
.set_modified(later)
.unwrap();
let mut folder = std::fs::OpenOptions::new();
folder.read(true);
// Windows opens a folder only with FILE_FLAG_BACKUP_SEMANTICS, and sets its time only
// with write access.
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
folder.write(true).custom_flags(0x0200_0000);
}
folder.open(&ruby).unwrap().set_modified(later).unwrap();
assert_eq!(verdict(&ruby, &manifest, false), switches_on_restart);
std::fs::remove_dir_all(&dir).unwrap();
}
+9 -1
View File
@@ -493,11 +493,19 @@ dependencies = [
]
[[package]]
name = "horizon-shader-api"
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
]
[[package]]
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"horizon-render-pipeline",
"naga",
"png",
"serde",
"serde_json",
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "horizon-render-pipeline"
version = "0.1.0"
edition.workspace = true
license.workspace = true
rust-version.workspace = true
description = "What an external shader pipeline gives the renderer: its buffers, shadow map and programs. Engine layer; the pack formats that fill it are platform crates"
[dependencies]
# The frame uniform is uploaded as bytes (inputs.rs).
bytemuck = { version = "1.24", features = ["derive"] }
# A translated program's stages are naga modules (program.rs).
naga = "30.0.1"
[lints]
workspace = true
+33
View File
@@ -0,0 +1,33 @@
//! The lines of the WGSL stage interface ([`crate::stages`], [`crate::inputs`]).
/// A shader API line: the stages, their signatures and the inputs a pack's WGSL may read.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Api {
/// Vanilla's gamma values.
V0_1,
/// Linear light (docs/design/hdr-linear-light.md). Experimental: checked, not yet drawn.
V0_2,
}
impl Api {
/// Every line this build reads and checks, oldest first.
pub const SERVED: [Api; 2] = [Api::V0_1, Api::V0_2];
/// The manifest's `api` value.
pub const fn as_str(self) -> &'static str {
match self {
Api::V0_1 => "0.1",
Api::V0_2 => "0.2",
}
}
pub fn parse(line: &str) -> Option<Api> {
Self::SERVED.into_iter().find(|a| a.as_str() == line)
}
}
impl std::fmt::Display for Api {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
@@ -0,0 +1,266 @@
//! The pipeline's buffers and shadow map (spec 1.7, 6.1, 6.3, 7.1): each colortex buffer's
//! format, clearing and mipmaps, the shadow map's settings, and the names packs give them. A pack
//! format's reader fills a [`Config`] (`horizon-shaders` reads them from a `shaders/` pack's stage
//! files); the renderer makes its targets from it.
/// colortex buffers (6.1.1).
pub const BUFFERS: usize = 16;
/// The shadowcolor buffers there are: two, and `shadowcolor2` to `shadowcolor7` for a pack that
/// names the `HIGHER_SHADOWCOLOR` feature.
pub const SHADOW_COLORS: usize = 8;
/// A colour format of 6.7, as channels, bits per channel and kind. Packed formats are described
/// by their widest channel: `RGB10_A2` is 4 × 10 bits, `R11F_G11F_B10F` 3 × 11 bits float.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Format {
pub channels: u8,
pub bits: u8,
pub kind: Kind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Unorm,
Snorm,
Float,
Int,
Uint,
}
impl Format {
/// The default `RGBA` (6.1.3).
pub const RGBA8: Format = Format {
channels: 4,
bits: 8,
kind: Kind::Unorm,
};
/// A format name of 6.7, in any case.
pub fn parse(name: &str) -> Option<Format> {
let name = name.trim().to_ascii_uppercase();
let packed = |channels, bits, kind| {
Some(Format {
channels,
bits,
kind,
})
};
match name.as_str() {
"RGBA" => return Some(Self::RGBA8),
"R3_G3_B2" => return packed(3, 3, Kind::Unorm),
"RGB5_A1" => return packed(4, 5, Kind::Unorm),
"RGB10_A2" => return packed(4, 10, Kind::Unorm),
"RGB10_A2UI" => return packed(4, 10, Kind::Uint),
"R11F_G11F_B10F" => return packed(3, 11, Kind::Float),
"RGB9_E5" => return packed(3, 9, Kind::Float),
"RGBA4" => return packed(4, 4, Kind::Unorm),
"RGB565" => return packed(3, 6, Kind::Unorm),
"RGBA2" => return packed(4, 2, Kind::Unorm),
_ => {}
}
let letters = name.bytes().take_while(u8::is_ascii_alphabetic).count();
let channels = match &name[..letters] {
"R" => 1,
"RG" => 2,
"RGB" => 3,
"RGBA" => 4,
_ => return None,
};
let rest = &name[letters..];
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
let bits: u8 = rest[..digits].parse().ok()?;
let kind = match &rest[digits..] {
"" => Kind::Unorm,
"_SNORM" => Kind::Snorm,
"F" => Kind::Float,
"I" => Kind::Int,
"UI" => Kind::Uint,
_ => return None,
};
let valid = match kind {
Kind::Unorm | Kind::Snorm => matches!(bits, 8 | 16),
Kind::Float => matches!(bits, 16 | 32),
Kind::Int | Kind::Uint => matches!(bits, 8 | 16 | 32),
};
valid.then_some(Format {
channels,
bits,
kind,
})
}
}
/// The settings of one colortex buffer.
#[derive(Clone, Debug, PartialEq)]
pub struct Buffer {
pub format: Format,
/// Cleared every frame (`<buffer>Clear`, default true).
pub clear: bool,
/// `<buffer>ClearColor`, else 6.1.5's default: colortex0 the fog colour, colortex1 white,
/// the others transparent black.
pub clear_color: Option<[f32; 4]>,
/// `<buffer>MipmapEnabled`.
pub mipmap: bool,
}
impl Default for Buffer {
fn default() -> Self {
Self {
format: Format::RGBA8,
clear: true,
clear_color: None,
mipmap: false,
}
}
}
/// The shadow map's settings (6.3, 7.1), from `const` lines of the stage files.
#[derive(Clone, Debug, PartialEq)]
pub struct Shadow {
/// `shadowMapResolution`: the map's side in pixels.
pub resolution: u32,
/// `shadowDistance`: half the side of the area the map covers, in blocks.
pub distance: f32,
/// `shadowDistanceRenderMul`: when > 0, geometry beyond `distance × mul` is not drawn.
pub render_mul: f32,
/// `voxelDistance`: with `shadow.culling=reversed`, nothing within it is culled.
pub voxel_distance: f32,
/// `shadowIntervalSize`: the shadow camera moves in steps of this many blocks (0: none).
pub interval: f32,
/// Native packs: the snap's remainder is euclidean, so the centre is within half an interval
/// of the player at negative coordinates too. `shaders/` packs keep Iris's.
pub euclidean_snap: bool,
/// `sunPathRotation`, degrees.
pub sun_path_rotation: f32,
/// `shadowMapFov`: a perspective projection of this many degrees instead of orthographic.
pub fov: Option<f32>,
/// `shadowNearPlane`, `shadowFarPlane`: the orthographic projection's planes, as JOML's
/// `zNear`/`zFar`. None when the pack doesn't set one: Iris 1.11.7 then uses -100.05 and
/// 156 (measured with a probe pack), each on its own.
pub near: Option<f32>,
pub far: Option<f32>,
/// `shadowHardwareFiltering(0|1)`: shadowtex0/1 sampled with depth comparison.
pub hardware_filtering: [bool; 2],
/// `shadowtex0Nearest` (or `shadowtexNearest`, `shadow0MinMagNearest`) and
/// `shadowtex1Nearest` (or `shadow1MinMagNearest`): shadowtex0/1 read through a plain sampler
/// take the nearest texel. Otherwise Iris 1.11.7 filters them linearly (measured).
pub nearest: [bool; 2],
/// `shadowcolor0` to `shadowcolor7`: format, cleared every frame, clear colour (default
/// white).
pub colors: [Buffer; SHADOW_COLORS],
}
impl Default for Shadow {
fn default() -> Self {
let white = Buffer {
clear_color: Some([1.0; 4]),
..Buffer::default()
};
Self {
resolution: 1024,
distance: 160.0,
render_mul: -1.0,
voxel_distance: 160.0,
interval: 2.0,
euclidean_snap: false,
sun_path_rotation: 0.0,
fov: None,
near: None,
far: None,
hardware_filtering: [false; 2],
nearest: [false; 2],
colors: std::array::from_fn(|_| white.clone()),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
pub buffers: [Buffer; BUFFERS],
pub shadow: Shadow,
/// `noiseTextureResolution`: the side of the `noisetex` Horizon makes when the pack gives none.
pub noise_resolution: Option<u32>,
/// The smoothed uniforms' half-lives in ticks, in [`HALF_LIVES`] order, when the pack sets
/// them (`const float wetnessHalflife = 30.0;`...).
pub half_lives: [Option<f32>; 4],
/// Conflicting or unreadable settings, as `file:line: message`.
pub notes: Vec<String>,
/// Each colortex setting the pack's files set so far and its value, so that its reader can
/// report a later file changing it.
pub settings: Vec<(String, String)>,
}
impl Default for Config {
fn default() -> Self {
Self {
buffers: std::array::from_fn(|_| Buffer::default()),
shadow: Shadow::default(),
noise_resolution: None,
half_lives: [None; 4],
notes: Vec::new(),
settings: Vec::new(),
}
}
}
/// The shadowcolor buffer a sampler or image name reads: `shadowcolor` and `shadowcolor0` to
/// `shadowcolor7`.
pub fn shadow_color(name: &str) -> Option<usize> {
match name.strip_prefix("shadowcolor")?.as_bytes() {
[] => Some(0),
&[d @ b'0'..=b'7'] => Some(usize::from(d - b'0')),
_ => None,
}
}
/// The colortex index of a buffer name or alias (6.1.1).
pub fn buffer_index(name: &str) -> Option<usize> {
let alias = [
"gcolor",
"gdepth",
"gnormal",
"composite",
"gaux1",
"gaux2",
"gaux3",
"gaux4",
];
if let Some(i) = alias.iter().position(|a| *a == name) {
return Some(i);
}
let n: usize = name.strip_prefix("colortex")?.parse().ok()?;
(n < BUFFERS).then_some(n)
}
/// The `const float` half-lives (ShaderDoc, OptiFine's shaders.txt) of `wetness` rising and
/// falling, `eyeBrightnessSmooth` and `centerDepthSmooth`, in ticks.
pub const HALF_LIVES: [&str; 4] = [
"wetnessHalflife",
"drynessHalflife",
"eyeBrightnessHalflife",
"centerDepthHalflife",
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_parse() {
let f = |channels, bits, kind| {
Some(Format {
channels,
bits,
kind,
})
};
assert_eq!(Format::parse("rgba16f"), f(4, 16, Kind::Float));
assert_eq!(Format::parse("R32UI"), f(1, 32, Kind::Uint));
assert_eq!(Format::parse("RG8_SNORM"), f(2, 8, Kind::Snorm));
assert_eq!(Format::parse("R11F_G11F_B10F"), f(3, 11, Kind::Float));
assert_eq!(Format::parse("RGB16"), f(3, 16, Kind::Unorm));
assert_eq!(Format::parse("RGBA12"), None);
assert_eq!(Format::parse("R16_SNORMX"), None);
}
}
@@ -0,0 +1,53 @@
//! A pack's custom uniforms: values the pack computes on the CPU each frame from the game's own
//! uniforms, which the renderer then hands its programs after the frame's. The renderer only
//! drives the evaluator ([`CustomUniforms`]); the pack format supplies it (`horizon-shaders`'
//! `Custom` reads `shaders.properties`' expressions).
/// A value: up to 16 components (matrices only as names, for `m.column.row`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Val {
pub v: [f64; 16],
/// Components: 1 for a scalar.
pub n: usize,
}
impl Val {
pub fn scalar(x: f64) -> Self {
let mut v = [0.0; 16];
v[0] = x;
Self { v, n: 1 }
}
pub fn vector(c: &[f64]) -> Self {
let mut v = [0.0; 16];
v[..c.len()].copy_from_slice(c);
Self { v, n: c.len() }
}
}
/// What the renderer asks of a pack's custom uniforms. Called once per frame of a pack, never on
/// vanilla's path.
pub trait CustomUniforms: Send + Sync {
/// The uniforms' names and declared types, in the order [`CustomUniforms::eval`] returns
/// their values.
fn names(&self) -> Vec<(String, String)>;
/// Evaluates everything for a frame `dt` seconds after the last; `builtin` gives the values
/// of the game's uniforms and constants. Returns the uniforms' values, in `names` order.
fn eval(&mut self, dt: f64, builtin: &dyn Fn(&str) -> Option<Val>) -> Vec<Val>;
/// Forgets what smoothing keeps: each smoothed value starts again from its next value.
fn restart_smoothing(&mut self);
/// Called before each frame's `eval`, with whether the camera's biome is known.
fn world_frame(&mut self, biome_known: bool);
/// Forgets the world's frames, as a pack just loaded has seen none.
fn forget_world(&mut self);
/// Restarts smoothing and randomness for a frame of the world's tick `tick`, so a frame
/// drawn again at that tick evaluates the same.
fn restart_at(&mut self, tick: u64);
/// The problems found so far (expressions that did not parse, unknown names), emptied.
fn take_notes(&mut self) -> Vec<String>;
}
@@ -0,0 +1,380 @@
//! What a pack's WGSL reads: the frame uniform every stage sees, which the
//! renderer fills and uploads as the Rust struct below, and the stage input
//! and output structs. The WGSL of each is generated from these definitions
//! (`Struct::wgsl`), and so is the reference (docs/api/shaders-reference.md).
/// One field of a WGSL struct, for the prelude and the reference.
#[derive(Debug)]
pub struct Field {
pub name: &'static str,
pub ty: &'static str,
/// Its meaning, unit and range.
pub doc: &'static str,
}
/// A struct a pack's WGSL can name.
#[derive(Debug)]
pub struct Struct {
pub name: &'static str,
pub doc: &'static str,
pub fields: &'static [Field],
}
impl Struct {
pub fn wgsl(&self) -> String {
let mut out = format!("struct {} {{\n", self.name);
for f in self.fields {
out += &format!(" {}: {},\n", f.name, f.ty);
}
out + "}\n"
}
}
/// The WGSL type of a Rust field the renderer uploads.
pub trait Wgsl {
const TYPE: &'static str;
}
macro_rules! wgsl_types {
($($rust:ty => $wgsl:literal),* $(,)?) => {
$(impl Wgsl for $rust {
const TYPE: &'static str = $wgsl;
})*
};
}
wgsl_types! {
f32 => "f32",
u32 => "u32",
i32 => "i32",
[f32; 2] => "vec2<f32>",
[f32; 3] => "vec3<f32>",
[f32; 4] => "vec4<f32>",
[[f32; 4]; 4] => "mat4x4<f32>",
[[f32; 4]; 8] => "array<vec4<f32>, 8>",
}
/// A uniform struct: the Rust type the renderer uploads, and its WGSL. `pad` names a trailing
/// field in Rust only, which fills the struct out to WGSL's size (a multiple of its 16-byte
/// alignment), since `Pod` allows no implicit padding.
macro_rules! uniform {
($(#[doc = $doc:literal])* $name:ident {
$($(#[doc = $fdoc:literal])* $field:ident: $ty:ty,)*
} $(pad $pad:ident: $pad_ty:ty)?) => {
$(#[doc = $doc])*
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct $name {
$($(#[doc = $fdoc])* pub $field: $ty,)*
$(/// Not in the WGSL: zeros up to its size.
pub $pad: $pad_ty,)?
}
impl $name {
pub const WGSL: Struct = Struct {
name: stringify!($name),
doc: concat!($($doc, "\n"),*),
fields: &[$(Field {
name: stringify!($field),
ty: <$ty as Wgsl>::TYPE,
doc: concat!($($fdoc),*),
}),*],
};
/// Each field's byte offset, in `WGSL.fields`' order.
pub const OFFSETS: &'static [usize] = &[$(std::mem::offset_of!($name, $field)),*];
}
};
}
uniform! {
/// The frame, as `frame` in every stage. Positions and directions are
/// in world axes relative to the camera (x east, y up, z south), so they
/// stay precise far from the world's origin; depth is reversed (1 near,
/// 0 at the far plane and in the sky).
Frame {
/// Camera-relative world to view space: the camera's rotation.
view: [[f32; 4]; 4],
/// View to clip space (reversed depth, wgpu's clip space: z 0..1).
projection: [[f32; 4]; 4],
/// View to camera-relative world space.
view_inverse: [[f32; 4]; 4],
/// Clip to view space.
projection_inverse: [[f32; 4]; 4],
/// The camera's world position, in blocks (about a millimetre of precision 10 000 blocks out).
camera_position: [f32; 3],
/// Seconds since the pack was loaded, wrapping to 0 every hour.
time: f32,
/// Unit vector toward the sun (below the horizon at night).
sun_direction: [f32; 3],
/// The time of day, 0..1: 0 sunrise, 0.25 noon, 0.5 sunset, 0.75 midnight.
day_time: f32,
/// Unit vector toward the moon.
moon_direction: [f32; 3],
/// Vanilla's GameTime: (game time % 24000 + partial tick) / 24000, 0..1.
game_time: f32,
/// Vanilla's sky colour this frame, 0..1.
sky_color: [f32; 3],
/// Rain strength, 0..1.
rain: f32,
/// Vanilla's fog colour this frame (also the clear colour), 0..1.
fog_color: [f32; 3],
/// Thunder strength, 0..1.
thunder: f32,
/// The size of the world's picture, in pixels (the resolution scale applied).
resolution: [f32; 2],
/// Block and sky light at the camera, 0..1 (level / 15).
eye_light: [f32; 2],
/// Where vanilla's fog starts, in blocks from the camera.
fog_start: f32,
/// Where vanilla's fog is full, in blocks.
fog_end: f32,
/// The render distance, in blocks.
render_distance: f32,
/// The far plane, in blocks.
far: f32,
/// Night vision strength, 0..1.
night_vision: f32,
/// Blindness strength, 0..1.
blindness: f32,
/// The darkness effect's strength, 0..1.
darkness: f32,
/// The Brightness option, 0..1 (Moody to Bright).
brightness: f32,
/// What the camera is in: 0 air, 1 water, 2 lava, 3 powder snow.
eye_in: u32,
/// 0 the Overworld, -1 the Nether, 1 the End, 2 any other dimension.
dimension: i32,
/// Frames drawn since the pack was loaded (wraps).
frame_index: u32,
/// 1 while F1 hides the HUD, else 0.
hide_gui: u32,
/// What falls in the biome at the camera while it rains: 0 nothing, 1 rain, 2 snow.
precipitation: u32,
/// The biome at the camera's temperature (vanilla's, -0.7..2.0; it snows below 0.15).
temperature: f32,
/// The biome at the camera's downfall, 0..1.
downfall: f32,
/// `rain`, smoothed: rises over about 30 seconds and dries over about 10, 0..1.
wetness: f32,
/// Last frame's camera-relative world to clip space (its `projection * view`); a point
/// at camera-relative `p` now was at `p + camera_motion` relative to last frame's camera.
/// This frame's on the first frame and after a gap of over half a second (a pause, a
/// world load), when last frame's camera says nothing about this one.
previous_view_projection: [[f32; 4]; 4],
/// How far the camera moved since last frame, in blocks (0 after such a gap).
camera_motion: [f32; 3],
/// Seconds since last frame (0 on the first and after such a gap).
delta_time: f32,
/// `eye_light` eased with a half-life of 2 seconds: it follows the light levels' steps
/// without stepping itself (for exposure and indoor fog).
eye_light_smooth: [f32; 2],
/// 1 while the camera is in water (`eye_in` 1), eased with a half-life of a quarter
/// second in and out.
in_water: f32,
/// The same for lava (`eye_in` 2).
in_lava: f32,
/// The y of the horizontal water plane `reflection` mirrors the world about, in world
/// blocks, while `has_reflection` is 1.
water_plane: f32,
/// 1 while the material stage's `reflection` holds this frame's water mirror, else 0.
has_reflection: u32,
/// The size of `reflection`, in pixels: half the world's picture.
reflection_resolution: [f32; 2],
/// Eight values a native mod hands its own pack each frame (`native_shader_params`,
/// docs/native-mods.md): what they mean is that mod's, such as where and when
/// something happened in the world. All 0 when no mod sets them.
mods: [[f32; 4]; 8],
/// Unit vector toward the body casting this frame's shadows: the sun by day, the moon
/// by night.
light_direction: [f32; 3],
/// 1 while `shadow_map` holds this frame's shadow map, else 0.
has_shadow: u32,
/// Camera-relative world to the shadow camera's clip space, before the distortion
/// (orthographic, looking along `-light_direction`; z 0 at the light, 1 away from it).
shadow_view_proj: [[f32; 4]; 4],
/// Half the side of the square the shadow map covers, in blocks (the manifest's
/// `[shadow] distance`); 0 without a shadow map.
shadow_distance: f32,
/// How strongly the shadow map is distorted toward its centre, 0..1: a point at
/// shadow clip `xy` is stored at `xy / (length(xy) * k + 1 - k)`, so texels near the
/// camera cover less ground (`shadow_position` applies it).
shadow_distortion: f32,
/// The shadow map's side, in texels; 0 without a shadow map.
shadow_resolution: f32,
/// The ground one shadow texel covers at the map's centre, in blocks (for a bias that
/// scales with it); 0 without a shadow map.
shadow_center_texel: f32,
/// 1 while the output is HDR, else 0.
hdr: u32,
/// The peak over paper white: the brightest value the display shows (Apple's EDR
/// headroom, Android's HDR/SDR ratio). 1 in SDR. It changes while the game runs (the
/// system's SDR white, a calibration, another display), so read it every frame.
headroom: f32,
/// Paper white in nits; a nominal 80 in SDR.
paper_white: f32,
/// The peak brightness in nits; a nominal 80 in SDR, as `paper_white`.
peak: f32,
/// The display's black in nits (HDR's Black Level); 0 in SDR.
black_level: f32,
} pad padding: [u32; 3]
}
impl Frame {
/// The fields 0.1's prelude declares: the struct up to `shadow_center_texel`, a prefix of
/// the buffer the renderer uploads.
pub const FIELDS_0_1: usize = 49;
/// `Frame` as the line's prelude declares it.
pub fn line(api: crate::Api) -> Struct {
let fields = match api {
crate::Api::V0_1 => &Self::WGSL.fields[..Self::FIELDS_0_1],
crate::Api::V0_2 => Self::WGSL.fields,
};
Struct {
fields,
..Self::WGSL
}
}
}
macro_rules! fields {
($($name:ident: $ty:literal = $doc:literal,)*) => {
&[$(Field { name: stringify!($name), ty: $ty, doc: $doc }),*]
};
}
/// The post stage's input: one pixel of the picture.
pub const POST_INPUT: Struct = Struct {
name: "PostInput",
doc: "One pixel of the picture, for `fn post(input: PostInput) -> vec4<f32>`.",
fields: fields! {
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of what the pass draws (the world's size; the window's for the last pass).",
color: "vec4<f32>" = "The previous pass's colour here (the world's picture for the first pass).",
depth: "f32" = "The world's depth here, reversed: 0 is the sky.",
},
};
/// The material stage's input: one point of a block's surface.
pub const MATERIAL_INPUT: Struct = Struct {
name: "MaterialInput",
doc: "One point of a block's surface, for `fn material(input: MaterialInput) -> Surface`.",
fields: fields! {
albedo: "vec4<f32>" = "The block texture's colour here (alpha 0..1).",
tint: "vec4<f32>" = "The vertex colour: biome tint, face shade and ambient occlusion.",
light: "vec2<f32>" = "Block and sky light, 0..1, smoothed over the face.",
lightmap: "vec3<f32>" = "Vanilla's light colour for that light this frame.",
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
uv: "vec2<f32>" = "The texture coordinate inside the block atlas.",
translucent: "bool" = "Drawn in the translucent pass (water, stained glass, ice).",
tags: "u32" = "The pack's sprite tags this surface's sprite has: bit i for the manifest's i-th `[[tags]]`.",
material: "u32" = "The `[[materials]]` id of this surface's sprite (0 when no entry names it).",
screen: "vec2<f32>" = "Where the point is on the screen, 0..1 from the top left (`opaque_depth` and `opaque_color` at `screen * frame.resolution`).",
face: "u32" = "For a fluid, which face of the block the point is on, `Direction` order: 0 down, 1 up, 2 north, 3 south, 4 west, 5 east. 0 elsewhere.",
fluid: "u32" = "0 not a fluid, 1 water, 2 lava.",
emission: "f32" = "How strongly the block itself shines, 0..1, by its kind of light: a redstone torch or a candle 7/15, a torch or lava 14/15, glowstone 1; 0 for most blocks.",
normal: "vec3<f32>" = "The surface's geometric normal, a unit vector in world axes, facing the camera. A normal within about 3.6 degrees of an axis is that axis exactly, so distant block faces do not shimmer; flowing water's slopes are steeper and keep theirs.",
tangent: "vec4<f32>" = "The surface's tangent: xyz the unit direction in world axes along which `uv.x` grows, w the sign (1 or -1) that makes `cross(normal, tangent.xyz) * tangent.w` the direction `uv.y` grows along (down the texture). With `normal` it turns a normal map's normals into world axes.",
colored_light: "vec3<f32>" = "The frame's coloured lights at this point (`[[particle_lights]]`, held and dropped items in their `[[lights]]` colour while dynamic lights are on, and mods' visual lights), 0..1, to light the surface with as the pack likes; 0 where none reaches.",
displaced: "f32" = "How much of the relief `[displacement]` gave this point as geometry, 0..1: 1 near the camera, where the surface already lies where `displacement` put it (`position` is that point; a parallax effect would count its depth twice), easing to 0 at `[displacement] distance`; always 0 without `[displacement]`.",
},
};
/// What the material stage returns.
pub const SURFACE: Struct = Struct {
name: "Surface",
doc: "What `material` returns. Vanilla's own is `albedo * tint * vec4(lightmap, 1.0)`; \
Horizon then applies the alpha test (cutout and translucent) and fog.",
fields: fields! {
color: "vec4<f32>" = "The lit colour, 0..1, with its alpha.",
},
};
/// The sky stage's input: one direction of the sky.
pub const SKY_INPUT: Struct = Struct {
name: "SkyInput",
doc: "One direction of the sky, for `fn sky(input: SkyInput) -> vec4<f32>`. The sun, moon, \
stars and clouds are drawn over the result.",
fields: fields! {
direction: "vec3<f32>" = "Unit view ray, camera-relative world axes.",
color: "vec4<f32>" = "Vanilla's sky colour along the ray (sky, fog and sunrise).",
},
};
/// The input of the material stage's `displacement`, under `[displacement]`.
pub const DISPLACEMENT_INPUT: Struct = Struct {
name: "DisplacementInput",
doc: "One corner of the grid a block face near the camera is cut into, for `fn displacement(input: DisplacementInput) -> f32` (`[displacement]`), which returns how deep below the face, in blocks, the surface lies at `height`.",
fields: fields! {
position: "vec3<f32>" = "The corner on the flat face, camera-relative, in blocks.",
normal: "vec3<f32>" = "The face's normal, a unit vector in world axes.",
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite (as `MaterialInput.uv`).",
material: "u32" = "The `[[materials]]` id of the block texture (0 when none matches).",
height: "f32" = "A LabPBR height, 0..1 (1 the top of the relief): the normal map's alpha here, or the texture's average height (what the face's edges are pinned to).",
},
};
/// The vertex stage's input: one corner of a block's surface.
pub const VERTEX_INPUT: Struct = Struct {
name: "VertexInput",
doc: "One corner of a block's surface, for `fn vertex(input: VertexInput) -> vec3<f32>`, which returns where it is drawn (camera-relative, in blocks; `input.position` where it stays).",
fields: fields! {
position: "vec3<f32>" = "The corner, camera-relative, in blocks (`+ frame.camera_position` for the world's).",
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite.",
material: "u32" = "The `[[materials]]` id of the block's sprite (0 when none matches).",
light: "vec2<f32>" = "Block and sky light, 0..1.",
tint: "vec4<f32>" = "The vertex colour (biome tint, face shade, ambient occlusion); alpha 1.",
face: "u32" = "For a fluid, the face of the block the corner is on (as `MaterialInput.face`), else 0.",
fluid: "u32" = "1 on a fluid's surface (water or lava), else 0.",
},
};
/// The weather stage's input: one point of a rain or snow streak.
pub const WEATHER_INPUT: Struct = Struct {
name: "WeatherInput",
doc: "One point of a rain or snow streak, for `fn weather(input: WeatherInput) -> vec4<f32>`, \
which returns the colour blended over the world (alpha its opacity).",
fields: fields! {
color: "vec4<f32>" = "The rain or snow texture's colour here times the streak's vertex colour (alpha 0.1..1).",
light: "vec2<f32>" = "Block and sky light, 0..1.",
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
snow: "bool" = "A snow streak (rain otherwise).",
},
};
/// The tonemap stage's input (0.2): one pixel of the window.
pub const TONEMAP_INPUT: Struct = Struct {
name: "TonemapInput",
doc: "One pixel of an output (the window), for `fn tonemap(input: TonemapInput) -> vec3<f32>`, which returns its colour for the output `frame` describes: linear light, 1.0 at paper white, from 0 to `frame.headroom` (Horizon clamps it there).",
fields: fields! {
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of the output (the window).",
color: "vec3<f32>" = "The post chain's picture here (the world's without one), linear light, never negative or NaN.",
},
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_fields_carry_their_docs() {
let wgsl = Frame::WGSL;
assert_eq!(wgsl.fields.len(), 54);
assert!(wgsl.fields.iter().all(|f| !f.doc.trim().is_empty()));
// 0.1's 49 fields, then 0.2's five and the padding to a multiple of 16.
assert_eq!(
std::mem::size_of::<Frame>(),
6 * 64 + 15 * 16 + 8 * 16 + 2 * 16
);
assert_eq!(Frame::OFFSETS[Frame::FIELDS_0_1], 6 * 64 + 15 * 16 + 8 * 16);
assert_eq!(
Frame::line(crate::Api::V0_1).fields.len(),
Frame::FIELDS_0_1
);
assert!(
wgsl.wgsl()
.contains(" camera_position: vec3<f32>,\n time: f32,\n")
);
}
}
+37
View File
@@ -0,0 +1,37 @@
//! The external shader pipeline the renderer runs (docs/layers.md, layer plan M6): what a shader
//! pack hands the renderer once its own format is read. The renderer (`horizon-client-render`,
//! engine) builds its targets and passes from these types and does nothing with them on
//! vanilla's path; the pack formats that fill them are platform crates (`horizon-shaders` reads
//! `shaders/` packs). No wgpu and no pack file here.
//!
//! - [`config`]: the colortex buffers and the shadow map: formats, clearing, mipmaps, settings.
//! - [`custom`]: the pack's custom uniforms, which the renderer evaluates each frame.
//! - [`program`]: a translated program (naga modules and their interface) and the vertex
//! inputs the renderer offers it.
//! - [`textures`]: the custom textures the programs read; [`RENDER_STAGES`]: the stages they draw in.
//! - [`stages`], [`inputs`], [`Api`]: the WGSL stage interface Horizon's own packs are written
//! against (`horizon-shader-api` reads and checks those packs): the stages' functions, preludes,
//! bindings and entry points, and the frame uniform the renderer fills.
mod api;
pub mod config;
pub mod custom;
pub mod inputs;
pub mod program;
mod render_stage;
pub mod stages;
pub mod textures;
pub use api::Api;
pub use config::{
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, SHADOW_COLORS, Shadow, buffer_index,
shadow_color,
};
pub use custom::{CustomUniforms, Val};
pub use inputs::Frame;
pub use program::{
AlphaTest, ComputeProgram, GbuffersInputs, GeometryInputs, Interface, Program, WATER_MASK,
WATER_PATCHES,
};
pub use render_stage::{RENDER_STAGES, render_stage};
pub use stages::StageKind;
@@ -0,0 +1,199 @@
//! A translated program as the renderer builds its pipelines from it: its naga modules and the
//! interface they expect (uniforms, samplers, images, outputs), and what the renderer offers a
//! program's vertex stage (`GbuffersInputs`, `GeometryInputs`). `horizon-shaders` translates a
//! `shaders/` pack's GLSL into these.
/// What a program's stages need from the game.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Interface {
/// The uniform block at binding 0: its size and each value's name, GLSL type and offset.
/// Ours come first: `hz_target` (the target's width, height, 1/width, 1/height), and the
/// fixed-function fog the compatibility `gl_Fog` reads (OQ-28): `hz_fog_color` (vanilla's fog
/// colour) and `hz_fog` (density, start, end, 1 / (end - start)), then `hz_depth_linear`:
/// 1 where `shadowtex0`, `shadowtex1`, `shadow` and `watershadow` (x to w) read through a
/// plain sampler filter linearly, 0 where they take the nearest texel.
pub uniform_size: u32,
pub uniforms: Vec<Uniform>,
/// std140 initial contents; runtime values overwrite their members each frame.
pub initial_values: Vec<u8>,
/// Each sampler: texture at `binding`, sampler object at `binding + 1`.
pub samplers: Vec<Sampler>,
/// The fragment stage's output locations.
pub outputs: Vec<u32>,
/// A gbuffers program reads `DRAW_UNIFORMS`: the block at set 2, binding 0, set per draw.
pub draw: bool,
/// Storage images (`colorimg4`...), after the samplers.
pub images: Vec<Image>,
/// The SSBOs' bindings in set 4 (`bufferObject.<n>`).
pub buffers: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Image {
pub name: String,
/// `image2D`, `uimage3D`...
pub ty: String,
/// The format qualifier (`rgba16f`).
pub format: String,
/// `readonly`, `writeonly`, empty for both, or `atomic` for an image an atomic function
/// uses.
pub access: String,
pub binding: u32,
/// A compute program's copy of image `before` as it was before the dispatch, which its
/// loads read (see `ordering`).
pub before: Option<String>,
}
/// A compute program (`deferred4_a.csh`): its module, what it reads and writes, and how many
/// work groups it runs: `workGroups`, or `workGroupsRender` times the target's size over the
/// local size (1, 1 when neither is set).
#[derive(Clone)]
pub struct ComputeProgram {
pub module: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
pub work_groups: Option<[u32; 3]>,
pub work_groups_render: [f32; 2],
}
/// The uniforms a gbuffers program reads per draw (5.3's "D"), in a block of their own
/// (`HzDraw`, std140: four ints, a vec4; 32 bytes) the game sets for each draw.
pub const DRAW_UNIFORMS: [(&str, &str); 5] = [
("renderStage", "int"),
("entityId", "int"),
("blockEntityId", "int"),
("currentRenderedItemId", "int"),
("entityColor", "vec4"),
];
#[derive(Clone, Debug, PartialEq)]
pub struct Uniform {
pub name: String,
/// The type the pack declared (`float`, `ivec2`, `mat4`; `bool` is stored as an `int`).
pub ty: String,
pub offset: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sampler {
pub name: String,
/// The type the pack declared: `sampler2D`, `sampler2DShadow`, `usampler2D`, `sampler3D`...
pub ty: String,
pub binding: u32,
}
/// A translation diagnostic at a pack file and line.
#[derive(Clone, Debug, PartialEq)]
pub struct Error {
pub file: String,
pub line: u32,
pub message: String,
}
#[derive(Clone)]
pub struct Program {
pub vertex: naga::Module,
pub fragment: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
}
/// Where the program draws, which decides the conventions of 5.5.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Target {
/// Pack buffers: rows stored as GL stores them.
Buffers,
/// The window (`final`).
Window,
}
/// What a gbuffers-style program's vertex stage reads. The game writes it: it knows its vertex
/// formats, the translator only places it.
#[derive(Clone, Debug, Default)]
pub struct GbuffersInputs {
/// GLSL declarations: the vertex inputs and any bindings of set 1.
pub declarations: String,
/// GLSL statements that set `hz_Vertex`, `hz_Color`, `hz_MultiTexCoord0`,
/// `hz_MultiTexCoord1` and `hz_Normal` (the compatibility inputs), run first in `main`.
pub body: String,
/// The value of each attribute a pack may declare (`mc_Entity`, `vaPosition`...), as a GLSL
/// expression of the declared type's size; an attribute not listed reads zero. Terrain
/// also supplies `mc_chunkFade`, an implicit float input rather than a declared attribute.
pub attributes: Vec<(String, String)>,
/// GLSL statements run last in `main`, with `gl_Position` in wgpu's clip space.
pub after: String,
/// The alpha test applied to output 0 (`gl_FragData[0]`, `gl_FragColor`), the loader's
/// (3.2 `alphaTest.<program>`): `None` keeps every fragment.
pub alpha_test: Option<AlphaTest>,
/// The program may read `currentRenderedItemId` without declaring it, as Iris 1.11.7
/// lets the hand's (observed with probe packs; terrain's may not).
pub item_id_declared: bool,
/// The depth test sees the far plane (window depth 1) unless the program writes
/// `gl_FragDepth`, while `gl_FragCoord.z` stays the geometry's own: the sky shows only where
/// nothing else drew, and reads its own depth as in Iris 1.11.7.
pub far_depth: bool,
/// `normals` and `specular` are bound as paged maps of the atlas `texture` reads
/// (`horizon-client-render`'s `paged_maps.rs`), not as images of its layout: the program's
/// texture calls on them read through `hz_pagedTexture`.
pub paged_maps: bool,
/// Translucent terrain: a fragment behind the boats' water patches (`horizonWaterMask`, the
/// patches' depth, 1 where there are none) is discarded, so no water shows inside a hull
/// (`RenderTypes.waterMask`), while the frame's depth stays the pack's own.
pub water_mask: bool,
/// The shadow pass's translucent terrain: a fragment inside a boat's water patch
/// (`horizonWaterPatch*`, camera-relative boxes) is discarded, so no water casts its light
/// into a hull. Not with a geometry stage.
pub water_patches: bool,
}
/// The sampler of the boats' water patches' depth (`GbuffersInputs::water_mask`).
pub const WATER_MASK: &str = "horizonWaterMask";
/// How many boats' water patches the shadow pass keeps water out of (`GbuffersInputs::water_patches`).
pub const WATER_PATCHES: usize = 16;
/// A fragment stays when its output 0's alpha compares to `reference` as `op` says (a GLSL
/// comparison operator, `true` for GL's `ALWAYS`, `false` for `NEVER`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AlphaTest {
pub op: &'static str,
pub reference: f32,
}
impl AlphaTest {
/// `alphaTest.<program>`'s value: `off`, or a GL comparison and a reference.
pub fn parse(value: &str) -> Option<Option<Self>> {
let mut words = value.split_whitespace();
let func = words.next()?;
if func == "off" {
return Some(None);
}
let op = match func.trim_start_matches("GL_") {
"NEVER" => "false",
"LESS" => "<",
"EQUAL" => "==",
"LEQUAL" => "<=",
"GREATER" => ">",
"NOTEQUAL" => "!=",
"GEQUAL" => ">=",
"ALWAYS" => return Some(None),
_ => return None,
};
let reference = words.next()?.parse().ok()?;
Some(Some(Self { op, reference }))
}
}
/// How a geometry program's vertex invocations reach their triangle's vertices.
#[derive(Clone, Debug, Default)]
pub struct GeometryInputs {
/// The game's vertex inputs, as `GbuffersInputs`; their `layout(location = n) in`
/// declarations become plain globals the pull sets.
pub inputs: GbuffersInputs,
/// GLSL statements that set the vertex inputs from vertex `hz_vertex_index` (a `uint`).
pub pull: String,
/// The vertex of the `@`th corner of the draw (a `uint` expression, `hz_indices[@]`).
pub index: String,
}
@@ -0,0 +1,38 @@
//! The render stages a pipeline's programs are told they draw in (`renderStage`, and the
//! `MC_RENDER_STAGE_*` macros `horizon-shaders` defines from them).
/// `MC_RENDER_STAGE_*` in the order the stages draw (spec 5.4); each is its index (OQ-7).
pub const RENDER_STAGES: [&str; 24] = [
"NONE",
"SKY",
"SUNSET",
"CUSTOM_SKY",
"SUN",
"MOON",
"STARS",
"VOID",
"TERRAIN_SOLID",
"TERRAIN_CUTOUT_MIPPED",
"TERRAIN_CUTOUT",
"ENTITIES",
"BLOCK_ENTITIES",
"DESTROY",
"OUTLINE",
"DEBUG",
"HAND_SOLID",
"TERRAIN_TRANSLUCENT",
"TRIPWIRE",
"PARTICLES",
"CLOUDS",
"RAIN_SNOW",
"WORLD_BORDER",
"HAND_TRANSLUCENT",
];
/// The value of `MC_RENDER_STAGE_<name>` (OQ-7: its index in `RENDER_STAGES`).
pub fn render_stage(name: &str) -> u32 {
RENDER_STAGES
.iter()
.position(|s| *s == name)
.unwrap_or_else(|| panic!("no render stage {name}")) as u32
}
@@ -0,0 +1,803 @@
//! The stages a WGSL pack can fill, each a WGSL function with a fixed signature
//! that the renderer calls inside its own passes, and the WGSL put around a
//! pack's files to build a stage: the prelude before (the inputs and the
//! resources), the entry points after. `horizon-shader-api` composes a pack's
//! files between them.
use crate::Api;
use crate::inputs::{
DISPLACEMENT_INPUT, Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct,
TONEMAP_INPUT, VERTEX_INPUT, WEATHER_INPUT,
};
/// The bind group of a pack's resources; groups 0 to 2 are the renderer's.
pub const GROUP: u32 = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StageKind {
/// The sky's colour along each view ray, before the sun, moon and stars.
Sky,
/// The colour of block surfaces (terrain), before the alpha test and fog.
Material,
/// Where each corner of a block's surface is drawn (waving plants, rolling water), for
/// the world and the shadow map.
Vertex,
/// The colour of rain and snow streaks.
Weather,
/// A full-screen pass over the world's picture, under the hand and HUD.
Post,
/// 0.2: the picture for an output (the display), once per pixel of it after the post chain,
/// before the camera overlays and the HUD.
Tonemap,
}
/// A resource a stage reads, declared by its prelude.
#[derive(Debug)]
pub struct Resource {
pub binding: u32,
pub name: &'static str,
pub ty: &'static str,
pub doc: &'static str,
}
const FRAME: Resource = Resource {
binding: 0,
name: "frame",
ty: "var<uniform> frame: Frame",
doc: "The frame (`Frame`).",
};
/// The shadow map at `binding`.
const fn shadow_map(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_map",
ty: "var shadow_map: texture_depth_2d",
doc: "This frame's shadow map: the depth of the solid terrain and entities the light sees, 0 at the light, 1 away from it (and where nothing is), stored at `shadow_position`'s mapping. Valid while `frame.has_shadow` is 1 (the pack asks for it with `[shadow]`), else a 1×1 map that lights everything.",
}
}
/// The shadow map's comparison sampler at `binding`.
const fn shadow_sampler(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_sampler",
ty: "var shadow_sampler: sampler_comparison",
doc: "Depth comparison, bilinear (2×2 percentage-closer filtering): `textureSampleCompareLevel(shadow_map, shadow_sampler, p.xy, p.z)` is 1 where the point is lit, 0 in shadow.",
}
}
/// The shadow map with translucent terrain at `binding`.
const fn shadow_map_translucent(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_map_translucent",
ty: "var shadow_map_translucent: texture_depth_2d",
doc: "`shadow_map` with the translucent terrain (water, stained glass, ice) the light passes through drawn in too, read with `shadow_sampler` the same way: a point lit in `shadow_map` but not here lies under such a surface, which `shadow_color` gives the colour of.",
}
}
/// The colour of the translucent terrain in the shadow map at `binding`.
const fn shadow_color(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_color",
ty: "var shadow_color: texture_2d<f32>",
doc: "The texture colour times the vertex colour (rgb: the tint, the face's directional shade and ambient occlusion) of the nearest translucent surface the light passes through at each texel of `shadow_map_translucent`, white where there is none; alpha is 1 on a fluid (water) and the texture's alpha, at most 0.99, on anything else. Read it with `textureLoad` at `shadow_position`'s xy times `textureDimensions(shadow_color)`.",
}
}
/// A function the material and post preludes give packs.
pub const SHADOW_HELPERS: &str = "\
// The shadow map's distortion toward its centre, of a point at shadow clip `xy`.
fn shadow_distort(xy: vec2<f32>) -> vec2<f32> {
let k = frame.shadow_distortion;
return xy / (length(xy) * k + 1.0 - k);
}
// Where the camera-relative point `position` is stored in the shadow map: xy its 0..1 texture
// coordinate (from the top left), z its depth from the light, for `textureSampleCompareLevel`.
fn shadow_position(position: vec3<f32>) -> vec3<f32> {
let clip = frame.shadow_view_proj * vec4<f32>(position, 1.0);
let xy = shadow_distort(clip.xy);
// At most 1, the depth where nothing is: past the map's far end a point stays lit.
return vec3<f32>(xy.x * 0.5 + 0.5, 0.5 - xy.y * 0.5, min(clip.z, 1.0));
}
";
const LINEAR_SAMPLER: Resource = Resource {
binding: 4,
name: "linear_sampler",
ty: "var linear_sampler: sampler",
doc: "Bilinear filtering, clamped to the edges.",
};
const NEAREST_SAMPLER: Resource = Resource {
binding: 5,
name: "nearest_sampler",
ty: "var nearest_sampler: sampler",
doc: "Nearest texel, clamped to the edges.",
};
/// The tonemap stage's resources, at the post stage's bindings for the same names.
const TONEMAP_RESOURCES: [Resource; 4] = [
FRAME,
Resource {
binding: 3,
name: "previous",
ty: "var previous: texture_2d<f32>",
doc: "The post chain's picture at the window's size (without a post chain, the world's at the world's size), linear light: what `input.color` samples, for a curve that reads around the pixel.",
},
LINEAR_SAMPLER,
NEAREST_SAMPLER,
];
/// 0.2's colour boundary, in every 0.2 prelude: vanilla's 2.2 power both ways.
const COLOR_HELPERS: &str = "\
// Negatives and NaN made 0 (a comparison with NaN is false), so no `pow` sees them.
fn horizon_sanitize(c: vec3<f32>) -> vec3<f32> {
return select(vec3<f32>(0.0), c, c > vec3<f32>(0.0));
}
fn horizon_decode(c: vec3<f32>) -> vec3<f32> {
return pow(horizon_sanitize(c), vec3<f32>(2.2));
}
fn horizon_encode(c: vec3<f32>) -> vec3<f32> {
return pow(horizon_sanitize(c), vec3<f32>(1.0 / 2.2));
}
// Alpha is coverage: it stays as it is.
fn horizon_decode_rgba(c: vec4<f32>) -> vec4<f32> {
return vec4<f32>(horizon_decode(c.rgb), c.a);
}
fn horizon_encode_rgba(c: vec4<f32>) -> vec4<f32> {
return vec4<f32>(horizon_encode(c.rgb), c.a);
}
";
const POST_RESOURCES: [Resource; 12] = [
FRAME,
Resource {
binding: 1,
name: "scene_color",
ty: "var scene_color: texture_2d<f32>",
doc: "The world's picture before any pass.",
},
Resource {
binding: 2,
name: "scene_depth",
ty: "var scene_depth: texture_depth_2d",
doc: "The world's depth, reversed (1 near, 0 far and sky).",
},
Resource {
binding: 3,
name: "previous",
ty: "var previous: texture_2d<f32>",
doc: "The previous pass's output (`scene_color` for the first pass).",
},
LINEAR_SAMPLER,
NEAREST_SAMPLER,
shadow_map(6),
shadow_sampler(7),
Resource {
binding: 8,
name: "history",
ty: "var history: texture_2d<f32>",
doc: "Last frame's input to the last pass (what the pass before it wrote), for temporal passes (TAA, denoising): put such a pass second to last and it reads its own previous output; reproject with `frame.previous_view_projection`. Black on the first frame, after a gap of over half a second, and with a single pass.",
},
shadow_map_translucent(9),
shadow_color(10),
Resource {
binding: 11,
name: "scene_surface",
ty: "var scene_surface: texture_2d<u32>",
doc: "What the material stage's `set_surface` stored for the solid terrain at each pixel, unfiltered (`textureLoad`; its texels are the bits of floats, so `bitcast<vec4<f32>>` them): read it through the prelude's `surface_at`, which knows its layout and where it no longer shows. Blank without a material stage that calls `set_surface`.",
},
];
/// The material stage's resources: the frame, and the world as it was before translucent
/// terrain drew (for water and glass to see what lies behind them).
const MATERIAL_RESOURCES: [Resource; 11] = [
FRAME,
Resource {
binding: 1,
name: "reflection",
ty: "var reflection: texture_2d<f32>",
doc: "The water mirror: the world as a camera mirrored about the horizontal water plane around the camera (`frame.water_plane`) sees it, at half resolution, mapped like the world's picture, so water samples it at `input.screen`. Solid terrain, cut away below the plane, over the vanilla sky gradient: not the pack's `sky` stage, nor the sun, moon and stars. Drawn only for a pack whose material reads it, while `frame.has_reflection` is 1; else a 1×1 blank.",
},
Resource {
binding: 2,
name: "reflection_sampler",
ty: "var reflection_sampler: sampler",
doc: "Bilinear filtering, clamped to the edges.",
},
shadow_map(3),
shadow_sampler(4),
shadow_map_translucent(7),
shadow_color(8),
Resource {
binding: 6,
name: "opaque_depth",
ty: "var opaque_depth: texture_depth_2d",
doc: "The world's depth before translucent terrain (reversed, like `scene_depth`), for the translucent pass: `textureLoad(opaque_depth, vec2<i32>(input.screen * frame.resolution), 0)` is what lies behind water or glass. Stale while solid terrain draws.",
},
Resource {
binding: 9,
name: "block_light_colors",
ty: "var block_light_colors: texture_3d<f32>",
doc: "The colour of the block light around the camera, from the `[[lights]]` of the light sources nearby: rgb their colours averaged by how near each is, over 4; a how strongly they reach the cell. Read it through `block_light_color`.",
},
Resource {
binding: 10,
name: "block_light_grid",
ty: "var<uniform> block_light_grid: vec4<f32>",
doc: "Where `block_light_colors` lies: xyz its corner, camera-relative, w the side of one of its cells in blocks.",
},
Resource {
binding: 11,
name: "opaque_color",
ty: "var opaque_color: texture_2d<f32>",
doc: "The world's picture before translucent terrain (sky, solid terrain, entities), the size of `opaque_depth`, for the translucent pass: what a surface refracts. Read it with `textureLoad`. Stale while solid terrain draws; black when the world draws straight to the window (a pack without `[[post]]` passes, at full resolution).",
},
];
/// Functions the post prelude gives packs.
const POST_HELPERS: &str = "\
// The world's depth at `uv` (reversed: 0 is the sky).
fn depth_at(uv: vec2<f32>) -> f32 {
let size = vec2<f32>(textureDimensions(scene_depth));
let texel = vec2<i32>(clamp(uv * size, vec2<f32>(0.0), size - 1.0));
return textureLoad(scene_depth, texel, 0);
}
// The camera-relative world position, in blocks, of the point at `uv` and `depth`.
fn view_position(uv: vec2<f32>, depth: f32) -> vec3<f32> {
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
let view = frame.projection_inverse * clip;
return (frame.view_inverse * vec4<f32>(view.xyz / view.w, 0.0)).xyz;
}
";
/// What the material entries (the checker's below and the renderer's) share: the
/// surface's normal and tangent from the derivatives of its position and uv. The arguments are
/// the line's return statements of `block_light_color`, `block_texture` and
/// `block_texture_level`, the helpers that read colour.
macro_rules! material_helpers {
($light:literal, $texture:literal, $texture_level:literal) => {
concat!(
"\
// The colour of the block light at the camera-relative `position` (rgb, from the `[[lights]]`
// of the sources nearby) and how strongly they reach it (a, 0 where none does). Block light
// keeps its own brightness (`input.light.x`): this is its colour only.
fn block_light_color(position: vec3<f32>) -> vec4<f32> {
let size = vec3<f32>(textureDimensions(block_light_colors));
let uvw = (position - block_light_grid.xyz) / (block_light_grid.w * size);
if any(uvw < vec3<f32>(0.0)) || any(uvw > vec3<f32>(1.0)) {
return vec4<f32>(0.0);
}
let c = textureSampleLevel(block_light_colors, reflection_sampler, uvw, 0.0);
",
$light,
"}
// What the post stage's `surface_at` reads for this point of solid terrain: its `normal` (a
// unit vector in world axes), `smoothness` (0..1, say for reflections) and `data`, a value of
// the pack's own passed through. Unset, the point keeps the face's own normal and 0 for both;
// translucent terrain stores none.
var<private> horizon_surface: vec4<f32>;
fn set_surface(normal: vec3<f32>, smoothness: f32, data: f32) {
// Octahedral, y up: the upper half folds onto xz, the lower half unfolds around it.
let n = normal / (abs(normal.x) + abs(normal.y) + abs(normal.z));
var e = n.xz;
if n.y < 0.0 {
e = (1.0 - abs(n.zx)) * select(vec2<f32>(-1.0), vec2<f32>(1.0), n.xz >= vec2<f32>(0.0));
}
horizon_surface = vec4<f32>(bitcast<f32>(pack2x16snorm(e)), smoothness, data, 1.0);
}
// The point being shaded: its sprite's layer and mip level (the entries set them).
var<private> horizon_block_layer: u32;
var<private> horizon_block_lod: f32;
// The surface's block texture at `uv` (the sprite's space, as `input.uv`), without the tint:
// what `input.albedo` holds at the point itself, readable anywhere on the sprite, for normals
// or detail generated from the texture.
fn block_texture(uv: vec2<f32>) -> vec4<f32> {
",
$texture,
"}
// `block_texture` at mip level `lod` (0 the full-size texture).
fn block_texture_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
",
$texture_level,
"}
// The block texture's size, in texels (16 x 16 for vanilla's).
fn block_texture_size() -> vec2<f32> {
return horizon_block_size();
}
// The mip level `block_texture` reads at this point (0 up close, more far away).
fn block_texture_lod() -> f32 {
return horizon_block_lod;
}
// LabPBR: whether the resource packs ship normal (`_n`) or specular (`_s`) maps beside their
// block textures. Without them `block_normal_map` and `block_specular_map` give the defaults
// below.
fn block_pbr_maps() -> bool {
return horizon_block_pbr_present();
}
// The surface's LabPBR normal map at `uv` (the sprite's space, as `input.uv`): rg the normal's
// x and y in the texture's space (x along `input.tangent`, y down the texture; 0..1 for
// -1..1), b ambient occlusion (1 none), a height (1 the top). (0.5, 0.5, 1, 1) where a texture
// has none.
fn block_normal_map(uv: vec2<f32>) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 0u);
}
// `block_normal_map` at the block texture's mip level `lod` (the same footprint on the surface,
// where the map is smaller than the texture).
fn block_normal_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, lod, 0u);
}
// The surface's LabPBR specular map at `uv`: r perceptual smoothness, g F0 up to 229/255 and
// a metal's id above, b porosity or subsurface scattering, a emission (up to 254/255; 1
// none). (0, 0, 0, 0) where a texture has none.
fn block_specular_map(uv: vec2<f32>) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 1u);
}
// `block_specular_map` at the block texture's mip level `lod`.
fn block_specular_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, lod, 1u);
}
// The LabPBR maps' size, in texels (they may be smaller than the block's texture).
fn block_pbr_size() -> vec2<f32> {
return horizon_block_pbr_size();
}
// Block faces are flat: the derivatives of the position give the face's normal, turned
// toward the camera. Far away they lose precision, so a normal within about 3.6 degrees of
// an axis (every full block face) is that axis exactly: no shimmer on distant faces. The
// gentlest slope of flowing water, one level over a block, tilts about 6 degrees.
fn horizon_material_normal(position: vec3<f32>) -> vec3<f32> {
let n = normalize(cross(dpdx(position), dpdy(position)));
let a = abs(n);
let m = max(a.x, max(a.y, a.z));
let axis = select(select(vec3(0.0, 0.0, sign(n.z)), vec3(0.0, sign(n.y), 0.0), a.y == m),
vec3(sign(n.x), 0.0, 0.0), a.x == m);
let flat_n = select(n, axis, m > 0.998);
return select(flat_n, -flat_n, dot(flat_n, position) > 0.0);
}
// The direction `uv.x` grows along on the surface, and the sign of the one `uv.y` grows
// along against `cross(normal, tangent)`.
fn horizon_material_tangent(position: vec3<f32>, uv: vec2<f32>, normal: vec3<f32>) -> vec4<f32> {
let dp1 = dpdx(position);
let dp2 = dpdy(position);
let duv1 = dpdx(uv);
let duv2 = dpdy(uv);
let dp2perp = cross(dp2, normal);
let dp1perp = cross(normal, dp1);
let t = dp2perp * duv1.x + dp1perp * duv2.x;
let b = dp2perp * duv1.y + dp1perp * duv2.y;
if length(t) < 1e-12 {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
let tangent = normalize(t - normal * dot(normal, t));
return vec4<f32>(tangent, select(-1.0, 1.0, dot(cross(normal, tangent), b) >= 0.0));
}
"
)
};
}
const MATERIAL_HELPERS: &str = material_helpers!(
" return vec4<f32>(c.rgb * 4.0, c.a);\n",
" return horizon_block_sample(uv, horizon_block_layer, horizon_block_lod);\n",
" return horizon_block_sample(uv, horizon_block_layer, lod);\n"
);
/// 0.2's: the `[[lights]]` colours and the block texture decoded (the LabPBR maps are data).
const MATERIAL_HELPERS_0_2: &str = material_helpers!(
" return vec4<f32>(horizon_decode(c.rgb * 4.0), c.a);\n",
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, horizon_block_lod));\n",
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, lod));\n"
);
/// The post prelude's `surface_at`, for the post helpers' end.
const POST_SURFACE: &str = "\
// The solid terrain's surface at a pixel, as the material stage's `set_surface` left it.
struct SurfaceData {
// False where the sky, an entity, water, glass or a particle shows instead.
present: bool,
// A unit vector in world axes.
normal: vec3<f32>,
smoothness: f32,
// The pack's own value, passed through.
data: f32,
}
fn surface_at(texel: vec2<i32>) -> SurfaceData {
let size = vec2<i32>(textureDimensions(scene_surface));
let t = clamp(texel, vec2<i32>(0), size - 1);
let s = bitcast<vec4<f32>>(textureLoad(scene_surface, t, 0));
let depth_size = vec2<i32>(textureDimensions(scene_depth));
let depth = textureLoad(scene_depth, clamp(texel, vec2<i32>(0), depth_size - 1), 0);
// The terrain's own depth is stored with it: anything drawn nearer since hides it.
let present = s.w > 0.0 && abs(s.w - depth) <= s.w * 1e-5;
let e = unpack2x16snorm(bitcast<u32>(s.x));
var n = vec3<f32>(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
if n.y < 0.0 {
n = vec3<f32>((1.0 - abs(n.z)) * sign(n.x), n.y, (1.0 - abs(n.x)) * sign(n.z));
}
return SurfaceData(present, select(vec3<f32>(0.0), normalize(n), present),
select(0.0, s.y, present), select(0.0, s.z, present));
}
";
/// The post stage's entry points, as the renderer builds them: a full-screen
/// triangle, and a fragment that calls the pack's `post` for each pixel.
const POST_ENTRIES: &str = "\
struct HorizonPostOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonPostOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
return post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv)));
}
";
/// What the renderer gives the material prelude's LabPBR functions (`native_material.rs`); the
/// check and the entries below put a stand-in in its place.
macro_rules! pbr_stub {
() => {
"fn horizon_block_sample(uv: vec2<f32>, layer: u32, lod: f32) -> vec4<f32> {
return vec4<f32>(1.0);
}
fn horizon_block_size() -> vec2<f32> {
return vec2<f32>(16.0);
}
fn horizon_block_pbr(uv: vec2<f32>, layer: u32, lod: f32, map: u32) -> vec4<f32> {
return select(vec4<f32>(0.5, 0.5, 1.0, 1.0), vec4<f32>(0.0), map == 1u);
}
fn horizon_block_pbr_present() -> bool {
return false;
}
fn horizon_block_pbr_size() -> vec2<f32> {
return vec2<f32>(16.0);
}
"
};
}
pub const PBR_STUB: &str = pbr_stub!();
/// Entry points with the material stage's inputs: the renderer calls
/// `material` from its own terrain shader in the same way. The arguments are the line's
/// `MaterialInput` and return statements.
macro_rules! material_entries {
($input:literal, $result:literal) => {
concat!(
pbr_stub!(),
"\
struct HorizonMaterialIn {
@builtin(position) position: vec4<f32>,
@location(0) albedo: vec4<f32>,
@location(1) tint: vec4<f32>,
@location(2) light: vec2<f32>,
@location(3) lightmap: vec3<f32>,
@location(4) world: vec3<f32>,
@location(5) uv: vec2<f32>,
@location(6) @interpolate(flat) translucent: u32,
@location(7) @interpolate(flat) tags: u32,
@location(8) @interpolate(flat) face: u32,
@location(9) @interpolate(flat) fluid_quad: u32,
@location(10) @interpolate(flat) emission: f32,
@location(11) @interpolate(flat) material: u32,
}
@fragment fn horizon_fs_material(in: HorizonMaterialIn) -> @location(0) vec4<f32> {
let fluid = select(0u, select(2u, 1u, in.translucent != 0u), in.fluid_quad != 0u);
let normal = horizon_material_normal(in.world);
let tangent = horizon_material_tangent(in.world, in.uv, normal);
",
$input,
$result,
"}
"
)
};
}
const MATERIAL_ENTRIES: &str = material_entries!(
" let input = MaterialInput(in.albedo, in.tint, in.light, in.lightmap, in.world, in.uv, in.translucent != 0u, in.tags, in.material,
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
",
" return material(input).color;\n"
);
/// 0.2's: the colours decoded on the way in, and the result encoded for the frame
/// (`[frame] blend = "gamma"`; under `"linear"` the renderer stores it as it is).
const MATERIAL_ENTRIES_0_2: &str = material_entries!(
" let input = MaterialInput(horizon_decode_rgba(in.albedo), horizon_decode_rgba(in.tint), in.light,
horizon_decode(in.lightmap), in.world, in.uv, in.translucent != 0u, in.tags, in.material,
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
",
" return horizon_encode_rgba(material(input).color);\n"
);
/// Entry points with the sky stage's inputs: the renderer calls `sky` from
/// its own sky shader in the same way.
const SKY_ENTRIES: &str = "\
struct HorizonSkyIn {
@builtin(position) position: vec4<f32>,
@location(0) direction: vec3<f32>,
@location(1) color: vec4<f32>,
}
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
return sky(SkyInput(normalize(in.direction), in.color));
}
";
const SKY_ENTRIES_0_2: &str = "\
struct HorizonSkyIn {
@builtin(position) position: vec4<f32>,
@location(0) direction: vec3<f32>,
@location(1) color: vec4<f32>,
}
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
return horizon_encode_rgba(sky(SkyInput(normalize(in.direction), horizon_decode_rgba(in.color))));
}
";
/// An entry point with the weather stage's inputs: the renderer calls `weather` from its own
/// weather shader in the same way.
const VERTEX_ENTRIES: &str = "\
@vertex fn horizon_vs_vertex(
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
) -> @builtin(position) vec4<f32> {
let input = VertexInput(position, uv, material, light, tint, flags & 7u, flags >> 31u);
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
}
";
/// 0.2's: the tint is a colour, so it is decoded too.
const VERTEX_ENTRIES_0_2: &str = "\
@vertex fn horizon_vs_vertex(
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
) -> @builtin(position) vec4<f32> {
let input = VertexInput(position, uv, material, light, horizon_decode_rgba(tint), flags & 7u, flags >> 31u);
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
}
";
/// `fn displacement(input: DisplacementInput) -> f32`, the material stage's function
/// under `[displacement]`.
pub const DISPLACEMENT_SIGNATURE: &str = "fn displacement(input: DisplacementInput) -> f32";
/// An entry point calling the material stage's `displacement`, after its entries under
/// `[displacement]`: the renderer calls it from its own terrain vertex shader in the same way.
pub const DISPLACEMENT_ENTRIES: &str = "@vertex fn horizon_vs_displacement(
@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>,
@location(3) material: u32, @location(4) height: f32,
) -> @builtin(position) vec4<f32> {
let depth = displacement(DisplacementInput(position, normal, uv, material, height));
return frame.projection * frame.view * vec4<f32>(position - normal * depth, 1.0);
}
";
const WEATHER_ENTRIES: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) light: vec2<f32>,
@location(2) world: vec3<f32>,
@location(3) @interpolate(flat) snow: u32,
}
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
return weather(WeatherInput(in.color, in.light, in.world, in.snow != 0u));
}
";
const WEATHER_ENTRIES_0_2: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) light: vec2<f32>,
@location(2) world: vec3<f32>,
@location(3) @interpolate(flat) snow: u32,
}
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
let input = WeatherInput(horizon_decode_rgba(in.color), in.light, in.world, in.snow != 0u);
return horizon_encode_rgba(weather(input));
}
";
/// The post stage's entry points under 0.2: the chain's targets hold linear light, so nothing
/// converts, and the pass's alpha is ignored.
const POST_ENTRIES_0_2: &str = "\
struct HorizonPostOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonPostOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
return vec4<f32>(post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv))).rgb, 1.0);
}
";
/// The tonemap stage's entry points: a full-screen triangle at the window's size, and a fragment
/// that holds the result to what the display pass takes.
const TONEMAP_ENTRIES: &str = "\
struct HorizonTonemapOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_tonemap(@builtin(vertex_index) i: u32) -> HorizonTonemapOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonTonemapOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_tonemap(in: HorizonTonemapOut) -> @location(0) vec4<f32> {
let color = horizon_sanitize(textureSampleLevel(previous, linear_sampler, in.uv, 0.0).rgb);
let out = tonemap(TonemapInput(in.uv, in.position.xy, color));
return vec4<f32>(min(horizon_sanitize(out), vec3<f32>(frame.headroom)), 1.0);
}
";
impl Api {
/// The stages the line has, in `StageKind::ALL`'s order.
pub fn stages(self) -> &'static [StageKind] {
match self {
Api::V0_1 => &StageKind::ALL[..5],
Api::V0_2 => &StageKind::ALL,
}
}
}
impl StageKind {
pub const ALL: [StageKind; 6] = [
StageKind::Sky,
StageKind::Vertex,
StageKind::Material,
StageKind::Weather,
StageKind::Post,
StageKind::Tonemap,
];
/// The manifest's name for the stage (`[sky]`, `[vertex]`, `[material]`, `[weather]`,
/// `[[post]]`, `[tonemap]`).
pub fn name(self) -> &'static str {
match self {
StageKind::Sky => "sky",
StageKind::Material => "material",
StageKind::Vertex => "vertex",
StageKind::Weather => "weather",
StageKind::Post => "post",
StageKind::Tonemap => "tonemap",
}
}
/// The function a pack defines: it has the stage's name.
pub fn function(self) -> &'static str {
self.name()
}
pub fn input(self) -> &'static Struct {
match self {
StageKind::Sky => &SKY_INPUT,
StageKind::Material => &MATERIAL_INPUT,
StageKind::Vertex => &VERTEX_INPUT,
StageKind::Weather => &WEATHER_INPUT,
StageKind::Post => &POST_INPUT,
StageKind::Tonemap => &TONEMAP_INPUT,
}
}
/// The type the function returns.
pub fn output(self) -> &'static str {
match self {
StageKind::Material => SURFACE.name,
StageKind::Vertex | StageKind::Tonemap => "vec3<f32>",
StageKind::Sky | StageKind::Weather | StageKind::Post => "vec4<f32>",
}
}
/// `fn <name>(input: <Input>) -> <Output>`.
pub fn signature(self) -> String {
format!(
"fn {}(input: {}) -> {}",
self.function(),
self.input().name,
self.output()
)
}
pub fn resources(self) -> &'static [Resource] {
match self {
StageKind::Post => &POST_RESOURCES,
StageKind::Sky | StageKind::Vertex | StageKind::Weather => std::slice::from_ref(&FRAME),
StageKind::Material => &MATERIAL_RESOURCES,
StageKind::Tonemap => &TONEMAP_RESOURCES,
}
}
/// Everything the pack's files may use under the line `api`, declared before them.
pub fn prelude(self, api: Api) -> String {
let mut out = match api {
Api::V0_1 => {
String::from("// Horizon shader API prelude (docs/api/shaders-reference.md)\n")
}
Api::V0_2 => String::from(
"// Horizon shader API 0.2 prelude (docs/api/shaders-reference-0.2.md)\n",
),
};
out += &Frame::line(api).wgsl();
out += &self.input().wgsl();
if self == StageKind::Material {
out += &SURFACE.wgsl();
// The vertex stage's file joins the material's module (`compose`).
out += &VERTEX_INPUT.wgsl();
out += &DISPLACEMENT_INPUT.wgsl();
}
for r in self.resources() {
out += &format!("@group({GROUP}) @binding({}) {};\n", r.binding, r.ty);
}
if api == Api::V0_2 {
out += COLOR_HELPERS;
}
match self {
StageKind::Post => {
out += POST_HELPERS;
out += POST_SURFACE;
out += SHADOW_HELPERS;
}
StageKind::Material => {
out += match api {
Api::V0_1 => MATERIAL_HELPERS,
Api::V0_2 => MATERIAL_HELPERS_0_2,
};
out += SHADOW_HELPERS;
}
StageKind::Vertex => out += SHADOW_HELPERS,
StageKind::Sky | StageKind::Weather | StageKind::Tonemap => {}
}
out
}
/// The entry points put after the pack's files under the line `api`.
pub fn entries(self, api: Api) -> &'static str {
match (api, self) {
(Api::V0_1, StageKind::Sky) => SKY_ENTRIES,
(Api::V0_1, StageKind::Material) => MATERIAL_ENTRIES,
(Api::V0_1, StageKind::Vertex) => VERTEX_ENTRIES,
(Api::V0_1, StageKind::Weather) => WEATHER_ENTRIES,
(Api::V0_1, StageKind::Post) => POST_ENTRIES,
(Api::V0_2, StageKind::Sky) => SKY_ENTRIES_0_2,
(Api::V0_2, StageKind::Material) => MATERIAL_ENTRIES_0_2,
(Api::V0_2, StageKind::Vertex) => VERTEX_ENTRIES_0_2,
(Api::V0_2, StageKind::Weather) => WEATHER_ENTRIES_0_2,
(Api::V0_2, StageKind::Post) => POST_ENTRIES_0_2,
// 0.1 has no tonemap stage: a manifest with one is refused before it is built.
(_, StageKind::Tonemap) => TONEMAP_ENTRIES,
}
}
}
/// The first binding of a pack's `[[textures]]` in group 3; their samplers follow at
/// `TEXTURE_SAMPLER_BINDING`.
pub const TEXTURE_BINDING: u32 = 16;
pub const TEXTURE_SAMPLER_BINDING: u32 = 24;
/// The binding of a pack's first post target (`[[post]] output`) in group 3, the others after.
pub const POST_TARGET_BINDING: u32 = 32;
/// Whether a material stage's `source` (with its prelude) calls `set_surface` itself, and so
/// needs the surface target, which a pack without it doesn't pay for.
pub fn calls_set_surface(source: &str) -> bool {
let call = "set_surface(";
source.matches(call).count() > MATERIAL_HELPERS.matches(call).count()
}
@@ -0,0 +1,71 @@
//! The custom textures a pipeline's programs read: an image of the pack (a PNG or a raw dump,
//! decoded) or a resource location the game resolves, bound to a sampler name in the programs of
//! a stage (`horizon-shaders` reads them from `shaders.properties`, spec 3.4).
use crate::Format;
/// The stage of a `customTexture.<name>`: every program's.
pub const EVERY_STAGE: &str = "*";
/// A texture bound to sampler `name` in the programs of `stage` (`gbuffers`, `deferred`,
/// `composite`...; [`EVERY_STAGE`] for a `customTexture`), or the noise texture (stage and
/// name `noise`).
#[derive(Clone, Debug)]
pub struct CustomTexture {
pub stage: String,
pub name: String,
pub source: TextureSource,
}
#[derive(Clone, Debug)]
pub enum TextureSource {
Image(Texture),
/// A resource location for the game to resolve.
Resource(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dim {
D1,
D2,
D3,
}
#[derive(Clone, Debug)]
pub struct Texture {
pub dim: Dim,
/// Width, height and depth (1 for the dimensions the texture lacks).
pub size: [u32; 3],
/// The internal format the pack asked for.
pub format: Format,
pub texels: Texels,
/// Linear filtering, and clamping instead of repeating: on for a raw texture and off for a
/// PNG, unless the `.mcmeta` beside it says otherwise.
pub blur: bool,
pub clamp: bool,
/// A raw dump rather than a PNG.
pub raw: bool,
}
/// Texel values, `components` per texel: 1, 2 or 4 (three widen to four, alpha 1).
#[derive(Clone, Debug, PartialEq)]
pub enum Texels {
Float(Vec<f32>),
Int(Vec<i32>),
Uint(Vec<u32>),
}
impl Texture {
pub fn components(&self) -> usize {
components(self.format.channels)
}
}
/// The components per texel a texture of `channels` channels stores: three widen to four.
pub fn components(channels: u8) -> usize {
match channels {
1 => 1,
2 => 2,
_ => 4,
}
}
+2 -1
View File
@@ -7,7 +7,8 @@ rust-version.workspace = true
description = "Horizon's own shader packs (WGSL, docs/shaders.md): the format, the stages, the check, and the horizon-shader tool"
[dependencies]
bytemuck = { version = "1.24", features = ["derive"] }
# The stage interface packs are written against (the renderer's, engine).
horizon-render-pipeline = { path = "../horizon-render-pipeline" }
# Parses and validates a pack's WGSL, and translates it as wgpu does for Vulkan, Metal and DirectX 12.
png = "0.18"
naga = { version = "30.0.1", features = ["wgsl-in", "spv-out", "msl-out", "hlsl-out"] }
+3 -380
View File
@@ -1,380 +1,3 @@
//! What a pack's WGSL reads: the frame uniform every stage sees, which the
//! renderer fills and uploads as the Rust struct below, and the stage input
//! and output structs. The WGSL of each is generated from these definitions
//! (`Struct::wgsl`), and so is the reference (docs/api/shaders-reference.md).
/// One field of a WGSL struct, for the prelude and the reference.
#[derive(Debug)]
pub struct Field {
pub name: &'static str,
pub ty: &'static str,
/// Its meaning, unit and range.
pub doc: &'static str,
}
/// A struct a pack's WGSL can name.
#[derive(Debug)]
pub struct Struct {
pub name: &'static str,
pub doc: &'static str,
pub fields: &'static [Field],
}
impl Struct {
pub fn wgsl(&self) -> String {
let mut out = format!("struct {} {{\n", self.name);
for f in self.fields {
out += &format!(" {}: {},\n", f.name, f.ty);
}
out + "}\n"
}
}
/// The WGSL type of a Rust field the renderer uploads.
pub trait Wgsl {
const TYPE: &'static str;
}
macro_rules! wgsl_types {
($($rust:ty => $wgsl:literal),* $(,)?) => {
$(impl Wgsl for $rust {
const TYPE: &'static str = $wgsl;
})*
};
}
wgsl_types! {
f32 => "f32",
u32 => "u32",
i32 => "i32",
[f32; 2] => "vec2<f32>",
[f32; 3] => "vec3<f32>",
[f32; 4] => "vec4<f32>",
[[f32; 4]; 4] => "mat4x4<f32>",
[[f32; 4]; 8] => "array<vec4<f32>, 8>",
}
/// A uniform struct: the Rust type the renderer uploads, and its WGSL. `pad` names a trailing
/// field in Rust only, which fills the struct out to WGSL's size (a multiple of its 16-byte
/// alignment), since `Pod` allows no implicit padding.
macro_rules! uniform {
($(#[doc = $doc:literal])* $name:ident {
$($(#[doc = $fdoc:literal])* $field:ident: $ty:ty,)*
} $(pad $pad:ident: $pad_ty:ty)?) => {
$(#[doc = $doc])*
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
pub struct $name {
$($(#[doc = $fdoc])* pub $field: $ty,)*
$(/// Not in the WGSL: zeros up to its size.
pub $pad: $pad_ty,)?
}
impl $name {
pub const WGSL: Struct = Struct {
name: stringify!($name),
doc: concat!($($doc, "\n"),*),
fields: &[$(Field {
name: stringify!($field),
ty: <$ty as Wgsl>::TYPE,
doc: concat!($($fdoc),*),
}),*],
};
/// Each field's byte offset, in `WGSL.fields`' order.
pub const OFFSETS: &'static [usize] = &[$(std::mem::offset_of!($name, $field)),*];
}
};
}
uniform! {
/// The frame, as `frame` in every stage. Positions and directions are
/// in world axes relative to the camera (x east, y up, z south), so they
/// stay precise far from the world's origin; depth is reversed (1 near,
/// 0 at the far plane and in the sky).
Frame {
/// Camera-relative world to view space: the camera's rotation.
view: [[f32; 4]; 4],
/// View to clip space (reversed depth, wgpu's clip space: z 0..1).
projection: [[f32; 4]; 4],
/// View to camera-relative world space.
view_inverse: [[f32; 4]; 4],
/// Clip to view space.
projection_inverse: [[f32; 4]; 4],
/// The camera's world position, in blocks (about a millimetre of precision 10 000 blocks out).
camera_position: [f32; 3],
/// Seconds since the pack was loaded, wrapping to 0 every hour.
time: f32,
/// Unit vector toward the sun (below the horizon at night).
sun_direction: [f32; 3],
/// The time of day, 0..1: 0 sunrise, 0.25 noon, 0.5 sunset, 0.75 midnight.
day_time: f32,
/// Unit vector toward the moon.
moon_direction: [f32; 3],
/// Vanilla's GameTime: (game time % 24000 + partial tick) / 24000, 0..1.
game_time: f32,
/// Vanilla's sky colour this frame, 0..1.
sky_color: [f32; 3],
/// Rain strength, 0..1.
rain: f32,
/// Vanilla's fog colour this frame (also the clear colour), 0..1.
fog_color: [f32; 3],
/// Thunder strength, 0..1.
thunder: f32,
/// The size of the world's picture, in pixels (the resolution scale applied).
resolution: [f32; 2],
/// Block and sky light at the camera, 0..1 (level / 15).
eye_light: [f32; 2],
/// Where vanilla's fog starts, in blocks from the camera.
fog_start: f32,
/// Where vanilla's fog is full, in blocks.
fog_end: f32,
/// The render distance, in blocks.
render_distance: f32,
/// The far plane, in blocks.
far: f32,
/// Night vision strength, 0..1.
night_vision: f32,
/// Blindness strength, 0..1.
blindness: f32,
/// The darkness effect's strength, 0..1.
darkness: f32,
/// The Brightness option, 0..1 (Moody to Bright).
brightness: f32,
/// What the camera is in: 0 air, 1 water, 2 lava, 3 powder snow.
eye_in: u32,
/// 0 the Overworld, -1 the Nether, 1 the End, 2 any other dimension.
dimension: i32,
/// Frames drawn since the pack was loaded (wraps).
frame_index: u32,
/// 1 while F1 hides the HUD, else 0.
hide_gui: u32,
/// What falls in the biome at the camera while it rains: 0 nothing, 1 rain, 2 snow.
precipitation: u32,
/// The biome at the camera's temperature (vanilla's, -0.7..2.0; it snows below 0.15).
temperature: f32,
/// The biome at the camera's downfall, 0..1.
downfall: f32,
/// `rain`, smoothed: rises over about 30 seconds and dries over about 10, 0..1.
wetness: f32,
/// Last frame's camera-relative world to clip space (its `projection * view`); a point
/// at camera-relative `p` now was at `p + camera_motion` relative to last frame's camera.
/// This frame's on the first frame and after a gap of over half a second (a pause, a
/// world load), when last frame's camera says nothing about this one.
previous_view_projection: [[f32; 4]; 4],
/// How far the camera moved since last frame, in blocks (0 after such a gap).
camera_motion: [f32; 3],
/// Seconds since last frame (0 on the first and after such a gap).
delta_time: f32,
/// `eye_light` eased with a half-life of 2 seconds: it follows the light levels' steps
/// without stepping itself (for exposure and indoor fog).
eye_light_smooth: [f32; 2],
/// 1 while the camera is in water (`eye_in` 1), eased with a half-life of a quarter
/// second in and out.
in_water: f32,
/// The same for lava (`eye_in` 2).
in_lava: f32,
/// The y of the horizontal water plane `reflection` mirrors the world about, in world
/// blocks, while `has_reflection` is 1.
water_plane: f32,
/// 1 while the material stage's `reflection` holds this frame's water mirror, else 0.
has_reflection: u32,
/// The size of `reflection`, in pixels: half the world's picture.
reflection_resolution: [f32; 2],
/// Eight values a native mod hands its own pack each frame (`native_shader_params`,
/// docs/native-mods.md): what they mean is that mod's, such as where and when
/// something happened in the world. All 0 when no mod sets them.
mods: [[f32; 4]; 8],
/// Unit vector toward the body casting this frame's shadows: the sun by day, the moon
/// by night.
light_direction: [f32; 3],
/// 1 while `shadow_map` holds this frame's shadow map, else 0.
has_shadow: u32,
/// Camera-relative world to the shadow camera's clip space, before the distortion
/// (orthographic, looking along `-light_direction`; z 0 at the light, 1 away from it).
shadow_view_proj: [[f32; 4]; 4],
/// Half the side of the square the shadow map covers, in blocks (the manifest's
/// `[shadow] distance`); 0 without a shadow map.
shadow_distance: f32,
/// How strongly the shadow map is distorted toward its centre, 0..1: a point at
/// shadow clip `xy` is stored at `xy / (length(xy) * k + 1 - k)`, so texels near the
/// camera cover less ground (`shadow_position` applies it).
shadow_distortion: f32,
/// The shadow map's side, in texels; 0 without a shadow map.
shadow_resolution: f32,
/// The ground one shadow texel covers at the map's centre, in blocks (for a bias that
/// scales with it); 0 without a shadow map.
shadow_center_texel: f32,
/// 1 while the output is HDR, else 0.
hdr: u32,
/// The peak over paper white: the brightest value the display shows (Apple's EDR
/// headroom, Android's HDR/SDR ratio). 1 in SDR. It changes while the game runs (the
/// system's SDR white, a calibration, another display), so read it every frame.
headroom: f32,
/// Paper white in nits; a nominal 80 in SDR.
paper_white: f32,
/// The peak brightness in nits; a nominal 80 in SDR, as `paper_white`.
peak: f32,
/// The display's black in nits (HDR's Black Level); 0 in SDR.
black_level: f32,
} pad padding: [u32; 3]
}
impl Frame {
/// The fields 0.1's prelude declares: the struct up to `shadow_center_texel`, a prefix of
/// the buffer the renderer uploads.
pub const FIELDS_0_1: usize = 49;
/// `Frame` as the line's prelude declares it.
pub fn line(api: crate::Api) -> Struct {
let fields = match api {
crate::Api::V0_1 => &Self::WGSL.fields[..Self::FIELDS_0_1],
crate::Api::V0_2 => Self::WGSL.fields,
};
Struct {
fields,
..Self::WGSL
}
}
}
macro_rules! fields {
($($name:ident: $ty:literal = $doc:literal,)*) => {
&[$(Field { name: stringify!($name), ty: $ty, doc: $doc }),*]
};
}
/// The post stage's input: one pixel of the picture.
pub const POST_INPUT: Struct = Struct {
name: "PostInput",
doc: "One pixel of the picture, for `fn post(input: PostInput) -> vec4<f32>`.",
fields: fields! {
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of what the pass draws (the world's size; the window's for the last pass).",
color: "vec4<f32>" = "The previous pass's colour here (the world's picture for the first pass).",
depth: "f32" = "The world's depth here, reversed: 0 is the sky.",
},
};
/// The material stage's input: one point of a block's surface.
pub const MATERIAL_INPUT: Struct = Struct {
name: "MaterialInput",
doc: "One point of a block's surface, for `fn material(input: MaterialInput) -> Surface`.",
fields: fields! {
albedo: "vec4<f32>" = "The block texture's colour here (alpha 0..1).",
tint: "vec4<f32>" = "The vertex colour: biome tint, face shade and ambient occlusion.",
light: "vec2<f32>" = "Block and sky light, 0..1, smoothed over the face.",
lightmap: "vec3<f32>" = "Vanilla's light colour for that light this frame.",
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
uv: "vec2<f32>" = "The texture coordinate inside the block atlas.",
translucent: "bool" = "Drawn in the translucent pass (water, stained glass, ice).",
tags: "u32" = "The pack's sprite tags this surface's sprite has: bit i for the manifest's i-th `[[tags]]`.",
material: "u32" = "The `[[materials]]` id of this surface's sprite (0 when no entry names it).",
screen: "vec2<f32>" = "Where the point is on the screen, 0..1 from the top left (`opaque_depth` and `opaque_color` at `screen * frame.resolution`).",
face: "u32" = "For a fluid, which face of the block the point is on, `Direction` order: 0 down, 1 up, 2 north, 3 south, 4 west, 5 east. 0 elsewhere.",
fluid: "u32" = "0 not a fluid, 1 water, 2 lava.",
emission: "f32" = "How strongly the block itself shines, 0..1, by its kind of light: a redstone torch or a candle 7/15, a torch or lava 14/15, glowstone 1; 0 for most blocks.",
normal: "vec3<f32>" = "The surface's geometric normal, a unit vector in world axes, facing the camera. A normal within about 3.6 degrees of an axis is that axis exactly, so distant block faces do not shimmer; flowing water's slopes are steeper and keep theirs.",
tangent: "vec4<f32>" = "The surface's tangent: xyz the unit direction in world axes along which `uv.x` grows, w the sign (1 or -1) that makes `cross(normal, tangent.xyz) * tangent.w` the direction `uv.y` grows along (down the texture). With `normal` it turns a normal map's normals into world axes.",
colored_light: "vec3<f32>" = "The frame's coloured lights at this point (`[[particle_lights]]`, held and dropped items in their `[[lights]]` colour while dynamic lights are on, and mods' visual lights), 0..1, to light the surface with as the pack likes; 0 where none reaches.",
displaced: "f32" = "How much of the relief `[displacement]` gave this point as geometry, 0..1: 1 near the camera, where the surface already lies where `displacement` put it (`position` is that point; a parallax effect would count its depth twice), easing to 0 at `[displacement] distance`; always 0 without `[displacement]`.",
},
};
/// What the material stage returns.
pub const SURFACE: Struct = Struct {
name: "Surface",
doc: "What `material` returns. Vanilla's own is `albedo * tint * vec4(lightmap, 1.0)`; \
Horizon then applies the alpha test (cutout and translucent) and fog.",
fields: fields! {
color: "vec4<f32>" = "The lit colour, 0..1, with its alpha.",
},
};
/// The sky stage's input: one direction of the sky.
pub const SKY_INPUT: Struct = Struct {
name: "SkyInput",
doc: "One direction of the sky, for `fn sky(input: SkyInput) -> vec4<f32>`. The sun, moon, \
stars and clouds are drawn over the result.",
fields: fields! {
direction: "vec3<f32>" = "Unit view ray, camera-relative world axes.",
color: "vec4<f32>" = "Vanilla's sky colour along the ray (sky, fog and sunrise).",
},
};
/// The input of the material stage's `displacement`, under `[displacement]`.
pub const DISPLACEMENT_INPUT: Struct = Struct {
name: "DisplacementInput",
doc: "One corner of the grid a block face near the camera is cut into, for `fn displacement(input: DisplacementInput) -> f32` (`[displacement]`), which returns how deep below the face, in blocks, the surface lies at `height`.",
fields: fields! {
position: "vec3<f32>" = "The corner on the flat face, camera-relative, in blocks.",
normal: "vec3<f32>" = "The face's normal, a unit vector in world axes.",
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite (as `MaterialInput.uv`).",
material: "u32" = "The `[[materials]]` id of the block texture (0 when none matches).",
height: "f32" = "A LabPBR height, 0..1 (1 the top of the relief): the normal map's alpha here, or the texture's average height (what the face's edges are pinned to).",
},
};
/// The vertex stage's input: one corner of a block's surface.
pub const VERTEX_INPUT: Struct = Struct {
name: "VertexInput",
doc: "One corner of a block's surface, for `fn vertex(input: VertexInput) -> vec3<f32>`, which returns where it is drawn (camera-relative, in blocks; `input.position` where it stays).",
fields: fields! {
position: "vec3<f32>" = "The corner, camera-relative, in blocks (`+ frame.camera_position` for the world's).",
uv: "vec2<f32>" = "The texture coordinate inside the block's sprite.",
material: "u32" = "The `[[materials]]` id of the block's sprite (0 when none matches).",
light: "vec2<f32>" = "Block and sky light, 0..1.",
tint: "vec4<f32>" = "The vertex colour (biome tint, face shade, ambient occlusion); alpha 1.",
face: "u32" = "For a fluid, the face of the block the corner is on (as `MaterialInput.face`), else 0.",
fluid: "u32" = "1 on a fluid's surface (water or lava), else 0.",
},
};
/// The weather stage's input: one point of a rain or snow streak.
pub const WEATHER_INPUT: Struct = Struct {
name: "WeatherInput",
doc: "One point of a rain or snow streak, for `fn weather(input: WeatherInput) -> vec4<f32>`, \
which returns the colour blended over the world (alpha its opacity).",
fields: fields! {
color: "vec4<f32>" = "The rain or snow texture's colour here times the streak's vertex colour (alpha 0.1..1).",
light: "vec2<f32>" = "Block and sky light, 0..1.",
position: "vec3<f32>" = "The point, camera-relative, in blocks.",
snow: "bool" = "A snow streak (rain otherwise).",
},
};
/// The tonemap stage's input (0.2): one pixel of the window.
pub const TONEMAP_INPUT: Struct = Struct {
name: "TonemapInput",
doc: "One pixel of an output (the window), for `fn tonemap(input: TonemapInput) -> vec3<f32>`, which returns its colour for the output `frame` describes: linear light, 1.0 at paper white, from 0 to `frame.headroom` (Horizon clamps it there).",
fields: fields! {
uv: "vec2<f32>" = "Where the pixel is, 0..1 from the top left.",
pixel: "vec2<f32>" = "The pixel's centre, in pixels from the top left of the output (the window).",
color: "vec3<f32>" = "The post chain's picture here (the world's without one), linear light, never negative or NaN.",
},
};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_fields_carry_their_docs() {
let wgsl = Frame::WGSL;
assert_eq!(wgsl.fields.len(), 54);
assert!(wgsl.fields.iter().all(|f| !f.doc.trim().is_empty()));
// 0.1's 49 fields, then 0.2's five and the padding to a multiple of 16.
assert_eq!(
std::mem::size_of::<Frame>(),
6 * 64 + 15 * 16 + 8 * 16 + 2 * 16
);
assert_eq!(Frame::OFFSETS[Frame::FIELDS_0_1], 6 * 64 + 15 * 16 + 8 * 16);
assert_eq!(
Frame::line(crate::Api::V0_1).fields.len(),
Frame::FIELDS_0_1
);
assert!(
wgsl.wgsl()
.contains(" camera_position: vec3<f32>,\n time: f32,\n")
);
}
}
//! What a pack's WGSL reads: the renderer's frame uniform and the stages' input and output
//! structs (`horizon_render_pipeline::inputs`).
pub use horizon_render_pipeline::inputs::*;
+1 -31
View File
@@ -9,37 +9,7 @@ pub const SCHEMA: u32 = 1;
/// templates write, and what the game and the registry take.
pub const API: &str = Api::V0_1.as_str();
/// A shader API line: the stages, their signatures and the inputs a pack's WGSL may read.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Api {
/// Vanilla's gamma values.
V0_1,
/// Linear light (docs/design/hdr-linear-light.md). Experimental: checked, not yet drawn.
V0_2,
}
impl Api {
/// Every line this build reads and checks, oldest first.
pub const SERVED: [Api; 2] = [Api::V0_1, Api::V0_2];
/// The manifest's `api` value.
pub const fn as_str(self) -> &'static str {
match self {
Api::V0_1 => "0.1",
Api::V0_2 => "0.2",
}
}
pub fn parse(line: &str) -> Option<Api> {
Self::SERVED.into_iter().find(|a| a.as_str() == line)
}
}
impl std::fmt::Display for Api {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
pub use horizon_render_pipeline::Api;
/// `[frame] blend` (0.2): what the frame stores, and so how see-through draws blend.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
+5 -802
View File
@@ -1,797 +1,10 @@
//! The stages a pack can fill, each a WGSL function with a fixed signature
//! that Horizon calls inside its own passes, and the WGSL put around a
//! pack's files to build a stage: the prelude before (the inputs and the
//! resources), the entry points after.
use crate::inputs::{
DISPLACEMENT_INPUT, Frame, MATERIAL_INPUT, POST_INPUT, SKY_INPUT, SURFACE, Struct,
TONEMAP_INPUT, VERTEX_INPUT, WEATHER_INPUT,
};
use crate::manifest::Api;
//! The stages a pack can fill and the WGSL around its files: the stage interface is the
//! renderer's (`horizon_render_pipeline::stages`, re-exported here); this module puts a pack's
//! files and declarations between its prelude and its entry points.
pub use horizon_render_pipeline::stages::*;
use crate::package::Pack;
/// The bind group of a pack's resources; groups 0 to 2 are the renderer's.
pub const GROUP: u32 = 3;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StageKind {
/// The sky's colour along each view ray, before the sun, moon and stars.
Sky,
/// The colour of block surfaces (terrain), before the alpha test and fog.
Material,
/// Where each corner of a block's surface is drawn (waving plants, rolling water), for
/// the world and the shadow map.
Vertex,
/// The colour of rain and snow streaks.
Weather,
/// A full-screen pass over the world's picture, under the hand and HUD.
Post,
/// 0.2: the picture for an output (the display), once per pixel of it after the post chain,
/// before the camera overlays and the HUD.
Tonemap,
}
/// A resource a stage reads, declared by its prelude.
#[derive(Debug)]
pub struct Resource {
pub binding: u32,
pub name: &'static str,
pub ty: &'static str,
pub doc: &'static str,
}
const FRAME: Resource = Resource {
binding: 0,
name: "frame",
ty: "var<uniform> frame: Frame",
doc: "The frame (`Frame`).",
};
/// The shadow map at `binding`.
const fn shadow_map(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_map",
ty: "var shadow_map: texture_depth_2d",
doc: "This frame's shadow map: the depth of the solid terrain and entities the light sees, 0 at the light, 1 away from it (and where nothing is), stored at `shadow_position`'s mapping. Valid while `frame.has_shadow` is 1 (the pack asks for it with `[shadow]`), else a 1×1 map that lights everything.",
}
}
/// The shadow map's comparison sampler at `binding`.
const fn shadow_sampler(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_sampler",
ty: "var shadow_sampler: sampler_comparison",
doc: "Depth comparison, bilinear (2×2 percentage-closer filtering): `textureSampleCompareLevel(shadow_map, shadow_sampler, p.xy, p.z)` is 1 where the point is lit, 0 in shadow.",
}
}
/// The shadow map with translucent terrain at `binding`.
const fn shadow_map_translucent(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_map_translucent",
ty: "var shadow_map_translucent: texture_depth_2d",
doc: "`shadow_map` with the translucent terrain (water, stained glass, ice) the light passes through drawn in too, read with `shadow_sampler` the same way: a point lit in `shadow_map` but not here lies under such a surface, which `shadow_color` gives the colour of.",
}
}
/// The colour of the translucent terrain in the shadow map at `binding`.
const fn shadow_color(binding: u32) -> Resource {
Resource {
binding,
name: "shadow_color",
ty: "var shadow_color: texture_2d<f32>",
doc: "The texture colour times the vertex colour (rgb: the tint, the face's directional shade and ambient occlusion) of the nearest translucent surface the light passes through at each texel of `shadow_map_translucent`, white where there is none; alpha is 1 on a fluid (water) and the texture's alpha, at most 0.99, on anything else. Read it with `textureLoad` at `shadow_position`'s xy times `textureDimensions(shadow_color)`.",
}
}
/// A function the material and post preludes give packs.
pub const SHADOW_HELPERS: &str = "\
// The shadow map's distortion toward its centre, of a point at shadow clip `xy`.
fn shadow_distort(xy: vec2<f32>) -> vec2<f32> {
let k = frame.shadow_distortion;
return xy / (length(xy) * k + 1.0 - k);
}
// Where the camera-relative point `position` is stored in the shadow map: xy its 0..1 texture
// coordinate (from the top left), z its depth from the light, for `textureSampleCompareLevel`.
fn shadow_position(position: vec3<f32>) -> vec3<f32> {
let clip = frame.shadow_view_proj * vec4<f32>(position, 1.0);
let xy = shadow_distort(clip.xy);
// At most 1, the depth where nothing is: past the map's far end a point stays lit.
return vec3<f32>(xy.x * 0.5 + 0.5, 0.5 - xy.y * 0.5, min(clip.z, 1.0));
}
";
const LINEAR_SAMPLER: Resource = Resource {
binding: 4,
name: "linear_sampler",
ty: "var linear_sampler: sampler",
doc: "Bilinear filtering, clamped to the edges.",
};
const NEAREST_SAMPLER: Resource = Resource {
binding: 5,
name: "nearest_sampler",
ty: "var nearest_sampler: sampler",
doc: "Nearest texel, clamped to the edges.",
};
/// The tonemap stage's resources, at the post stage's bindings for the same names.
const TONEMAP_RESOURCES: [Resource; 4] = [
FRAME,
Resource {
binding: 3,
name: "previous",
ty: "var previous: texture_2d<f32>",
doc: "The post chain's picture at the window's size (without a post chain, the world's at the world's size), linear light: what `input.color` samples, for a curve that reads around the pixel.",
},
LINEAR_SAMPLER,
NEAREST_SAMPLER,
];
/// 0.2's colour boundary, in every 0.2 prelude: vanilla's 2.2 power both ways.
const COLOR_HELPERS: &str = "\
// Negatives and NaN made 0 (a comparison with NaN is false), so no `pow` sees them.
fn horizon_sanitize(c: vec3<f32>) -> vec3<f32> {
return select(vec3<f32>(0.0), c, c > vec3<f32>(0.0));
}
fn horizon_decode(c: vec3<f32>) -> vec3<f32> {
return pow(horizon_sanitize(c), vec3<f32>(2.2));
}
fn horizon_encode(c: vec3<f32>) -> vec3<f32> {
return pow(horizon_sanitize(c), vec3<f32>(1.0 / 2.2));
}
// Alpha is coverage: it stays as it is.
fn horizon_decode_rgba(c: vec4<f32>) -> vec4<f32> {
return vec4<f32>(horizon_decode(c.rgb), c.a);
}
fn horizon_encode_rgba(c: vec4<f32>) -> vec4<f32> {
return vec4<f32>(horizon_encode(c.rgb), c.a);
}
";
const POST_RESOURCES: [Resource; 12] = [
FRAME,
Resource {
binding: 1,
name: "scene_color",
ty: "var scene_color: texture_2d<f32>",
doc: "The world's picture before any pass.",
},
Resource {
binding: 2,
name: "scene_depth",
ty: "var scene_depth: texture_depth_2d",
doc: "The world's depth, reversed (1 near, 0 far and sky).",
},
Resource {
binding: 3,
name: "previous",
ty: "var previous: texture_2d<f32>",
doc: "The previous pass's output (`scene_color` for the first pass).",
},
LINEAR_SAMPLER,
NEAREST_SAMPLER,
shadow_map(6),
shadow_sampler(7),
Resource {
binding: 8,
name: "history",
ty: "var history: texture_2d<f32>",
doc: "Last frame's input to the last pass (what the pass before it wrote), for temporal passes (TAA, denoising): put such a pass second to last and it reads its own previous output; reproject with `frame.previous_view_projection`. Black on the first frame, after a gap of over half a second, and with a single pass.",
},
shadow_map_translucent(9),
shadow_color(10),
Resource {
binding: 11,
name: "scene_surface",
ty: "var scene_surface: texture_2d<u32>",
doc: "What the material stage's `set_surface` stored for the solid terrain at each pixel, unfiltered (`textureLoad`; its texels are the bits of floats, so `bitcast<vec4<f32>>` them): read it through the prelude's `surface_at`, which knows its layout and where it no longer shows. Blank without a material stage that calls `set_surface`.",
},
];
/// The material stage's resources: the frame, and the world as it was before translucent
/// terrain drew (for water and glass to see what lies behind them).
const MATERIAL_RESOURCES: [Resource; 11] = [
FRAME,
Resource {
binding: 1,
name: "reflection",
ty: "var reflection: texture_2d<f32>",
doc: "The water mirror: the world as a camera mirrored about the horizontal water plane around the camera (`frame.water_plane`) sees it, at half resolution, mapped like the world's picture, so water samples it at `input.screen`. Solid terrain, cut away below the plane, over the vanilla sky gradient: not the pack's `sky` stage, nor the sun, moon and stars. Drawn only for a pack whose material reads it, while `frame.has_reflection` is 1; else a 1×1 blank.",
},
Resource {
binding: 2,
name: "reflection_sampler",
ty: "var reflection_sampler: sampler",
doc: "Bilinear filtering, clamped to the edges.",
},
shadow_map(3),
shadow_sampler(4),
shadow_map_translucent(7),
shadow_color(8),
Resource {
binding: 6,
name: "opaque_depth",
ty: "var opaque_depth: texture_depth_2d",
doc: "The world's depth before translucent terrain (reversed, like `scene_depth`), for the translucent pass: `textureLoad(opaque_depth, vec2<i32>(input.screen * frame.resolution), 0)` is what lies behind water or glass. Stale while solid terrain draws.",
},
Resource {
binding: 9,
name: "block_light_colors",
ty: "var block_light_colors: texture_3d<f32>",
doc: "The colour of the block light around the camera, from the `[[lights]]` of the light sources nearby: rgb their colours averaged by how near each is, over 4; a how strongly they reach the cell. Read it through `block_light_color`.",
},
Resource {
binding: 10,
name: "block_light_grid",
ty: "var<uniform> block_light_grid: vec4<f32>",
doc: "Where `block_light_colors` lies: xyz its corner, camera-relative, w the side of one of its cells in blocks.",
},
Resource {
binding: 11,
name: "opaque_color",
ty: "var opaque_color: texture_2d<f32>",
doc: "The world's picture before translucent terrain (sky, solid terrain, entities), the size of `opaque_depth`, for the translucent pass: what a surface refracts. Read it with `textureLoad`. Stale while solid terrain draws; black when the world draws straight to the window (a pack without `[[post]]` passes, at full resolution).",
},
];
/// Functions the post prelude gives packs.
const POST_HELPERS: &str = "\
// The world's depth at `uv` (reversed: 0 is the sky).
fn depth_at(uv: vec2<f32>) -> f32 {
let size = vec2<f32>(textureDimensions(scene_depth));
let texel = vec2<i32>(clamp(uv * size, vec2<f32>(0.0), size - 1.0));
return textureLoad(scene_depth, texel, 0);
}
// The camera-relative world position, in blocks, of the point at `uv` and `depth`.
fn view_position(uv: vec2<f32>, depth: f32) -> vec3<f32> {
let clip = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
let view = frame.projection_inverse * clip;
return (frame.view_inverse * vec4<f32>(view.xyz / view.w, 0.0)).xyz;
}
";
/// What the material entries (the checker's below and the renderer's) share: the
/// surface's normal and tangent from the derivatives of its position and uv. The arguments are
/// the line's return statements of `block_light_color`, `block_texture` and
/// `block_texture_level`, the helpers that read colour.
macro_rules! material_helpers {
($light:literal, $texture:literal, $texture_level:literal) => {
concat!(
"\
// The colour of the block light at the camera-relative `position` (rgb, from the `[[lights]]`
// of the sources nearby) and how strongly they reach it (a, 0 where none does). Block light
// keeps its own brightness (`input.light.x`): this is its colour only.
fn block_light_color(position: vec3<f32>) -> vec4<f32> {
let size = vec3<f32>(textureDimensions(block_light_colors));
let uvw = (position - block_light_grid.xyz) / (block_light_grid.w * size);
if any(uvw < vec3<f32>(0.0)) || any(uvw > vec3<f32>(1.0)) {
return vec4<f32>(0.0);
}
let c = textureSampleLevel(block_light_colors, reflection_sampler, uvw, 0.0);
",
$light,
"}
// What the post stage's `surface_at` reads for this point of solid terrain: its `normal` (a
// unit vector in world axes), `smoothness` (0..1, say for reflections) and `data`, a value of
// the pack's own passed through. Unset, the point keeps the face's own normal and 0 for both;
// translucent terrain stores none.
var<private> horizon_surface: vec4<f32>;
fn set_surface(normal: vec3<f32>, smoothness: f32, data: f32) {
// Octahedral, y up: the upper half folds onto xz, the lower half unfolds around it.
let n = normal / (abs(normal.x) + abs(normal.y) + abs(normal.z));
var e = n.xz;
if n.y < 0.0 {
e = (1.0 - abs(n.zx)) * select(vec2<f32>(-1.0), vec2<f32>(1.0), n.xz >= vec2<f32>(0.0));
}
horizon_surface = vec4<f32>(bitcast<f32>(pack2x16snorm(e)), smoothness, data, 1.0);
}
// The point being shaded: its sprite's layer and mip level (the entries set them).
var<private> horizon_block_layer: u32;
var<private> horizon_block_lod: f32;
// The surface's block texture at `uv` (the sprite's space, as `input.uv`), without the tint:
// what `input.albedo` holds at the point itself, readable anywhere on the sprite, for normals
// or detail generated from the texture.
fn block_texture(uv: vec2<f32>) -> vec4<f32> {
",
$texture,
"}
// `block_texture` at mip level `lod` (0 the full-size texture).
fn block_texture_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
",
$texture_level,
"}
// The block texture's size, in texels (16 x 16 for vanilla's).
fn block_texture_size() -> vec2<f32> {
return horizon_block_size();
}
// The mip level `block_texture` reads at this point (0 up close, more far away).
fn block_texture_lod() -> f32 {
return horizon_block_lod;
}
// LabPBR: whether the resource packs ship normal (`_n`) or specular (`_s`) maps beside their
// block textures. Without them `block_normal_map` and `block_specular_map` give the defaults
// below.
fn block_pbr_maps() -> bool {
return horizon_block_pbr_present();
}
// The surface's LabPBR normal map at `uv` (the sprite's space, as `input.uv`): rg the normal's
// x and y in the texture's space (x along `input.tangent`, y down the texture; 0..1 for
// -1..1), b ambient occlusion (1 none), a height (1 the top). (0.5, 0.5, 1, 1) where a texture
// has none.
fn block_normal_map(uv: vec2<f32>) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 0u);
}
// `block_normal_map` at the block texture's mip level `lod` (the same footprint on the surface,
// where the map is smaller than the texture).
fn block_normal_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, lod, 0u);
}
// The surface's LabPBR specular map at `uv`: r perceptual smoothness, g F0 up to 229/255 and
// a metal's id above, b porosity or subsurface scattering, a emission (up to 254/255; 1
// none). (0, 0, 0, 0) where a texture has none.
fn block_specular_map(uv: vec2<f32>) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, horizon_block_lod, 1u);
}
// `block_specular_map` at the block texture's mip level `lod`.
fn block_specular_map_level(uv: vec2<f32>, lod: f32) -> vec4<f32> {
return horizon_block_pbr(uv, horizon_block_layer, lod, 1u);
}
// The LabPBR maps' size, in texels (they may be smaller than the block's texture).
fn block_pbr_size() -> vec2<f32> {
return horizon_block_pbr_size();
}
// Block faces are flat: the derivatives of the position give the face's normal, turned
// toward the camera. Far away they lose precision, so a normal within about 3.6 degrees of
// an axis (every full block face) is that axis exactly: no shimmer on distant faces. The
// gentlest slope of flowing water, one level over a block, tilts about 6 degrees.
fn horizon_material_normal(position: vec3<f32>) -> vec3<f32> {
let n = normalize(cross(dpdx(position), dpdy(position)));
let a = abs(n);
let m = max(a.x, max(a.y, a.z));
let axis = select(select(vec3(0.0, 0.0, sign(n.z)), vec3(0.0, sign(n.y), 0.0), a.y == m),
vec3(sign(n.x), 0.0, 0.0), a.x == m);
let flat_n = select(n, axis, m > 0.998);
return select(flat_n, -flat_n, dot(flat_n, position) > 0.0);
}
// The direction `uv.x` grows along on the surface, and the sign of the one `uv.y` grows
// along against `cross(normal, tangent)`.
fn horizon_material_tangent(position: vec3<f32>, uv: vec2<f32>, normal: vec3<f32>) -> vec4<f32> {
let dp1 = dpdx(position);
let dp2 = dpdy(position);
let duv1 = dpdx(uv);
let duv2 = dpdy(uv);
let dp2perp = cross(dp2, normal);
let dp1perp = cross(normal, dp1);
let t = dp2perp * duv1.x + dp1perp * duv2.x;
let b = dp2perp * duv1.y + dp1perp * duv2.y;
if length(t) < 1e-12 {
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
let tangent = normalize(t - normal * dot(normal, t));
return vec4<f32>(tangent, select(-1.0, 1.0, dot(cross(normal, tangent), b) >= 0.0));
}
"
)
};
}
const MATERIAL_HELPERS: &str = material_helpers!(
" return vec4<f32>(c.rgb * 4.0, c.a);\n",
" return horizon_block_sample(uv, horizon_block_layer, horizon_block_lod);\n",
" return horizon_block_sample(uv, horizon_block_layer, lod);\n"
);
/// 0.2's: the `[[lights]]` colours and the block texture decoded (the LabPBR maps are data).
const MATERIAL_HELPERS_0_2: &str = material_helpers!(
" return vec4<f32>(horizon_decode(c.rgb * 4.0), c.a);\n",
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, horizon_block_lod));\n",
" return horizon_decode_rgba(horizon_block_sample(uv, horizon_block_layer, lod));\n"
);
/// The post prelude's `surface_at`, for the post helpers' end.
const POST_SURFACE: &str = "\
// The solid terrain's surface at a pixel, as the material stage's `set_surface` left it.
struct SurfaceData {
// False where the sky, an entity, water, glass or a particle shows instead.
present: bool,
// A unit vector in world axes.
normal: vec3<f32>,
smoothness: f32,
// The pack's own value, passed through.
data: f32,
}
fn surface_at(texel: vec2<i32>) -> SurfaceData {
let size = vec2<i32>(textureDimensions(scene_surface));
let t = clamp(texel, vec2<i32>(0), size - 1);
let s = bitcast<vec4<f32>>(textureLoad(scene_surface, t, 0));
let depth_size = vec2<i32>(textureDimensions(scene_depth));
let depth = textureLoad(scene_depth, clamp(texel, vec2<i32>(0), depth_size - 1), 0);
// The terrain's own depth is stored with it: anything drawn nearer since hides it.
let present = s.w > 0.0 && abs(s.w - depth) <= s.w * 1e-5;
let e = unpack2x16snorm(bitcast<u32>(s.x));
var n = vec3<f32>(e.x, 1.0 - abs(e.x) - abs(e.y), e.y);
if n.y < 0.0 {
n = vec3<f32>((1.0 - abs(n.z)) * sign(n.x), n.y, (1.0 - abs(n.x)) * sign(n.z));
}
return SurfaceData(present, select(vec3<f32>(0.0), normalize(n), present),
select(0.0, s.y, present), select(0.0, s.z, present));
}
";
/// The post stage's entry points, as the renderer builds them: a full-screen
/// triangle, and a fragment that calls the pack's `post` for each pixel.
const POST_ENTRIES: &str = "\
struct HorizonPostOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonPostOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
return post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv)));
}
";
/// What the renderer gives the material prelude's LabPBR functions (`native_material.rs`); the
/// check and the entries below put a stand-in in its place.
macro_rules! pbr_stub {
() => {
"fn horizon_block_sample(uv: vec2<f32>, layer: u32, lod: f32) -> vec4<f32> {
return vec4<f32>(1.0);
}
fn horizon_block_size() -> vec2<f32> {
return vec2<f32>(16.0);
}
fn horizon_block_pbr(uv: vec2<f32>, layer: u32, lod: f32, map: u32) -> vec4<f32> {
return select(vec4<f32>(0.5, 0.5, 1.0, 1.0), vec4<f32>(0.0), map == 1u);
}
fn horizon_block_pbr_present() -> bool {
return false;
}
fn horizon_block_pbr_size() -> vec2<f32> {
return vec2<f32>(16.0);
}
"
};
}
pub const PBR_STUB: &str = pbr_stub!();
/// Entry points with the material stage's inputs: the renderer calls
/// `material` from its own terrain shader in the same way. The arguments are the line's
/// `MaterialInput` and return statements.
macro_rules! material_entries {
($input:literal, $result:literal) => {
concat!(
pbr_stub!(),
"\
struct HorizonMaterialIn {
@builtin(position) position: vec4<f32>,
@location(0) albedo: vec4<f32>,
@location(1) tint: vec4<f32>,
@location(2) light: vec2<f32>,
@location(3) lightmap: vec3<f32>,
@location(4) world: vec3<f32>,
@location(5) uv: vec2<f32>,
@location(6) @interpolate(flat) translucent: u32,
@location(7) @interpolate(flat) tags: u32,
@location(8) @interpolate(flat) face: u32,
@location(9) @interpolate(flat) fluid_quad: u32,
@location(10) @interpolate(flat) emission: f32,
@location(11) @interpolate(flat) material: u32,
}
@fragment fn horizon_fs_material(in: HorizonMaterialIn) -> @location(0) vec4<f32> {
let fluid = select(0u, select(2u, 1u, in.translucent != 0u), in.fluid_quad != 0u);
let normal = horizon_material_normal(in.world);
let tangent = horizon_material_tangent(in.world, in.uv, normal);
",
$input,
$result,
"}
"
)
};
}
const MATERIAL_ENTRIES: &str = material_entries!(
" let input = MaterialInput(in.albedo, in.tint, in.light, in.lightmap, in.world, in.uv, in.translucent != 0u, in.tags, in.material,
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
",
" return material(input).color;\n"
);
/// 0.2's: the colours decoded on the way in, and the result encoded for the frame
/// (`[frame] blend = "gamma"`; under `"linear"` the renderer stores it as it is).
const MATERIAL_ENTRIES_0_2: &str = material_entries!(
" let input = MaterialInput(horizon_decode_rgba(in.albedo), horizon_decode_rgba(in.tint), in.light,
horizon_decode(in.lightmap), in.world, in.uv, in.translucent != 0u, in.tags, in.material,
in.position.xy / frame.resolution, in.face, fluid, in.emission, normal, tangent, vec3<f32>(0.0), 0.0);
",
" return horizon_encode_rgba(material(input).color);\n"
);
/// Entry points with the sky stage's inputs: the renderer calls `sky` from
/// its own sky shader in the same way.
const SKY_ENTRIES: &str = "\
struct HorizonSkyIn {
@builtin(position) position: vec4<f32>,
@location(0) direction: vec3<f32>,
@location(1) color: vec4<f32>,
}
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
return sky(SkyInput(normalize(in.direction), in.color));
}
";
const SKY_ENTRIES_0_2: &str = "\
struct HorizonSkyIn {
@builtin(position) position: vec4<f32>,
@location(0) direction: vec3<f32>,
@location(1) color: vec4<f32>,
}
@fragment fn horizon_fs_sky(in: HorizonSkyIn) -> @location(0) vec4<f32> {
return horizon_encode_rgba(sky(SkyInput(normalize(in.direction), horizon_decode_rgba(in.color))));
}
";
/// An entry point with the weather stage's inputs: the renderer calls `weather` from its own
/// weather shader in the same way.
const VERTEX_ENTRIES: &str = "\
@vertex fn horizon_vs_vertex(
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
) -> @builtin(position) vec4<f32> {
let input = VertexInput(position, uv, material, light, tint, flags & 7u, flags >> 31u);
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
}
";
/// 0.2's: the tint is a colour, so it is decoded too.
const VERTEX_ENTRIES_0_2: &str = "\
@vertex fn horizon_vs_vertex(
@location(0) position: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) light: vec2<f32>,
@location(3) tint: vec4<f32>, @location(4) material: u32, @location(5) flags: u32,
) -> @builtin(position) vec4<f32> {
let input = VertexInput(position, uv, material, light, horizon_decode_rgba(tint), flags & 7u, flags >> 31u);
return frame.projection * frame.view * vec4<f32>(vertex(input), 1.0);
}
";
/// `fn displacement(input: DisplacementInput) -> f32`, the material stage's function
/// under `[displacement]`.
pub const DISPLACEMENT_SIGNATURE: &str = "fn displacement(input: DisplacementInput) -> f32";
/// An entry point calling the material stage's `displacement`, after its entries under
/// `[displacement]`: the renderer calls it from its own terrain vertex shader in the same way.
const DISPLACEMENT_ENTRIES: &str = "@vertex fn horizon_vs_displacement(
@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>,
@location(3) material: u32, @location(4) height: f32,
) -> @builtin(position) vec4<f32> {
let depth = displacement(DisplacementInput(position, normal, uv, material, height));
return frame.projection * frame.view * vec4<f32>(position - normal * depth, 1.0);
}
";
const WEATHER_ENTRIES: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) light: vec2<f32>,
@location(2) world: vec3<f32>,
@location(3) @interpolate(flat) snow: u32,
}
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
return weather(WeatherInput(in.color, in.light, in.world, in.snow != 0u));
}
";
const WEATHER_ENTRIES_0_2: &str = "\
struct HorizonWeatherIn {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) light: vec2<f32>,
@location(2) world: vec3<f32>,
@location(3) @interpolate(flat) snow: u32,
}
@fragment fn horizon_fs_weather(in: HorizonWeatherIn) -> @location(0) vec4<f32> {
let input = WeatherInput(horizon_decode_rgba(in.color), in.light, in.world, in.snow != 0u);
return horizon_encode_rgba(weather(input));
}
";
/// The post stage's entry points under 0.2: the chain's targets hold linear light, so nothing
/// converts, and the pass's alpha is ignored.
const POST_ENTRIES_0_2: &str = "\
struct HorizonPostOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_post(@builtin(vertex_index) i: u32) -> HorizonPostOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonPostOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_post(in: HorizonPostOut) -> @location(0) vec4<f32> {
let color = textureSampleLevel(previous, linear_sampler, in.uv, 0.0);
return vec4<f32>(post(PostInput(in.uv, in.position.xy, color, depth_at(in.uv))).rgb, 1.0);
}
";
/// The tonemap stage's entry points: a full-screen triangle at the window's size, and a fragment
/// that holds the result to what the display pass takes.
const TONEMAP_ENTRIES: &str = "\
struct HorizonTonemapOut {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
}
@vertex fn horizon_vs_tonemap(@builtin(vertex_index) i: u32) -> HorizonTonemapOut {
let p = vec2<f32>(f32((i << 1u) & 2u), f32(i & 2u));
var out: HorizonTonemapOut;
out.position = vec4<f32>(p * 2.0 - 1.0, 0.0, 1.0);
out.uv = vec2<f32>(p.x, 1.0 - p.y);
return out;
}
@fragment fn horizon_fs_tonemap(in: HorizonTonemapOut) -> @location(0) vec4<f32> {
let color = horizon_sanitize(textureSampleLevel(previous, linear_sampler, in.uv, 0.0).rgb);
let out = tonemap(TonemapInput(in.uv, in.position.xy, color));
return vec4<f32>(min(horizon_sanitize(out), vec3<f32>(frame.headroom)), 1.0);
}
";
impl Api {
/// The stages the line has, in `StageKind::ALL`'s order.
pub fn stages(self) -> &'static [StageKind] {
match self {
Api::V0_1 => &StageKind::ALL[..5],
Api::V0_2 => &StageKind::ALL,
}
}
}
impl StageKind {
pub const ALL: [StageKind; 6] = [
StageKind::Sky,
StageKind::Vertex,
StageKind::Material,
StageKind::Weather,
StageKind::Post,
StageKind::Tonemap,
];
/// The manifest's name for the stage (`[sky]`, `[vertex]`, `[material]`, `[weather]`,
/// `[[post]]`, `[tonemap]`).
pub fn name(self) -> &'static str {
match self {
StageKind::Sky => "sky",
StageKind::Material => "material",
StageKind::Vertex => "vertex",
StageKind::Weather => "weather",
StageKind::Post => "post",
StageKind::Tonemap => "tonemap",
}
}
/// The function a pack defines: it has the stage's name.
pub fn function(self) -> &'static str {
self.name()
}
pub fn input(self) -> &'static Struct {
match self {
StageKind::Sky => &SKY_INPUT,
StageKind::Material => &MATERIAL_INPUT,
StageKind::Vertex => &VERTEX_INPUT,
StageKind::Weather => &WEATHER_INPUT,
StageKind::Post => &POST_INPUT,
StageKind::Tonemap => &TONEMAP_INPUT,
}
}
/// The type the function returns.
pub fn output(self) -> &'static str {
match self {
StageKind::Material => SURFACE.name,
StageKind::Vertex | StageKind::Tonemap => "vec3<f32>",
StageKind::Sky | StageKind::Weather | StageKind::Post => "vec4<f32>",
}
}
/// `fn <name>(input: <Input>) -> <Output>`.
pub fn signature(self) -> String {
format!(
"fn {}(input: {}) -> {}",
self.function(),
self.input().name,
self.output()
)
}
pub fn resources(self) -> &'static [Resource] {
match self {
StageKind::Post => &POST_RESOURCES,
StageKind::Sky | StageKind::Vertex | StageKind::Weather => std::slice::from_ref(&FRAME),
StageKind::Material => &MATERIAL_RESOURCES,
StageKind::Tonemap => &TONEMAP_RESOURCES,
}
}
/// Everything the pack's files may use under the line `api`, declared before them.
pub fn prelude(self, api: Api) -> String {
let mut out = match api {
Api::V0_1 => {
String::from("// Horizon shader API prelude (docs/api/shaders-reference.md)\n")
}
Api::V0_2 => String::from(
"// Horizon shader API 0.2 prelude (docs/api/shaders-reference-0.2.md)\n",
),
};
out += &Frame::line(api).wgsl();
out += &self.input().wgsl();
if self == StageKind::Material {
out += &SURFACE.wgsl();
// The vertex stage's file joins the material's module (`compose`).
out += &VERTEX_INPUT.wgsl();
out += &DISPLACEMENT_INPUT.wgsl();
}
for r in self.resources() {
out += &format!("@group({GROUP}) @binding({}) {};\n", r.binding, r.ty);
}
if api == Api::V0_2 {
out += COLOR_HELPERS;
}
match self {
StageKind::Post => {
out += POST_HELPERS;
out += POST_SURFACE;
out += SHADOW_HELPERS;
}
StageKind::Material => {
out += match api {
Api::V0_1 => MATERIAL_HELPERS,
Api::V0_2 => MATERIAL_HELPERS_0_2,
};
out += SHADOW_HELPERS;
}
StageKind::Vertex => out += SHADOW_HELPERS,
StageKind::Sky | StageKind::Weather | StageKind::Tonemap => {}
}
out
}
/// The entry points put after the pack's files under the line `api`.
pub fn entries(self, api: Api) -> &'static str {
match (api, self) {
(Api::V0_1, StageKind::Sky) => SKY_ENTRIES,
(Api::V0_1, StageKind::Material) => MATERIAL_ENTRIES,
(Api::V0_1, StageKind::Vertex) => VERTEX_ENTRIES,
(Api::V0_1, StageKind::Weather) => WEATHER_ENTRIES,
(Api::V0_1, StageKind::Post) => POST_ENTRIES,
(Api::V0_2, StageKind::Sky) => SKY_ENTRIES_0_2,
(Api::V0_2, StageKind::Material) => MATERIAL_ENTRIES_0_2,
(Api::V0_2, StageKind::Vertex) => VERTEX_ENTRIES_0_2,
(Api::V0_2, StageKind::Weather) => WEATHER_ENTRIES_0_2,
(Api::V0_2, StageKind::Post) => POST_ENTRIES_0_2,
// 0.1 has no tonemap stage: a manifest with one is refused before it is built.
(_, StageKind::Tonemap) => TONEMAP_ENTRIES,
}
}
}
/// The first binding of a pack's `[[textures]]` in group 3; their samplers follow at
/// `TEXTURE_SAMPLER_BINDING`.
pub const TEXTURE_BINDING: u32 = 16;
pub const TEXTURE_SAMPLER_BINDING: u32 = 24;
/// The declarations of a pack's `[[textures]]`, part of every stage's prelude.
pub fn texture_declarations(textures: &[crate::manifest::PackTexture]) -> String {
let mut out = String::new();
@@ -832,9 +45,6 @@ impl Composed {
}
}
/// The binding of a pack's first post target (`[[post]] output`) in group 3, the others after.
pub const POST_TARGET_BINDING: u32 = 32;
/// The declarations of a pack's post targets, part of the post stage's prelude.
pub fn post_target_declarations(manifest: &crate::manifest::Manifest) -> String {
manifest
@@ -913,10 +123,3 @@ pub fn compose(pack: &Pack, kind: StageKind, file: &str) -> Composed {
}
composed
}
/// Whether a material stage's `source` (with its prelude) calls `set_surface` itself, and so
/// needs the surface target, which a pack without it doesn't pay for.
pub fn calls_set_surface(source: &str) -> bool {
let call = "set_surface(";
source.matches(call).count() > MATERIAL_HELPERS.matches(call).count()
}
+2
View File
@@ -8,6 +8,8 @@ description = "Shader packs (the shaders/ folder format): reading packs, their .
[dependencies]
horizon-core = { path = "../horizon-core" }
# The pipeline description this crate fills (engine).
horizon-render-pipeline = { path = "../horizon-render-pipeline" }
naga = { version = "30.0.1", features = ["glsl-in", "wgsl-in"] }
half = "2"
png = "0.18"
+32 -263
View File
@@ -8,248 +8,31 @@
//! the programs that run is read (OQ-17), and the shadow programs' when the shadow pass doesn't
//! run; a later file overrides an earlier one, with a note when the values differ. A colortex
//! format counts only in a fragment stage, and in a block comment only where `const` starts its
//! line, as Iris 1.11.7 read our probes ([`Config::read_vertex`], [`Config::read`]).
//! line, as Iris 1.11.7 read our probes ([`ReadConfig::read_vertex`], [`ReadConfig::read`]).
use crate::Preprocessed;
pub use horizon_render_pipeline::config::{
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, SHADOW_COLORS, Shadow, buffer_index,
shadow_color,
};
/// colortex buffers (6.1.1).
pub const BUFFERS: usize = 16;
/// The shadowcolor buffers there are: two, and `shadowcolor2` to `shadowcolor7` for a pack that
/// names the `HIGHER_SHADOWCOLOR` feature.
pub const SHADOW_COLORS: usize = 8;
/// A colour format of 6.7, as channels, bits per channel and kind. Packed formats are described
/// by their widest channel: `RGB10_A2` is 4 × 10 bits, `R11F_G11F_B10F` 3 × 11 bits float.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Format {
pub channels: u8,
pub bits: u8,
pub kind: Kind,
/// Reading a [`Config`] from a pack's stage files.
pub trait ReadConfig {
fn read(&mut self, pp: &Preprocessed);
fn read_formats(&mut self, pp: &Preprocessed);
fn read_formats_and_shadow(&mut self, pp: &Preprocessed);
fn read_vertex(&mut self, pp: &Preprocessed);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Unorm,
Snorm,
Float,
Int,
Uint,
}
impl Format {
/// The default `RGBA` (6.1.3).
pub const RGBA8: Format = Format {
channels: 4,
bits: 8,
kind: Kind::Unorm,
};
/// A format name of 6.7, in any case.
pub fn parse(name: &str) -> Option<Format> {
let name = name.trim().to_ascii_uppercase();
let packed = |channels, bits, kind| {
Some(Format {
channels,
bits,
kind,
})
};
match name.as_str() {
"RGBA" => return Some(Self::RGBA8),
"R3_G3_B2" => return packed(3, 3, Kind::Unorm),
"RGB5_A1" => return packed(4, 5, Kind::Unorm),
"RGB10_A2" => return packed(4, 10, Kind::Unorm),
"RGB10_A2UI" => return packed(4, 10, Kind::Uint),
"R11F_G11F_B10F" => return packed(3, 11, Kind::Float),
"RGB9_E5" => return packed(3, 9, Kind::Float),
"RGBA4" => return packed(4, 4, Kind::Unorm),
"RGB565" => return packed(3, 6, Kind::Unorm),
"RGBA2" => return packed(4, 2, Kind::Unorm),
_ => {}
}
let letters = name.bytes().take_while(u8::is_ascii_alphabetic).count();
let channels = match &name[..letters] {
"R" => 1,
"RG" => 2,
"RGB" => 3,
"RGBA" => 4,
_ => return None,
};
let rest = &name[letters..];
let digits = rest.bytes().take_while(u8::is_ascii_digit).count();
let bits: u8 = rest[..digits].parse().ok()?;
let kind = match &rest[digits..] {
"" => Kind::Unorm,
"_SNORM" => Kind::Snorm,
"F" => Kind::Float,
"I" => Kind::Int,
"UI" => Kind::Uint,
_ => return None,
};
let valid = match kind {
Kind::Unorm | Kind::Snorm => matches!(bits, 8 | 16),
Kind::Float => matches!(bits, 16 | 32),
Kind::Int | Kind::Uint => matches!(bits, 8 | 16 | 32),
};
valid.then_some(Format {
channels,
bits,
kind,
})
}
}
/// The settings of one colortex buffer.
#[derive(Clone, Debug, PartialEq)]
pub struct Buffer {
pub format: Format,
/// Cleared every frame (`<buffer>Clear`, default true).
pub clear: bool,
/// `<buffer>ClearColor`, else 6.1.5's default: colortex0 the fog colour, colortex1 white,
/// the others transparent black.
pub clear_color: Option<[f32; 4]>,
/// `<buffer>MipmapEnabled`.
pub mipmap: bool,
}
impl Default for Buffer {
fn default() -> Self {
Self {
format: Format::RGBA8,
clear: true,
clear_color: None,
mipmap: false,
}
}
}
/// The shadow map's settings (6.3, 7.1), from `const` lines of the stage files.
#[derive(Clone, Debug, PartialEq)]
pub struct Shadow {
/// `shadowMapResolution`: the map's side in pixels.
pub resolution: u32,
/// `shadowDistance`: half the side of the area the map covers, in blocks.
pub distance: f32,
/// `shadowDistanceRenderMul`: when > 0, geometry beyond `distance × mul` is not drawn.
pub render_mul: f32,
/// `voxelDistance`: with `shadow.culling=reversed`, nothing within it is culled.
pub voxel_distance: f32,
/// `shadowIntervalSize`: the shadow camera moves in steps of this many blocks (0: none).
pub interval: f32,
/// Native packs: the snap's remainder is euclidean, so the centre is within half an interval
/// of the player at negative coordinates too. `shaders/` packs keep Iris's.
pub euclidean_snap: bool,
/// `sunPathRotation`, degrees.
pub sun_path_rotation: f32,
/// `shadowMapFov`: a perspective projection of this many degrees instead of orthographic.
pub fov: Option<f32>,
/// `shadowNearPlane`, `shadowFarPlane`: the orthographic projection's planes, as JOML's
/// `zNear`/`zFar`. None when the pack doesn't set one: Iris 1.11.7 then uses -100.05 and
/// 156 (measured with a probe pack), each on its own.
pub near: Option<f32>,
pub far: Option<f32>,
/// `shadowHardwareFiltering(0|1)`: shadowtex0/1 sampled with depth comparison.
pub hardware_filtering: [bool; 2],
/// `shadowtex0Nearest` (or `shadowtexNearest`, `shadow0MinMagNearest`) and
/// `shadowtex1Nearest` (or `shadow1MinMagNearest`): shadowtex0/1 read through a plain sampler
/// take the nearest texel. Otherwise Iris 1.11.7 filters them linearly (measured).
pub nearest: [bool; 2],
/// `shadowcolor0` to `shadowcolor7`: format, cleared every frame, clear colour (default
/// white).
pub colors: [Buffer; SHADOW_COLORS],
}
impl Default for Shadow {
fn default() -> Self {
let white = Buffer {
clear_color: Some([1.0; 4]),
..Buffer::default()
};
Self {
resolution: 1024,
distance: 160.0,
render_mul: -1.0,
voxel_distance: 160.0,
interval: 2.0,
euclidean_snap: false,
sun_path_rotation: 0.0,
fov: None,
near: None,
far: None,
hardware_filtering: [false; 2],
nearest: [false; 2],
colors: std::array::from_fn(|_| white.clone()),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Config {
pub buffers: [Buffer; BUFFERS],
pub shadow: Shadow,
/// `noiseTextureResolution`: the side of the `noisetex` Horizon makes when the pack gives none.
pub noise_resolution: Option<u32>,
/// The smoothed uniforms' half-lives in ticks, in [`HALF_LIVES`] order, when the pack sets
/// them (`const float wetnessHalflife = 30.0;`...).
pub half_lives: [Option<f32>; 4],
/// Conflicting or unreadable settings, as `file:line: message`.
pub notes: Vec<String>,
/// Each setting read so far and its value, to report a later file changing it.
seen: Vec<(String, String)>,
}
impl Default for Config {
fn default() -> Self {
Self {
buffers: std::array::from_fn(|_| Buffer::default()),
shadow: Shadow::default(),
noise_resolution: None,
half_lives: [None; 4],
notes: Vec::new(),
seen: Vec::new(),
}
}
}
/// The shadowcolor buffer a sampler or image name reads: `shadowcolor` and `shadowcolor0` to
/// `shadowcolor7`.
pub fn shadow_color(name: &str) -> Option<usize> {
match name.strip_prefix("shadowcolor")?.as_bytes() {
[] => Some(0),
&[d @ b'0'..=b'7'] => Some(usize::from(d - b'0')),
_ => None,
}
}
/// The colortex index of a buffer name or alias (6.1.1).
pub fn buffer_index(name: &str) -> Option<usize> {
let alias = [
"gcolor",
"gdepth",
"gnormal",
"composite",
"gaux1",
"gaux2",
"gaux3",
"gaux4",
];
if let Some(i) = alias.iter().position(|a| *a == name) {
return Some(i);
}
let n: usize = name.strip_prefix("colortex")?.parse().ok()?;
(n < BUFFERS).then_some(n)
}
impl Config {
impl ReadConfig for Config {
/// Reads one preprocessed fragment stage file.
pub fn read(&mut self, pp: &Preprocessed) {
fn read(&mut self, pp: &Preprocessed) {
self.read_stage(pp, true, true, true);
}
/// Reads only the colortex formats of one preprocessed fragment stage file, as [`Self::read`]
/// reads them.
pub fn read_formats(&mut self, pp: &Preprocessed) {
fn read_formats(&mut self, pp: &Preprocessed) {
self.read_stage(pp, true, false, false);
}
@@ -257,7 +40,7 @@ impl Config {
/// file: `shadow.fsh` of a pack without a shadow pass. Iris 1.11.7 applied `shadowDistance`
/// and `sunPathRotation` declared only there (its shadow projection and sun position), with a
/// probe pack of ours whose shadow buffers nothing read (#10490).
pub fn read_formats_and_shadow(&mut self, pp: &Preprocessed) {
fn read_formats_and_shadow(&mut self, pp: &Preprocessed) {
self.read_stage(pp, true, true, false);
}
@@ -266,10 +49,18 @@ impl Config {
/// `gbuffers_terrain.vsh` or `shadow.vsh` of probe packs p9063a, p9063c and p9063s (ours),
/// while it honoured the same line in each program's fragment shader. Nor do the shadow
/// settings of [`VERTEX_IGNORED`].
pub fn read_vertex(&mut self, pp: &Preprocessed) {
fn read_vertex(&mut self, pp: &Preprocessed) {
self.read_stage(pp, false, false, true);
}
}
/// The settings of one stage file, into a [`Config`].
trait ReadStage {
fn read_stage(&mut self, pp: &Preprocessed, formats: bool, shadow: bool, others: bool);
fn constant(&mut self, place: &str, ty: &str, name: &str, value: &str, reads: [bool; 3]);
}
impl ReadStage for Config {
/// `formats`: whether colortex formats count; `shadow`: whether the shadow settings do (those
/// of [`VERTEX_IGNORED`] only with it); `others`: whether every other setting does.
fn read_stage(&mut self, pp: &Preprocessed, formats: bool, shadow: bool, others: bool) {
@@ -365,7 +156,7 @@ impl Config {
}
// The same setting under another name (an alias) counts as the same.
let key = format!("{buffer}{setting}");
match self.seen.iter_mut().find(|(k, _)| *k == key) {
match self.settings.iter_mut().find(|(k, _)| *k == key) {
Some((_, old)) if old != value => {
self.notes.push(format!(
"{place}: `{name}` changed by a later file to `{value}`"
@@ -373,20 +164,11 @@ impl Config {
*old = value.to_owned();
}
Some(_) => {}
None => self.seen.push((key, value.to_owned())),
None => self.settings.push((key, value.to_owned())),
}
}
}
/// The `const float` half-lives (ShaderDoc, OptiFine's shaders.txt) of `wetness` rising and
/// falling, `eyeBrightnessSmooth` and `centerDepthSmooth`, in ticks.
pub const HALF_LIVES: [&str; 4] = [
"wetnessHalflife",
"drynessHalflife",
"eyeBrightnessHalflife",
"centerDepthHalflife",
];
/// Shadow settings a vertex stage's `const` doesn't set: Iris 1.11.7 ignored each declared only in
/// `final.vsh`, `gbuffers_textured.vsh` or `shadow.vsh` of probe packs of ours (#10490), with
/// and without a shadow pass. The other shadow settings were not probed there.
@@ -398,7 +180,12 @@ const VERTEX_IGNORED: [&str; 5] = [
"shadowFarPlane",
];
impl Config {
/// The shadow settings of 6.3 and 7.1, into a [`Config`].
trait ShadowSetting {
fn shadow_constant(&mut self, ty: &str, name: &str, value: &str) -> bool;
}
impl ShadowSetting for Config {
/// A shadow setting of 6.3 or 7.1; true when `name` is one (read or not).
fn shadow_constant(&mut self, ty: &str, name: &str, value: &str) -> bool {
let s = &mut self.shadow;
@@ -580,24 +367,6 @@ mod tests {
preprocess(&pack, "composite.fsh", &Macros::standard())
}
#[test]
fn formats_parse() {
let f = |channels, bits, kind| {
Some(Format {
channels,
bits,
kind,
})
};
assert_eq!(Format::parse("rgba16f"), f(4, 16, Kind::Float));
assert_eq!(Format::parse("R32UI"), f(1, 32, Kind::Uint));
assert_eq!(Format::parse("RG8_SNORM"), f(2, 8, Kind::Snorm));
assert_eq!(Format::parse("R11F_G11F_B10F"), f(3, 11, Kind::Float));
assert_eq!(Format::parse("RGB16"), f(3, 16, Kind::Unorm));
assert_eq!(Format::parse("RGBA12"), None);
assert_eq!(Format::parse("R16_SNORMX"), None);
}
#[test]
fn draw_buffers_and_constants_come_from_live_code() {
let pp = stage(
+37 -17
View File
@@ -24,6 +24,8 @@
use std::collections::{HashMap, HashSet};
use horizon_render_pipeline::custom::CustomUniforms;
use crate::Properties;
/// The seconds one unit of a `smooth` fade time lasts. Fitted to Iris 1.11.7 with a probe pack of
@@ -36,25 +38,17 @@ use crate::Properties;
/// unit (`pack_uniforms.rs` `SECONDS_A_TICK`).
const FADE_UNIT: f64 = 0.1;
/// A value: up to 16 components (matrices only as names, for `m.column.row`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Val {
pub v: [f64; 16],
/// Components: 1 for a scalar.
pub n: usize,
pub use horizon_render_pipeline::custom::Val;
/// The evaluator's arithmetic on [`Val`].
trait ValOps {
fn x(&self) -> f64;
fn truth(&self) -> bool;
fn zip(self, o: Val, f: impl Fn(f64, f64) -> f64) -> Val;
fn map(self, f: impl Fn(f64) -> f64) -> Val;
}
impl Val {
pub fn scalar(x: f64) -> Self {
let mut v = [0.0; 16];
v[0] = x;
Self { v, n: 1 }
}
pub fn vector(c: &[f64]) -> Self {
let mut v = [0.0; 16];
v[..c.len()].copy_from_slice(c);
Self { v, n: c.len() }
}
impl ValOps for Val {
fn x(&self) -> f64 {
self.v[0]
}
@@ -588,6 +582,32 @@ impl Custom {
}
}
impl CustomUniforms for Custom {
fn names(&self) -> Vec<(String, String)> {
(self.uniforms())
.map(|(name, ty)| (name.to_owned(), ty.to_owned()))
.collect()
}
fn eval(&mut self, dt: f64, builtin: &dyn Fn(&str) -> Option<Val>) -> Vec<Val> {
Custom::eval(self, dt, builtin)
}
fn restart_smoothing(&mut self) {
Custom::restart_smoothing(self);
}
fn world_frame(&mut self, biome_known: bool) {
Custom::world_frame(self, biome_known);
}
fn forget_world(&mut self) {
Custom::forget_world(self);
}
fn restart_at(&mut self, tick: u64) {
Custom::restart_at(self, tick);
}
fn take_notes(&mut self) -> Vec<String> {
std::mem::take(&mut self.notes)
}
}
struct Parser<'a> {
tokens: Vec<String>,
at: usize,
+2 -2
View File
@@ -40,8 +40,8 @@ pub use biomes::{
pub use block_ids::BlockIds;
pub use condition::eval_condition;
pub use config::{
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, SHADOW_COLORS, Shadow, buffer_index,
draw_buffers, shadow_color,
BUFFERS, Buffer, Config, Format, HALF_LIVES, Kind, ReadConfig, SHADOW_COLORS, Shadow,
buffer_index, draw_buffers, shadow_color,
};
pub use core_shader::{BLOCKS, CoreProgram, Glue, SAMPLERS, translate_core};
pub use custom::{Custom, Val};
+2 -36
View File
@@ -3,6 +3,8 @@
use std::collections::BTreeMap;
pub use horizon_render_pipeline::{RENDER_STAGES, render_stage};
/// The feature flags of `iris.features.*` (spec 3.2) Horizon has. A pack that requires one of
/// them loads; one it lacks is noted. Horizon defines no `IRIS_FEATURE_<name>` macro for them
/// (spec OQ-2, docs/cleanroom/README.md): that macro family names another loader, so packs take
@@ -59,34 +61,6 @@ struct Macro {
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Macros(BTreeMap<String, Macro>);
/// `MC_RENDER_STAGE_*` in the order the stages draw (spec 5.4); each is its index (OQ-7).
pub const RENDER_STAGES: [&str; 24] = [
"NONE",
"SKY",
"SUNSET",
"CUSTOM_SKY",
"SUN",
"MOON",
"STARS",
"VOID",
"TERRAIN_SOLID",
"TERRAIN_CUTOUT_MIPPED",
"TERRAIN_CUTOUT",
"ENTITIES",
"BLOCK_ENTITIES",
"DESTROY",
"OUTLINE",
"DEBUG",
"HAND_SOLID",
"TERRAIN_TRANSLUCENT",
"TRIPWIRE",
"PARTICLES",
"CLOUDS",
"RAIN_SNOW",
"WORLD_BORDER",
"HAND_TRANSLUCENT",
];
/// `DH_BLOCK_*`, each its index: Distant Horizons' block materials.
pub const DH_BLOCK_MATERIALS: [&str; 16] = [
"UNKNOWN",
@@ -107,14 +81,6 @@ pub const DH_BLOCK_MATERIALS: [&str; 16] = [
"ILLUMINATED",
];
/// The value of `MC_RENDER_STAGE_<name>` (OQ-7: its index in `RENDER_STAGES`).
pub fn render_stage(name: &str) -> u32 {
RENDER_STAGES
.iter()
.position(|s| *s == name)
.unwrap_or_else(|| panic!("no render stage {name}")) as u32
}
impl Macros {
/// The standard macros that need no renderer (spec 1.5.1): `MC_VERSION`, this system's
/// `MC_OS_*`, the GL and GLSL versions Iris reports, the render stages and the host
+1 -1
View File
@@ -12,7 +12,7 @@
use std::collections::BTreeSet;
use crate::config::{self, Config};
use crate::config::{self, Config, ReadConfig};
use crate::custom::Custom;
use crate::{
ComputeProgram, eval_condition, preprocess, translate_composite, translate_compute,
+3 -65
View File
@@ -4,71 +4,9 @@
//! location the game resolves (`minecraft:textures/...`, `minecraft:dynamic/lightmap_1`).
use crate::{Format, Kind, Pack, Properties};
/// The stage of a `customTexture.<name>`: every program's.
pub const EVERY_STAGE: &str = "*";
/// A texture bound to sampler `name` in the programs of `stage` (`gbuffers`, `deferred`,
/// `composite`...; [`EVERY_STAGE`] for a `customTexture`), or the noise texture (stage and
/// name `noise`).
#[derive(Clone, Debug)]
pub struct CustomTexture {
pub stage: String,
pub name: String,
pub source: TextureSource,
}
#[derive(Clone, Debug)]
pub enum TextureSource {
Image(Texture),
/// A resource location for the game to resolve.
Resource(String),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dim {
D1,
D2,
D3,
}
#[derive(Clone, Debug)]
pub struct Texture {
pub dim: Dim,
/// Width, height and depth (1 for the dimensions the texture lacks).
pub size: [u32; 3],
/// The internal format the pack asked for.
pub format: Format,
pub texels: Texels,
/// Linear filtering, and clamping instead of repeating: on for a raw texture and off for a
/// PNG, unless the `.mcmeta` beside it says otherwise.
pub blur: bool,
pub clamp: bool,
/// A raw dump rather than a PNG.
pub raw: bool,
}
/// Texel values, `components` per texel: 1, 2 or 4 (three widen to four, alpha 1).
#[derive(Clone, Debug, PartialEq)]
pub enum Texels {
Float(Vec<f32>),
Int(Vec<i32>),
Uint(Vec<u32>),
}
impl Texture {
pub fn components(&self) -> usize {
components(self.format.channels)
}
}
fn components(channels: u8) -> usize {
match channels {
1 => 1,
2 => 2,
_ => 4,
}
}
pub use horizon_render_pipeline::textures::{
CustomTexture, Dim, EVERY_STAGE, Texels, Texture, TextureSource, components,
};
/// Every `texture.*` and `customTexture.*` entry, read and decoded, and a note for each that
/// failed.
@@ -13,21 +13,11 @@ use std::collections::BTreeSet;
use std::fmt::Write;
use super::scan::{QUALIFIERS, STORAGE, significant};
use super::{Error, GbuffersInputs, Program, Target};
use super::{Error, Program, Target};
use crate::Preprocessed;
use crate::macros::lex;
/// How a geometry program's vertex invocations reach their triangle's vertices.
#[derive(Clone, Debug, Default)]
pub struct GeometryInputs {
/// The game's vertex inputs, as `GbuffersInputs`; their `layout(location = n) in`
/// declarations become plain globals the pull sets.
pub inputs: GbuffersInputs,
/// GLSL statements that set the vertex inputs from vertex `hz_vertex_index` (a `uint`).
pub pull: String,
/// The vertex of the `@`th corner of the draw (a `uint` expression, `hz_indices[@]`).
pub index: String,
}
pub use horizon_render_pipeline::program::GeometryInputs;
/// Translates `vertex`, `geometry` and `fragment` (a shadow program): the program, and how many
/// triangles the geometry stage can emit (the instances to draw).
@@ -601,6 +591,7 @@ fn strip_storage(text: &mut [String], tokens: &[&str], range: std::ops::Range<us
#[cfg(test)]
mod tests {
use super::*;
use crate::GbuffersInputs;
use crate::pack::tests::zip;
use crate::{Macros, Pack, preprocess};
+4 -182
View File
@@ -44,188 +44,10 @@ pub use geometry::{GeometryInputs, translate_geometry};
use scan::Stage;
/// What a program's stages need from the game.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Interface {
/// The uniform block at binding 0: its size and each value's name, GLSL type and offset.
/// Ours come first: `hz_target` (the target's width, height, 1/width, 1/height), and the
/// fixed-function fog the compatibility `gl_Fog` reads (OQ-28): `hz_fog_color` (vanilla's fog
/// colour) and `hz_fog` (density, start, end, 1 / (end - start)), then `hz_depth_linear`:
/// 1 where `shadowtex0`, `shadowtex1`, `shadow` and `watershadow` (x to w) read through a
/// plain sampler filter linearly, 0 where they take the nearest texel.
pub uniform_size: u32,
pub uniforms: Vec<Uniform>,
/// std140 initial contents; runtime values overwrite their members each frame.
pub initial_values: Vec<u8>,
/// Each sampler: texture at `binding`, sampler object at `binding + 1`.
pub samplers: Vec<Sampler>,
/// The fragment stage's output locations.
pub outputs: Vec<u32>,
/// A gbuffers program reads `DRAW_UNIFORMS`: the block at set 2, binding 0, set per draw.
pub draw: bool,
/// Storage images (`colorimg4`...), after the samplers.
pub images: Vec<Image>,
/// The SSBOs' bindings in set 4 (`bufferObject.<n>`).
pub buffers: Vec<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Image {
pub name: String,
/// `image2D`, `uimage3D`...
pub ty: String,
/// The format qualifier (`rgba16f`).
pub format: String,
/// `readonly`, `writeonly`, empty for both, or `atomic` for an image an atomic function
/// uses.
pub access: String,
pub binding: u32,
/// A compute program's copy of image `before` as it was before the dispatch, which its
/// loads read (see `ordering`).
pub before: Option<String>,
}
/// A compute program (`deferred4_a.csh`): its module, what it reads and writes, and how many
/// work groups it runs: `workGroups`, or `workGroupsRender` times the target's size over the
/// local size (1, 1 when neither is set).
#[derive(Clone)]
pub struct ComputeProgram {
pub module: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
pub work_groups: Option<[u32; 3]>,
pub work_groups_render: [f32; 2],
}
/// The uniforms a gbuffers program reads per draw (5.3's "D"), in a block of their own
/// (`HzDraw`, std140: four ints, a vec4; 32 bytes) the game sets for each draw.
pub const DRAW_UNIFORMS: [(&str, &str); 5] = [
("renderStage", "int"),
("entityId", "int"),
("blockEntityId", "int"),
("currentRenderedItemId", "int"),
("entityColor", "vec4"),
];
#[derive(Clone, Debug, PartialEq)]
pub struct Uniform {
pub name: String,
/// The type the pack declared (`float`, `ivec2`, `mat4`; `bool` is stored as an `int`).
pub ty: String,
pub offset: u32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sampler {
pub name: String,
/// The type the pack declared: `sampler2D`, `sampler2DShadow`, `usampler2D`, `sampler3D`...
pub ty: String,
pub binding: u32,
}
/// A translation diagnostic at a pack file and line.
#[derive(Clone, Debug, PartialEq)]
pub struct Error {
pub file: String,
pub line: u32,
pub message: String,
}
#[derive(Clone)]
pub struct Program {
pub vertex: naga::Module,
pub fragment: naga::Module,
pub interface: Interface,
/// Non-fatal translation diagnostics, located in the authored source.
pub notes: Vec<Error>,
}
/// Where the program draws, which decides the conventions of 5.5.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Target {
/// Pack buffers: rows stored as GL stores them.
Buffers,
/// The window (`final`).
Window,
}
/// What a gbuffers-style program's vertex stage reads. The game writes it: it knows its vertex
/// formats, the translator only places it.
#[derive(Clone, Debug, Default)]
pub struct GbuffersInputs {
/// GLSL declarations: the vertex inputs and any bindings of set 1.
pub declarations: String,
/// GLSL statements that set `hz_Vertex`, `hz_Color`, `hz_MultiTexCoord0`,
/// `hz_MultiTexCoord1` and `hz_Normal` (the compatibility inputs), run first in `main`.
pub body: String,
/// The value of each attribute a pack may declare (`mc_Entity`, `vaPosition`...), as a GLSL
/// expression of the declared type's size; an attribute not listed reads zero. Terrain
/// also supplies `mc_chunkFade`, an implicit float input rather than a declared attribute.
pub attributes: Vec<(String, String)>,
/// GLSL statements run last in `main`, with `gl_Position` in wgpu's clip space.
pub after: String,
/// The alpha test applied to output 0 (`gl_FragData[0]`, `gl_FragColor`), the loader's
/// (3.2 `alphaTest.<program>`): `None` keeps every fragment.
pub alpha_test: Option<AlphaTest>,
/// The program may read `currentRenderedItemId` without declaring it, as Iris 1.11.7
/// lets the hand's (observed with probe packs; terrain's may not).
pub item_id_declared: bool,
/// The depth test sees the far plane (window depth 1) unless the program writes
/// `gl_FragDepth`, while `gl_FragCoord.z` stays the geometry's own: the sky shows only where
/// nothing else drew, and reads its own depth as in Iris 1.11.7.
pub far_depth: bool,
/// `normals` and `specular` are bound as paged maps of the atlas `texture` reads
/// (`horizon-client-render`'s `paged_maps.rs`), not as images of its layout: the program's
/// texture calls on them read through `hz_pagedTexture`.
pub paged_maps: bool,
/// Translucent terrain: a fragment behind the boats' water patches (`horizonWaterMask`, the
/// patches' depth, 1 where there are none) is discarded, so no water shows inside a hull
/// (`RenderTypes.waterMask`), while the frame's depth stays the pack's own.
pub water_mask: bool,
/// The shadow pass's translucent terrain: a fragment inside a boat's water patch
/// (`horizonWaterPatch*`, camera-relative boxes) is discarded, so no water casts its light
/// into a hull. Not with a geometry stage.
pub water_patches: bool,
}
/// The sampler of the boats' water patches' depth (`GbuffersInputs::water_mask`).
pub const WATER_MASK: &str = "horizonWaterMask";
/// How many boats' water patches the shadow pass keeps water out of (`GbuffersInputs::water_patches`).
pub const WATER_PATCHES: usize = 16;
/// A fragment stays when its output 0's alpha compares to `reference` as `op` says (a GLSL
/// comparison operator, `true` for GL's `ALWAYS`, `false` for `NEVER`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct AlphaTest {
pub op: &'static str,
pub reference: f32,
}
impl AlphaTest {
/// `alphaTest.<program>`'s value: `off`, or a GL comparison and a reference.
pub fn parse(value: &str) -> Option<Option<Self>> {
let mut words = value.split_whitespace();
let func = words.next()?;
if func == "off" {
return Some(None);
}
let op = match func.trim_start_matches("GL_") {
"NEVER" => "false",
"LESS" => "<",
"EQUAL" => "==",
"LEQUAL" => "<=",
"GREATER" => ">",
"NOTEQUAL" => "!=",
"GEQUAL" => ">=",
"ALWAYS" => return Some(None),
_ => return None,
};
let reference = words.next()?.parse().ok()?;
Some(Some(Self { op, reference }))
}
}
pub use horizon_render_pipeline::program::{
AlphaTest, ComputeProgram, DRAW_UNIFORMS, Error, GbuffersInputs, Image, Interface, Program,
Sampler, Target, Uniform, WATER_MASK, WATER_PATCHES,
};
/// Translates a composite-style program (`composite`, `deferred`, `final`...).
pub fn translate_composite(
+1
View File
@@ -30,6 +30,7 @@ To play, use [the launcher](../launcher/README.md); the project site is [playhor
- [Cosmetics](cosmetics.md): wearable items drawn only by Horizon clients (hats, back items, shoulder pets, name tag styles), with no gameplay effect, client-side visibility toggles and the JSON item format.
- [Layers](layers.md): engine, platform and Horizon: what belongs to each, where a new feature goes, the dependency rule, every built-in outside address.
- [Forking](forking.md): the `horizon-extras` feature that holds Horizon's own extras (cosmetics, Friends, its settings pages...), and building the game without them.
- [Translations](translations.md): where Horizon's own text and its ~85 languages live, the unreviewed mark on machine-written strings, and how a native speaker reviews a language in #translations on Discord.
- [Narrator](narrator.md): vanilla's text-to-speech (Ctrl+B): the voices per platform, how screens record what they narrate, `horizon-ctl narration`, and where it still differs from vanilla.
- [Shader packs](shaders.md): Horizon's own WGSL shader packs, how to make and check one (`horizon-shader`).
- [Shader-pack clean room](cleanroom/README.md): how Java shader-pack support was rebuilt without another loader's code: the [format specification](cleanroom/shader-packs-spec.md) and the [provenance log](cleanroom/provenance-log.md) of every source read.
+284 -105
View File
@@ -1,36 +1,58 @@
# Layers: engine, platform, Horizon
Horizon is three things in one repository:
Horizon is three layers:
- a **vanilla engine**;
- a **platform** that makes the vanilla game easier to change and run;
- **Horizon's own product** on top.
- a **platform** that makes the vanilla game easier to change, port and run;
- **Horizon's own layer**: its extras and its product (cosmetics, the shop, telemetry, sponsors, official servers,
the launcher...).
This page says what belongs to each layer, which crate may depend on which, and where a new feature goes.
Contributors fixing vanilla behaviour should never have to touch cosmetics, the shop or a server address. A fork
should be able to take the engine and the platform without Horizon's product.
**The target** (Ludovic, 2026-11-09). This repository holds the engine and the platform. Built alone, it is a
complete vanilla-like game. The Horizon layer moves to its own repository, public if its license needs it, and
ships with every official launcher download (see [The Horizon layer in its own repository](#the-horizon-layer-in-its-own-repository)).
The split comes first, inside this repository, one checked step at a time. The repositories separate after that.
CI checks the rules, so review doesn't have to. Everything stays in one repository: crates are checked now, and
the modules of the mixed crates follow as code moves.
This page says:
Credits: the three layers, the dependency rule, the address rule and the plan come from duckybertduck's proposal
("3-layer split: engine / platform / Horizon", #contributors, 2026-11-08). The rule that the mod API owns its types
and the property check that keeps engine types out of it come from arismoko's mod API work on
[arismoko/horizon](https://github.com/arismoko/horizon) (arismoko/horizon#1608).
- what belongs to each layer and which layer may depend on which;
- where a new feature goes;
- where every built-in outside address lives;
- the plan.
CI checks the rules, so review doesn't have to. Contributors fixing vanilla behaviour never have to touch
cosmetics, the shop or a server address. Mods and desktop-class ports (iOS, Android) build on the platform without
editing engine files; ports to machines the desktop client can't run on (a handheld console, a browser) build on
the [portable crates](#portable-crates).
Credits:
- **NikitaMGrimm** (DuckyBertDuck on Discord): the three layers, the dependency rule, the address rule and the plan
("3-layer split: engine / platform / Horizon", #contributors, 2026-11-08), the portable crates (#12997, #13002),
and the review that made the Horizon layer linked crates rather than native mods.
- **arismoko:** the mod API owns its types and a CI property check keeps engine types out of it
([arismoko/horizon](https://github.com/arismoko/horizon), arismoko/horizon#1608). Also the idea of shipping
Horizon's own parts from another repository as a launcher plugin.
- **Martipops** (controllers, splitscreen): the compositor's place in the engine, and where controllers,
splitscreen, Replay Studio, PvP & HUD and a UI page API go.
- **Contributors at the 2026-11-08 stage:** the file-size rule, the single list of addresses, sponsors in the
Horizon layer, a platform that exposes what vanilla already uses, and extras as native mods.
## The rule
```
engine < platform < Horizon (tool: dev tools, outside the order)
```
| Layer | May depend on |
|---|---|
| engine | engine |
| platform | engine, platform |
| Horizon | platform, Horizon |
| tool (dev tools) | anything; no shipped crate depends on a tool |
A crate depends only on crates of its own layer or of a lower one. The engine never depends on the platform or on
Horizon, and the platform never depends on Horizon. Normal, build and optional dependencies all count;
dev-dependencies don't, so tests may use anything.
The engine never depends on the platform or on Horizon. The platform never depends on Horizon. **The Horizon layer
never depends on the engine:** it uses only the platform's public API, so it can build from another repository.
Normal, build and optional dependencies all count; dev-dependencies don't, so tests may use anything.
When lower code needs something from above, the lower layer offers an extension point and the higher layer plugs
into it. The extension point is a trait, a callback list, an event or a registry, and it names no type from above.
The program players run is assembled at the top, where every layer is in reach.
When Horizon needs something from the engine, the platform exposes it as public API.
## Engine
@@ -39,44 +61,107 @@ Vanilla Minecraft: Java Edition 26.3, and nothing else.
- **Belongs:** gameplay, world data and generation, block behaviour, mobs, items, commands, the network
protocol, rendering, sound, vanilla's screens, saves, the integrated and dedicated servers. It also holds
optimizations that keep vanilla's behaviour, and the extension points the platform plugs into.
- **Also the render pipeline's own capabilities that vanilla's path doesn't use**, when a higher layer could only
get them by rebuilding the pipeline. The one today is the **compositor**: drawing a view into an offscreen target
and composing several targets into a window's views. With one view (vanilla, and splitscreen with one player)
the renderer takes vanilla's path and the compositor does nothing, so the engine still plays 1:1 vanilla.
Splitscreen, the feature that uses it, is platform (below).
- **Examples:** `horizon-world` (`worldgen/`, `level/redstone.rs`), `horizon-protocol` (`packets/`),
`horizon-items`, `horizon-command`, `horizon-core`, `game/creatures.rs`, `game/integrated.rs`, `gfx.rs`,
`mesher.rs`, `streaming.rs`, `lan/`.
- **Never:** a mod hook's implementation, a shader or pack format vanilla doesn't read, a Horizon-only setting
or screen, a network address vanilla doesn't contact, Horizon branding.
- **Never:** a mod hook's implementation, a shader or pack format vanilla doesn't read, a setting or screen vanilla
doesn't have, a network address vanilla doesn't contact, Horizon branding.
- **Network:** only what vanilla contacts: Mojang and Microsoft services, and the servers the player joins.
- **May depend on:** engine crates.
### Portable crates
`horizon-core`, `horizon-world`, `horizon-protocol`, `horizon-items`, `horizon-command`, `horizon-client-core`
and `horizon-assets` hold vanilla's rules and data with no desktop in them: no window, GPU, sound, gamepad or
clipboard library, no HTTP client, and no engine crate outside this list. The platform sits on the desktop engine
(`wgpu`, `winit`, `cpal`), which a handheld console or a browser can't run, so a port there builds on these crates
instead. They stay that way: the layer check fails when one takes `winit`, `wgpu`, `cpal`, `gilrs`, `arboard` or
`ureq`, or an engine crate outside the list (#13002). Gameplay itself still lives in `horizon-client`; step P of
the [plan](#plan) moves it here over time.
## Platform
What makes vanilla nicer to change and run, with no Horizon branding and no address of its own.
What makes vanilla nicer to change, port and run, with no Horizon branding and no address of its own. It is the
public API that mods, desktop-class ports and the Horizon layer build on.
- **Belongs:** the mod API and mod formats, the native mod host, the shader API and pack formats beyond vanilla
(OptiFine, Polytone, `shaders/`), HDR, controllers, Horizon-only settings that are off by default (zoom, mouse
tweaks, splitscreen), agent control (`horizon-ctl`, MCP), the dedicated server's anticheat and chat filter,
modpack porting, the design tokens of Horizon-only screens.
(OptiFine, Polytone, `shaders/`), HDR, controllers, splitscreen, replay recording and playback, the HUD widget
framework, UI pages and the settings system, agent control (`horizon-ctl`, MCP), the dedicated server's anticheat and chat
filter, modpack porting, the design tokens of non-vanilla screens.
- **Exposes what vanilla already uses.** FOV changes, potion effects, the player's state, screens, rendering
hooks and so on become public API with stable types (arismoko/horizon#1608's patterns). Then a mod, a port or a
Horizon feature builds on them instead of editing or copying engine code.
- **Examples:** `horizon-shader-api`, `horizon-shaders`, `horizon-optifine` (`cem/`), `horizon-polytone`,
`horizon-hdr`, `horizon-dualsense`, `horizon-mods`, `cargo-horizon-mod`, `horizon-anticheat`, `horizon-mcp`,
`horizon-theme`. Still in engine crates today: `game/mod_api_host.rs`, `game/native_*.rs`, `game/extras.rs`,
`shader_ui.rs`, `hdr_calibration.rs`.
- **Never:** a built-in address (every address comes from the player, a file or a setting), cosmetics, the shop,
official or partner servers, uploads to playhorizon.dev.
`horizon-theme`. Still in engine crates today: `game/mod_api_host.rs`, `game/native_*.rs`, `shader_ui.rs`,
`hdr_calibration.rs`.
- **Never:** a built-in address (every address comes from the player, a file or a setting), anything that names
a Horizon crate, cosmetics, the shop, official or partner servers, uploads to playhorizon.dev.
- **Network:** allowed, but never to a built-in address.
- **May depend on:** engine and platform crates.
### Controllers, splitscreen, replays and UI pages
Vanilla has none of these, and the engine stays 1:1 vanilla, so they are platform features on hooks the engine
exposes. Ports to consoles and handhelds need them, and other projects can build on them.
- **Controllers:** the pad backend (gilrs), bindings, deadzones and the look curve, button glyphs, the pad's menu
navigation and cursor, the controller settings, DualSense output. Today in `horizon-client-input/src/gamepad.rs`,
`game/pad.rs`, `game/pad_*.rs` and `game/pad/`, `pad_cursor.rs`, `pad_glyph.rs`, `controller_ui.rs`, and
`horizon-dualsense`. The engine's hooks:
- **Actions.** Every input the game reads is an `Action`, vanilla's key mappings (`horizon-client-input/src/actions.rs`),
and code asks whether an action is held (`held.rs`), never a key. A pad presses and holds actions like a
keyboard does. This hook exists today.
- **Movement and look.** An analog move vector and a look delta that the player's movement reads instead of
keys (today `game/pad.rs` calls `gamepad::move_vector` and `Game::look_turn` itself).
- **Menu focus.** Screens say which widget has focus and take "move focus" and "press" (today the pad's
cursor drives vanilla's screens through mouse events).
The last two become a registered input source, a trait in the engine that the platform's pad code implements,
in M6. Until then the pad code stays in its own modules, so that move is a pure file move.
- **Splitscreen:** more local players, each its own `Game` joined to P1's server thread through memory; which
device drives which player; the setup screen, window layouts and profiles. Today in `game/split.rs` and
`game/split/`, `split_layout.rs`, `split_map.rs`, `split_profiles.rs`, `split_ui.rs`. It draws through the
engine's compositor (see [Engine](#engine)), and its input comes through the input hooks above, one input
source per player.
The compositor stays in the engine, as Martipops (who builds splitscreen) and duckybertduck argued: it is part
of how the renderer presents a frame (targets, submission, pacing, present). Moving it above the engine would
mean exposing the whole frame pipeline as API or rebuilding it up there, for a feature that on vanilla's path
costs nothing. What the engine must not hold is the feature: the router, the screens, the layouts.
- **Replays:** recording the session's packets as `.mcpr` (the format other replay tools read) and playing them
back through the client (`studio/recorder.rs`, `mcpr.rs`, `replay.rs`) is platform: mods, tools and bug reports
build on it. The Studio on top of it (camera paths, splines, the timeline and its bar: `studio/camera.rs`,
`spline.rs`, `path_render.rs`, `timeline_ui.rs`, `topbar.rs`, `studio_ui.rs`) is a finished feature, so it
goes to the Horizon layer or a native mod (rule 4), built on the platform's replay API.
- **UI pages:** a general API for pages a mod, a port or the Horizon layer adds (the settings shell, its rows,
search and undo, today `settings_shell.rs` and `extras_ui.rs`'s pages). The engine exposes screens: open one, draw on
the GUI layer, take input and focus. The platform gives the page API stable types (as #1608 does for the mod
API). Horizon Settings, the controller pages and the PvP & HUD page all become clients of it.
- **PvP & HUD:** the HUD widget framework (placing, sizing and toggling HUD elements: `hud_kit.rs` and `hud_kit/`,
`extras_hud.rs`, `game/hud_room.rs`, `game/hud_editor.rs`) is platform. `hud_layout.rs` is vanilla's `Hud` as draw
calls, which the parity fixture compares with the vanilla client: engine, like `hud_overlays.rs`. The PvP presets and Horizon's own widgets (keystrokes, CPS, reach readout, `pvp.rs`, `pvp_hud.rs`)
are Horizon extras, and candidates to become native mods.
## Horizon
Horizon's product, and every built-in address of an outside service vanilla doesn't contact.
Horizon's own extras and its product, and every built-in address of an outside service vanilla doesn't contact.
- **Belongs:** cosmetics, emotes, the wardrobe and the shop, the official and partner servers in the list,
Play with Friends and its relay, Discord, the "I'm playing" ping, telemetry and benchmark uploads, releases and
updates, hosting, the launcher, and the assembled game.
- **Examples:** `horizon-addresses`, `horizon-cosmetics`, `horizon-friends`, `horizon-relay`, `horizon-telemetry`, `horizon-bench`,
`horizon-release-sign`, `horizon-server`, `launcher/`. Still in engine crates today: `game/cosmetics.rs`,
`game/emotes.rs`, `game/discord.rs`, `heartbeat.rs`, `multiplayer.rs` (`add_official_server`).
- **Rules:** in the game, every Horizon feature sits behind the `horizon-extras` feature ([forking.md](forking.md)),
so [horizon-plain](https://github.com/horizon-rust/horizon-plain) builds without it.
- **May depend on:** anything but tools.
- **Belongs:** Horizon's extras (zoom, Mouse Tweaks, sorting, smooth chat, the PvP & HUD presets and widgets, the
music page, keystrokes, auto-reconnect...), cosmetics, emotes, the wardrobe and the shop, sponsors, the official
and partner servers in the list, Play with Friends and its relay, Discord, the "I'm playing" ping, telemetry and
benchmark uploads, releases and updates, the hosting supervisor, the launcher.
- **Examples:** `horizon-addresses`, `horizon-cosmetics`, `horizon-friends`, `horizon-relay`, `horizon-telemetry`,
`horizon-bench`, `horizon-release-sign`, `horizon-server`, `horizon-mod-registry`, `launcher/`. Still in engine
crates today: `game/cosmetics.rs`, `game/emotes.rs`, `game/discord.rs`, `game/extras.rs`, `game/tweaks.rs`,
`heartbeat.rs`, `multiplayer.rs` (`add_official_server`), `extras_ui.rs`, `pvp.rs`.
- **Rules:**
- It uses the platform's public API only, never the engine.
- Until it moves, every Horizon feature in the game sits behind the `horizon-extras` feature
([forking.md](forking.md)), so [horizon-plain](https://github.com/horizon-rust/horizon-plain) builds without it.
## Tools
@@ -85,77 +170,156 @@ Developer tools that no player runs: `xtask`, `horizon-bots`, `horizon-mod-api-g
## Where a new feature goes
The rule, in order (duckybertduck's):
1. Vanilla has it: **engine**. Hooks in the engine are fine: the platform needs them.
2. It is directly about Horizon (server addresses, the website, the name or branding): **Horizon**.
3. It helps other people build on the project (mods, ports, other clients; dev tooling excepted, see
[Tools](#tools)): **platform**.
4. Everything else: **Horizon**, or a native mod that players add themselves, since it can be built on what's below.
A feature often splits along this rule: the part others build on is platform, and the finished feature on top of it
is Horizon (PvP & HUD and replays, [above](#controllers-splitscreen-replays-and-ui-pages)).
| I add... | Layer | Where |
|---|---|---|
| A vanilla behaviour, block, mob, packet, command or screen | engine | the crate that owns the area ([CLAUDE.md](../CLAUDE.md) crate table) |
| A speed-up that keeps vanilla's results | engine | next to the code it speeds up |
| A call to a Mojang or Microsoft service vanilla makes | engine | `auth.rs`, `realms/`, `skins.rs`, `horizon-server-files` |
| A place mods or packs can hook into | engine | an extension point (trait, callback, event) in the engine crate, naming no platform type |
| A place mods, ports or Horizon can hook into | engine + platform | an extension point in the engine crate, naming no type from above; its public face in the platform |
| A mod hook, host method or mod format | platform | `horizon-mods`, the mod API (today: `game/mod_api_host.rs`, `game/native_*.rs`) |
| A shader or resource pack format vanilla doesn't read | platform | `horizon-shaders`, `horizon-optifine`, `horizon-polytone`, or a new platform crate |
| A setting or screen vanilla doesn't have, off by default | platform | its own module, reached from Horizon Settings |
| A dedicated server option (anticheat, chat filter) | platform | `horizon-anticheat`, `horizon-chat-filter` |
| Anything that talks to playhorizon.dev, a partner or a third-party service by itself | Horizon | a Horizon crate, with its address in the address list below |
| Cosmetics, emotes, the shop, Discord, telemetry, official servers | Horizon | `horizon-cosmetics`, `horizon-telemetry`..., behind `horizon-extras` |
| Controller support: a device, bindings, glyphs, pad menus | platform | the pad modules, on the engine's input hooks ([Controllers](#controllers-splitscreen-replays-and-ui-pages)) |
| Splitscreen or more players on one machine | platform | the `split` modules, on the engine's compositor |
| A render pipeline capability vanilla's path doesn't use but a higher layer can't add without rebuilding the pipeline (the compositor) | engine | `gfx.rs` and `gfx/`, doing nothing on vanilla's path |
| A UI page others can add to, a HUD widget framework, replay recording and playback | platform | the page API, `hud_kit.rs` and `hud_kit/`, `studio/recorder.rs`, `mcpr.rs`, `replay.rs` |
| A finished feature built on those (PvP presets, the replay editor) | Horizon, or a native mod | behind `horizon-extras` |
| A Horizon extra (a setting, screen or HUD element vanilla doesn't have) | Horizon | its own module behind `horizon-extras`, built on platform API; or an optional native mod that players add themselves |
| Anything that talks to playhorizon.dev, a partner or a third-party service by itself | Horizon | a Horizon crate, its address in `horizon-addresses` |
| Cosmetics, emotes, the shop, sponsors, Discord, telemetry, official servers | Horizon | `horizon-cosmetics`, `horizon-telemetry`..., behind `horizon-extras` |
| A launcher page | Horizon | `launcher/src/` |
| A harness, script or dev tool | tool | `tools/`, `xtask/` |
| A new crate | its layer | add it to [`tools/layers.json`](../tools/layers.json) in the same PR |
| A new crate | its layer | give it a layer in `tools/layers.json` in the same PR |
Code that belongs higher than the crate it has to live in today (a platform setting in `horizon-client-render`,
say) goes in its own module, never mixed into an engine module. Horizon code in the game also goes behind
`horizon-extras`. That keeps each later move a pure file move.
Code that belongs to another layer than the crate it has to live in today (a Horizon extra in
`horizon-client-render`, say) goes in its own module, never mixed into an engine module. Horizon code in the game
also goes behind `horizon-extras`. That keeps each later move a pure file move.
## The check
## The Horizon layer in its own repository
`python3 tools/check_layers.py` runs in CI's verify job and in `tools/pr_ready.py`. It reads the four workspaces'
`cargo metadata` (no build, about a second) and [`tools/layers.json`](../tools/layers.json), which gives every
crate its layer. It fails when:
The target, after the split. It is not built yet: nothing moves before the split below is done.
- a crate has no layer;
- a dependency goes up a layer and `allowed` doesn't list it;
- a pull request adds an entry to `allowed` for a crate that already had a layer (a crate newly split out of another
may carry the old crate's exceptions);
- shipped code depends on a tool.
**What a build of this repository gives.** A complete game: vanilla play, the platform (mods, shader packs,
controllers, splitscreen, replays, HDR, the dedicated server) and no Horizon layer. That means no extras, cosmetics, shop, telemetry,
sponsors or official server, no built-in outside address, and no launcher. Forks and builds from source get exactly
that. horizon-plain becomes a plain build of this repository.
Each failure names the section of this page that applies. An `allowed` entry that has no dependency left is only
reported: take it out when you like. `--list` prints every crate's layer and the upward dependencies left.
**The seam.** The Horizon repository has to plug into the game without changing it:
1. No engine or platform code names a Horizon crate. The layer check refuses it.
2. The Horizon layer uses the platform's public API only. The layer check refuses its engine dependencies, and
today's are on the exceptions list.
3. Horizon's in-game parts (extras, cosmetics, emotes, Discord, Play with Friends, the ping) are crates linked into
the official game build in CI, plugged in through the platform's hooks. Players never compile them: a native
mod is source the launcher compiles per instance, so it is not the way. The official executable is a thin
binary crate in the Horizon repository that links the platform and the Horizon crates; this repository keeps
its own plain executable. The game runs the same without them.
4. Every Horizon address is in `horizon-addresses`, which moves with the layer.
5. The launcher is Horizon's: it moves too. It installs the game from this repository's releases and adds the
Horizon layer to it.
**Licence.** The project's licence is being decided (the poll ends around 2026-11-10); the Horizon repository's
licence follows from it. If this repository becomes GPL, the official game links the Horizon crates into the same
executable as the engine and the platform, so the Horizon layer must be GPL-compatible too: no closed native code in
it. Mods loaded as WebAssembly run as separate modules, not linked into the executable, so they could stay closed
(duckybertduck's reading; the licence decision settles it).
**Every official download includes it.** These paths must ship the Horizon layer:
| Path | Today |
|---|---|
| The launcher's release builds, every platform and format: Linux `.tar.gz` and AppImage, macOS `.zip` and signed `.dmg`, Windows `.zip` and `horizon-launcher-setup*.exe` | `.github/workflows/launcher.yml` |
| GitHub releases (`[email protected]`) | `launcher.yml`'s release job, `tools/release_train.py` |
| playhorizon.dev's download page | horizon-rust/horizon-site, reading `launcher/links.json` and the releases |
| The launcher's self-update | `launcher/src/backend.rs` (`stage_update`), `launcher/src/update_handoff.rs`, `launcher/src/nightly.rs` |
| The game builds the launcher installs, so the in-game part reaches every player | `.github/workflows/nightly-build.yml`, `release.yml`, `game-build.yml` |
| The links the launcher and the site read (Discord, sponsors, website, official server) | `launcher/links.json` |
These stay without it: builds of this repository from source, horizon-plain, and `packaging/server/install.sh`
and `install.ps1` (the dedicated server). The server's anticheat and chat filter are platform features.
## Extras that may leave the game
Horizon's extras are Horizon-layer code. Each one is a candidate to become an optional native mod that players add
themselves, or stays a cfg-gated module linked into the official build. None moves yet; the list is here so new work doesn't add to them. See [extras.md](extras.md) for each one:
- zoom (OptiFine-style), keystrokes and FPS overlays, own name tag;
- Mouse Tweaks controls, sorting, scroll-wheel settings, smooth chat;
- auto-reconnect, UI-sound channels, the music page;
- the PvP & HUD presets and widgets, the replay editor (Replay Studio's camera paths and timeline), the AI agent page;
- the Horizon Settings menu itself, the advanced menu that hosts them;
- the renderer's extras: fullbright, dynamic lights, hidden block-break particles;
- chunk pregeneration.
The minimap already moved to a native mod ([extras.md](extras.md#moved-to-native-mods)). Splitscreen,
controllers and replay recording are not extras: they are platform ([above](#controllers-splitscreen-replays-and-ui-pages)).
## File size
A Rust file over 3000 lines may not grow compared with the base of the branch. `game.rs` (~21,700 lines),
`game/remote.rs` (~10,900) and `gfx.rs` (~10,100) are the worst; they slow incremental builds. Smaller files may
grow to 3000 lines, and a big file may shrink or stay as it is. There is no list of files, so no check breaks on
an unrelated edit. Generated files and `third_party/` are not checked.
## The checks
- `python3 tools/check_layers.py` (#12983) reads the workspaces' `cargo metadata` (no build, about a second) and
`tools/layers.json`, which gives every crate its layer and each layer what it may use. It fails on:
- a crate with no layer;
- a dependency its layer may not use that `allowed` doesn't list;
- a branch adding an `allowed` entry for a crate that already had a layer (a crate newly split out of another
may carry the old crate's exceptions);
- shipped code depending on a tool.
Each failure names the section of this page that applies. An `allowed` entry with no dependency left is only a
note.
- `python3 tools/check_addresses.py` (#12990) checks the built-in addresses ([below](#built-in-addresses)).
- `python3 tools/check_file_size.py` (#13015) checks the file-size rule above.
All three run in CI's verify job and in `tools/pr_ready.py`.
### Today's exceptions
Today `allowed` has 40 entries, all from the three mixed engine crates (main at d5504cd328, 2026-11-08):
47, on main at d5504cd328 (2026-11-08):
| Crate | Depends upward on |
| Crate | Uses, against the rule |
|---|---|
| `horizon-client` | platform: `horizon-anticheat`, `-chat-filter`, `-dualsense`, `-hdr`, `-mod-api-gen`, `-mods`, `-optifine`, `-shader-api`, `-shaders`, `-theme`; Horizon: `horizon-bench`, `-cosmetics`, `-friends`, `-telemetry` |
| `horizon-client-lib` | the same, plus `horizon-mcp`, minus `horizon-mod-api-gen` |
| `horizon-client-render` | platform: `horizon-hdr`, `-mod-api-gen`, `-optifine`, `-polytone`, `-shader-api`, `-shaders`, `-theme`; Horizon: `horizon-bench`, `-cosmetics`, `-telemetry` |
| `horizon-client-input` | platform: `horizon-dualsense`, `horizon-theme` |
| `horizon-launcher` (Horizon) | engine: `horizon-core`, `horizon-prepare`, `horizon-protocol`, `horizon-world` |
| `horizon-server` (Horizon) | engine: `horizon-core`, `horizon-server-files`, `horizon-world` |
No platform crate depends on Horizon. `horizon-client` is the gameplay library and the executable at once. It
splits in two: the engine's gameplay library, and a thin executable at the Horizon layer that wires in the
platform and Horizon crates.
No platform crate depends on Horizon.
## Built-in addresses
An address is built in when the code contacts it without the player typing it. Engine code may hold vanilla's own
endpoints. Every other built-in address belongs to the Horizon layer, in one file: `crates/horizon-addresses/src/lib.rs`,
which the game's Horizon crates and the launcher both read. Platform code gets its addresses passed in from there.
endpoints. Every other built-in address belongs to the Horizon layer, in one list file:
`crates/horizon-addresses/src/lib.rs` (#12993). The game's Horizon crates and the launcher read it, and it moves
with the Horizon layer. Platform code gets its addresses passed in.
`python3 tools/check_addresses.py` (CI's verify job, and pr_ready's "Addresses" step) reads the string literals of
shipped Rust and finds every host: a URL's host, or a whole string that is a host name. Tests are skipped, and so are
placeholders (`example.com`, `*.test`) and local IPs. [`tools/addresses.json`](../tools/addresses.json) says where
each host may be:
`tools/check_addresses.py` reads the string literals of shipped Rust and finds every host. It skips tests,
placeholders (`example.com`, `*.test`) and local IPs. `tools/addresses.json` says where each host may be:
- `homes`: the address home above, where anything goes;
- `homes`: the list file;
- `vanilla`: vanilla's endpoints, allowed anywhere;
- `not_addresses`: strings that look like hosts but are not contacted, each with its reason;
- `allowed`: today's addresses outside the home, as "path host".
- `allowed`: today's addresses outside the list file, as "path host".
`allowed` only shrinks. A branch may move an entry with its code to another file, but it may not add a host or grow
the list: a new address goes in the home. An entry whose address left its file is reported as a note. `--list`
prints every host found and its files.
Where each address is today:
`allowed` only shrinks. A branch may move an entry with its code, but it may not add a host or grow the list.
Vanilla's endpoints (engine, the same as Java Edition):
@@ -171,49 +335,64 @@ Horizon's own and third-party addresses (Horizon layer):
| Address | For | Where today |
|---|---|---|
| `play.playhorizon.dev` (status `:24454`), previously `horizon.bunchy7s.dev` | the official server in the list, its status | `horizon-addresses`, `launcher/links.json` |
| `play.playhorizon.dev` (status `:24454`), previously `horizon.bunchy7s.dev` | the official server in the list, its status | `horizon-addresses` (#12993), `launcher/links.json` |
| `cosmetics.playhorizon.dev` | cosmetics, emotes, presence | `horizon-addresses` (#12993) |
| `telemetry.playhorizon.dev` | opt-in telemetry, the "I'm playing" ping, benchmark uploads | `horizon-addresses` (#12993) |
| `stun.cloudflare.com`, `stun.l.google.com` | Play with Friends NAT discovery | `horizon-addresses` (#12993) |
| `afri-network.co.za` | the withdrawn partner server, so the game can remove the entry it added | `multiplayer.rs` (`WITHDRAWN_PARTNER`) |
| `cosmetics.playhorizon.dev` | cosmetics, emotes, presence | `horizon-addresses` |
| `telemetry.playhorizon.dev` | opt-in telemetry, the "I'm playing" ping, benchmark uploads | `horizon-addresses` |
| `stun.cloudflare.com`, `stun.l.google.com` | Play with Friends NAT discovery | `horizon-addresses` |
| `horizon-rust.github.io/horizon-mods`, `github.com/horizon-rust/horizon-mods` | the mod registry index and files | `mod_registry.rs`, `horizon-mods/src/index*.rs`, `cargo-horizon-mod` |
| `horizon-rust.github.io/horizon-mods`, `github.com/horizon-rust/horizon-mods` | the mod registry index and files: a build of this repository alone asks the player for a registry | `mod_registry.rs`, `horizon-mods/src/index*.rs`, `cargo-horizon-mod` |
| `api.modrinth.com`, `cdn.modrinth.com` | the in-game resource pack browser, modpack porting | `pack_browser.rs`, `horizon-port-pack` |
| `github.com`, `raw.githubusercontent.com`, `gitlab.com`, `maven.neoforged.net` | modpack porting's sources | `horizon-port-pack` |
| `playhorizon.dev` | the download link shown with no resources | `no_resources.rs` |
| `api.github.com`, `discord.com` | hosting: updates, Discord webhooks | `horizon-server` |
| `api.github.com`, `uploads.github.com`, `playhorizon.dev` | the registry's submission service | `horizon-mod-registry` |
| Modrinth, CurseForge, Fabric, Quilt, Forge, NeoForge, GitHub, Microsoft and Xbox sign-in, Tailscale, `discord.gg`, Sculk Hosting, `static.rust-lang.org` | the launcher's browsers, loaders, sign-in, hosting and partner pages | `launcher/src/` |
| Modrinth, CurseForge, Fabric, Quilt, Forge, NeoForge, GitHub, Microsoft and Xbox sign-in, Tailscale, `discord.gg`, `mclo.gs`, Sculk Hosting, `static.rust-lang.org`, GitHub Sponsors | the launcher's browsers, loaders, sign-in, logs, hosting, partner and sponsor pages | `launcher/src/`, `launcher/links.json` |
These are not addresses:
- `bluemap_detect.rs` lists dynamic-DNS suffixes, to guess a server's own map host;
- `horizon-server/src/panel.rs` matches `.ts.net`;
- the Windows and Apple schema URLs in manifests.
- `horizon-server/src/discord.rs` lists the hosts a webhook URL may have;
- the Windows, Apple and SOAP schema URLs in manifests and requests, and the UPnP multicast group.
## Reviewer checklist
- The PR names its layer, and new code sits in a crate or module of that layer (the table above).
- No new upward dependency. CI fails on one; a module that calls up from an engine crate is just as wrong.
- Horizon features in the game are behind `horizon-extras`. Platform features that change the vanilla look are
off by default.
- No new built-in address outside the Horizon layer.
- No dependency its layer may not use. CI fails on one; a module in an engine crate that calls platform or Horizon
code is just as wrong, and so is Horizon code that reaches into the engine instead of the platform.
- Horizon features in the game are behind `horizon-extras`, and nothing new names a Horizon crate below it.
- No new built-in address outside `horizon-addresses`.
- No file over 3000 lines grows.
## Plan
One small PR at a time, stacked:
Review first. The design and this plan are open for review in the tracking issue for ~48 hours. The small
mechanical steps (checks, M1–M4) continue meanwhile. M5 and M6 started on 2026-11-09, after the review in #13011. Each step is a PR under ~500 lines. Each move:
1. This page.
2. `tools/check_layers.py` in CI and pr_ready, with the checklist in the PR template.
3. `tools/check_addresses.py` in CI and pr_ready: built-in addresses only in the home, the rest only shrinks.
4. Moves, most contributor value first. The extras (cosmetics, emotes, Discord, the official server, telemetry
and the ping) leave the engine crates for Horizon crates. Then the renderer's platform and Horizon modules
leave `horizon-client-render`. Then the executable splits from the gameplay library. Each move:
- keeps the default build the same for players and costs nothing at run time;
- keeps `--no-default-features` and horizon-plain building;
- takes its entries out of `allowed`.
5. Optionally, a lines-of-code check per file.
- keeps the default build the same for players and costs nothing at run time;
- keeps `--no-default-features` and horizon-plain building;
- takes its entries out of the allowlists;
- runs the harnesses `tools/parity.py --changed-files … --list` selects when it touches gameplay.
arismoko's API boundary work brings the mod API upstream from arismoko/horizon. The API owns its types,
`horizon-mod-runtime` gets no game dependency, and a CI property check keeps `horizon-mod-api` free of engine
crates other than `horizon-core`. With it, the mod API becomes a platform crate under this page's rule, and its own
stricter check stays.
| Step | What | Removes | State |
|---|---|---|---|
| 1 | This page, first version | | #12979, merged |
| 2 | Layer check in CI and pr_ready, with the PR template's checklist | | #12983, merged |
| 3 | Address check in CI and pr_ready | | #12990, merged |
| 4 | File-size check in CI and pr_ready | | #13015, merged |
| 2b | The portable crates' rule in the layer check ([Portable crates](#portable-crates)) | | #13002 (NikitaMGrimm), merged |
| M1 | `horizon-addresses`, the address list file; the Horizon crates read it | 7 address entries | #12993, merged |
| M2 | `horizon-system` (engine): the hardware probes leave `horizon-telemetry`, and the crash report and GPU info read them there; `horizon-platform`, the platform's public API, re-exports them for the Horizon layer | prepares M3 | #13058, merged |
| M3 | The renderer's telemetry module leaves `horizon-client-render` for `horizon-client-lib`, with Horizon's other in-game parts; the renderer offers the adapter (`gfx::adapter`) and a crash-report hook (`crash::on_report`) | render → telemetry, bench | #13060, merged |
| M4 | Cosmetics leave `horizon-client-render`: the tab list's mark through a name-decoration callback (#13094); the settings lines leave with `extras.rs` and `extras_ui.rs` in M5, as clients of the UI page API | render → cosmetics | #13094, merged; the rest in M5 |
| M5 | Horizon's in-game parts (extras, cosmetics, emotes, Discord, Friends, ping, benchmark upload, official server) leave `horizon-client` for Horizon crates linked into the official build through platform hooks | client, client-lib → cosmetics, friends, telemetry, bench | started 2026-11-09 |
| M6 | The engine crates stop depending on platform crates: the renderer's shader-pack, pack-format, HDR and theme code moves behind engine extension points; the mod host and agent control move to platform crates; controllers, splitscreen, replay recording and the UI page API move to platform crates on the engine's input-source, compositor and screen hooks; the launcher and `horizon-server` reach the engine through platform API | the engine → platform and Horizon → engine exceptions | started 2026-11-09; HDR's part with NikitaMGrimm, below |
| P | Optional, longer: gameplay into the portable crates. `Game` holds the world and its presentation (renderer, screens, particles, sound): split its state module by module, turn gameplay's direct particle and sound calls into level events as vanilla's server sends them, then move each clean module into a portable crate | | any time, small PRs |
| R | The Horizon layer moves to its own repository and ships in every official download (the table above) | | after the split, Ludovic's call |
HDR in M6: moving HDR behind engine extension points moves the frame pipeline (float targets, tone mapping,
present). NikitaMGrimm leads HDR, so that part is planned with them and comes after HDR's step 5 (float targets on
by default), so the pipeline moves once. HDR off stays bit-exact.
The mod API work on arismoko/horizon lands upstream as part of the platform. The API owns its types,
`horizon-mod-runtime` gets no game dependency, and its CI property check stays as a stricter check inside the
platform.
+6
View File
@@ -10,6 +10,12 @@ Two kinds of mod run in Horizon:
Fabric and NeoForge jars do **not** run as they are: their code calls and patches Minecraft's Java classes, and
Horizon has none. What carries over is the data and assets inside a jar ([modpacks.md](modpacks.md)).
Running Java mods unchanged needs a Java bridge (a JVM beside the game, proposed in [#10216](https://github.com/horizon-rust/horizon/pull/10216); not in Horizon today).
The project decided on 2026-11-09 that it will only ever be a **separate, optional download, never installed or on by
default**: when the game or the launcher finds a Java mod it may offer to turn it on, and the player decides. Before
it is promoted out of beta, its server MSPT and memory are measured against the Java client running the same mods
(plan: [#13914](https://github.com/horizon-rust/horizon/issues/13914)).
Mods are **off by default**. A fresh install has no `horizon-mods` folder and plays exactly like vanilla 26.3.
## Playing with mods
+79
View File
@@ -0,0 +1,79 @@
# Translations
Horizon's own text (its menus, settings and the launcher) is translated into about 85 languages. Vanilla's text
comes from the player's own Minecraft install and is not translated here. Most of Horizon's translations were
written by machine, and players reported some of them as wrong, so the project decided on 2026-11-09 that every
machine-written string carries an **unreviewed** mark until a person who speaks the language has read it. Players see
no difference: the mark lives in a file the game and the launcher never read.
## Where the text lives
| | Files | Keys | Read by |
|---|---|---|---|
| Game | `crates/horizon-client/assets/horizon/lang/<code>.json` (`fr_fr`, `de_de`...) | `horizon.options.foo` style; `en_us.json` is the reference | `crates/horizon-client-core/src/lang.rs` (`Text::tr`, `lang::get`); a missing key falls back to English |
| Launcher | `launcher/lang/<code>.json` (`fr`, `de`, `pt-br`...) | the English sentence itself | `launcher/src/i18n.rs` (`tr("…")`, `trf("… {arg} …")`) |
| Review record | `tools/lang_review/game/<code>.json`, `tools/lang_review/launcher/<code>.json` | the same keys, with the text a person checked | `tools/lang_coverage.py` only |
An entry is **reviewed** when the review record holds its key with exactly its present text. Anything else is
unreviewed: an entry the record does not hold, and one whose text changed since a person checked it (so a later
machine edit makes it unreviewed again). New strings need no extra step: added by machine, they start unreviewed.
## What is left to review
```sh
python3 tools/lang_coverage.py --summary # one line per file, "unreviewed N" at the end
python3 tools/lang_coverage.py --unreviewed --lang fr_fr fr # every unreviewed entry, its English and its text
python3 tools/lang_coverage.py --json # the same, for a script ("unreviewed" per file)
```
## Reviewing a language
Review happens in **#translations on Horizon's Discord** ([discord.gg/playhorizon](https://discord.gg/playhorizon)).
1. **Report a wrong string**: post the language, where you saw it (screen, setting, launcher page), the current text
and the better text. Anyone can do this; you don't need GitHub.
2. **Review a whole language**: ask in #translations for your language's list of unreviewed strings (it is the
`--unreviewed` output above). Go through it, in the game and launcher where you can, and answer with the strings
to change (current text, better text) and the ones that are fine as they are.
3. **The fix lands as a pull request**: a maintainer or agent changes the lang files with your wording and records
the checked entries as reviewed:
```sh
python3 tools/lang_coverage.py --lang fr_fr --mark-reviewed horizon.options.foo horizon.options.bar
python3 tools/lang_coverage.py --lang fr --mark-reviewed --keys-file checked.txt # one key per line
```
`--mark-reviewed` records the entries' present text, so change the text first, then mark it. It also drops
entries the file no longer has. A code that is both a game and a launcher file (`lmo`, `szl`) needs
`--game-only` or `--launcher-only`. Pull requests that only change translations take no label.
You can also open the pull request yourself: change the lang file, run `--mark-reviewed` for the entries you wrote
or checked, and say in the pull request which language you speak.
## Rules
- **Only a person who speaks the language clears a mark.** Never run `--mark-reviewed` on machine-written text, and
never on text an AI tool rewrote, even when it reads well. An agent marks only entries whose wording came from a
person, such as a #translations report it turns into a pull request (quote the report in the pull request).
- A new launcher string still needs an entry in every launcher lang file (`launcher/src/i18n/tests.rs` checks it),
and a new game string falls back to English where a language lacks it. A machine translation is fine for both: it
starts unreviewed by itself.
- Keep placeholders as they are (`%s`, `%1$s` in the game, `{name}` in the launcher): `lang_coverage.py` reports a
changed one.
## How the first record was made
On 2026-11-09 every entry of every lang file was traced to the commit that last changed it (`git blame -M`). An entry
counts as written by a person, and went into the record, only when that commit:
- was not made under an agent's identity (`Ludovic Marie`, which his agents commit as, `Horizon agent`, `Claude`,
bots) and is not a merge,
- has no AI co-author or AI mention in its message (Claude, Codex, GPT, Copilot, Cursor, Gemini, DeepSeek,
opencode...),
- changed at most three non-English lang files,
- and is by an author whose lang changes, over the whole history, cover at most two languages (a person writes the
one or two languages they speak; an author covering ten is a tool).
That kept 1,917 entries by nine contributors (Czech, German, Japanese, Brazilian Portuguese, Russian and Turkish).
The rule errs towards unreviewed: a wrong "unreviewed" costs a reviewer a glance, a wrong "reviewed" hides a bad
string. Everything else, about 197,000 entries, starts unreviewed.
+9 -1
View File
@@ -2841,11 +2841,19 @@ dependencies = [
]
[[package]]
name = "horizon-shader-api"
name = "horizon-render-pipeline"
version = "0.1.0"
dependencies = [
"bytemuck",
"naga",
]
[[package]]
name = "horizon-shader-api"
version = "0.1.0"
dependencies = [
"horizon-render-pipeline",
"naga",
"png 0.18.1",
"serde",
"serde_json",
+95 -1
View File
@@ -30,11 +30,76 @@ class ProblemsTests(unittest.TestCase):
def test_a_new_big_file_fails(self):
self.assertIn("a new file", check_file_size.problems([entry("a.rs", 0, 3500)])[0])
def test_the_message_names_both_ways_out(self):
found = check_file_size.problems([entry("crates/a/src/mobs/ai.rs", 3100, 3101)])[0]
self.assertIn('ai_tests.rs (`#[path = "ai_tests.rs"] mod tests;`)', found)
self.assertIn("pure-move PR first", found)
def test_third_party_and_generated_files_are_not_checked(self):
self.assertEqual(check_file_size.problems([entry("third_party/naga/src/x.rs", 8000, 9000),
entry("a.rs", 4000, 4100, generated=True)]), [])
TEST_MODULE = """#[cfg(test)]
mod tests {
use super::*;
#[test]
fn braces_in_literals_and_comments() {
let text = "}\\" {";
let close = '}';
let open = '{';
let quote = '\\'';
let raw = r#"
}
{"#;
// }
/* } /* nested { */ } */
assert!(text.contains(close) && open != quote && !raw.is_empty());
}
}
"""
def skipped(text):
return check_file_size.test_module_lines(text)
class TestModuleTests(unittest.TestCase):
"""Inline test modules don't count; anything else, and anything the rule isn't sure about, does."""
def test_a_test_module_is_skipped_with_the_braces_in_its_literals(self):
self.assertEqual(skipped("fn a() {}\n\n" + TEST_MODULE + "\nfn b() {}\n"), TEST_MODULE.count("\n"))
def test_visibility_and_nesting(self):
text = ("pub mod outer {\n #[cfg(test)]\n pub(crate) mod a {\n fn f() {\n }\n }\n"
" fn real() {}\n}\n#[cfg(test)]\npub mod b {\n #[cfg(test)]\n mod c {\n }\n}\n")
self.assertEqual(skipped(text), 5 + 6)
self.assertEqual(check_file_size.code_lines(text), 3)
def test_windows_line_endings(self):
self.assertEqual(skipped(TEST_MODULE.replace("\n", "\r\n")), TEST_MODULE.count("\n"))
def test_other_test_items_count(self):
for text in ("#[cfg(test)]\nfn helper() {\n}\n", "#[cfg(test)]\nmod tests;\n",
"#[cfg(test)]\nimpl A {\n}\n", "#[cfg(not(test))]\nmod live {\n}\n"):
self.assertEqual(skipped(text), 0, text)
def test_a_module_quoted_in_a_string_counts(self):
quoted = "const SOURCE: &str = r#\"\n#[cfg(test)]\nmod tests {\n}\n\"#;\n"
self.assertEqual(skipped(quoted), 0)
self.assertEqual(skipped("/*\n#[cfg(test)]\nmod tests {\n}\n*/\n"), 0)
def test_a_module_not_laid_out_by_rustfmt_counts(self):
for text in ("#[cfg(test)]\nmod tests {\n fn a() {}\n} // end\nfn real() {}\n}\n",
"#[cfg(test)]\nmod tests {\nfn a() {}\n}\n",
"#[cfg(test)]\nmod tests {\n fn a() {}\n }\nfn real() {\n}\n",
"#[cfg(test)]\nmod tests { fn a() {} }\nfn real() {\n}\n",
"#[cfg(test)]\nmod tests {\n fn a() {}\n } mod live { pub fn real() {}\n}\n",
"#[cfg(test)]\nmod tests {\n fn a() {}\n"):
self.assertEqual(skipped(text), 0, text)
class GitTests(unittest.TestCase):
"""Against a real repository: renames compare with the old name, the working tree counts."""
@@ -49,7 +114,8 @@ class GitTests(unittest.TestCase):
run("init", "-q", "-b", "main")
(root / "big.rs").write_text("x\n" * 3100)
(root / "small.rs").write_text("y\n" * 10)
# UTF-8 that Windows' code page cannot decode (0x9d): `git show` output is read as UTF-8.
(root / "small.rs").write_bytes("// ”\n".encode() * 10)
run("add", ".")
run("commit", "-q", "-m", "base")
run("checkout", "-q", "-b", "work")
@@ -61,6 +127,34 @@ class GitTests(unittest.TestCase):
(root / "moved.rs").write_text("x\n" * 3101)
self.assertEqual(check_file_size.main(["--base", "main"]), 1)
def test_test_modules_dont_count_on_either_side(self):
"""The base (git show) and the working tree count the same way: a file over the limit may grow inside its test
module, but not outside it."""
def source(code, tests):
return "x\n" * code + "#[cfg(test)]\nmod tests {\n" + " t();\n" * tests + "}\n"
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
env = dict(os.environ, GIT_AUTHOR_NAME="t", GIT_AUTHOR_EMAIL="[email protected]",
GIT_COMMITTER_NAME="t", GIT_COMMITTER_EMAIL="[email protected]")
def run(*args):
subprocess.run(["git", *args], cwd=root, check=True, capture_output=True, env=env)
run("init", "-q", "-b", "main")
(root / "big.rs").write_text(source(3100, 200))
run("add", ".")
run("commit", "-q", "-m", "base")
run("checkout", "-q", "-b", "work")
out = io.StringIO()
with mock.patch.object(check_file_size, "ROOT", root), contextlib.redirect_stdout(out):
(root / "big.rs").write_text(source(3100, 300))
run("commit", "-q", "-am", "more tests")
self.assertEqual(check_file_size.main(["--base", "main"]), 0)
(root / "big.rs").write_text(source(3101, 100))
self.assertEqual(check_file_size.main(["--base", "main"]), 1)
self.assertIn("big.rs: 3101 lines without test modules (3100 on the base)", out.getvalue())
if __name__ == "__main__":
unittest.main()
+72
View File
@@ -151,5 +151,77 @@ class CommandLineTests(unittest.TestCase):
self.assertIn("LAUNCHER es ", out)
class ReviewTests(unittest.TestCase):
"""The record of what a person checked: anything else, or anything changed since, is unreviewed."""
def test_only_entries_checked_in_their_present_text_count_as_reviewed(self):
with tempfile.TemporaryDirectory() as tmp:
folder, review = Path(tmp) / "lang", Path(tmp) / "review"
folder.mkdir()
review.mkdir()
write(folder, "en_us.json", '{\n "a": "Apple",\n "b": "Banana",\n "c": "Cherry",\n'
' "d": "Date"\n}\n')
write(folder, "xx.json", '{\n "a": "Manzana",\n "b": "Platano",\n "c": "Cereza",\n'
' "d": ""\n}\n')
# "b" was checked, then its text changed; "z" left the file.
write(review, "xx.json", '{\n "a": "Manzana",\n "b": "Banano",\n "z": "Zeta"\n}\n')
_, rows = lang_coverage.game_rows(folder, set(), review)
self.assertEqual(by_code(rows, "xx")["unreviewed"], ["b", "c"])
self.assertEqual(by_code(rows, "en_us")["unreviewed"], [])
def test_mark_reviewed_records_present_text_in_file_order_and_drops_gone_keys(self):
with tempfile.TemporaryDirectory() as tmp:
game, launcher, review = Path(tmp) / "game", Path(tmp) / "launcher", Path(tmp) / "review"
game.mkdir()
launcher.mkdir()
write(game, "en_us.json", '{\n "a": "Apple",\n "b": "Banana",\n "c": "Cherry"\n}\n')
write(game, "xx_yy.json", '{\n "a": "Manzana",\n "b": "Platano",\n "c": "Cereza"\n}\n')
(review / "game").mkdir(parents=True)
write(review / "game", "xx_yy.json", '{\n "gone": "Ido"\n}\n')
keys = Path(tmp) / "keys.txt"
keys.write_text("a\n\n", encoding="utf-8")
common = ("--game-dir", str(game), "--launcher-dir", str(launcher), "--review-dir", str(review))
code, out, _ = run(*common, "--lang", "xx_yy", "--mark-reviewed", "c", "--keys-file", str(keys))
self.assertEqual(code, 0, out)
record = (review / "game" / "xx_yy.json").read_text(encoding="utf-8")
self.assertEqual(record, '{\n "a": "Manzana",\n "c": "Cereza"\n}\n')
code, out, _ = run(*common, "--game-only", "--lang", "xx_yy", "--unreviewed")
self.assertEqual(code, 0)
self.assertIn("xx_yy: 1 unreviewed", out)
self.assertIn("en_us: Banana", out)
code, _, err = run(*common, "--lang", "xx_yy", "--mark-reviewed", "nosuch")
self.assertEqual(code, 1)
self.assertIn("no key 'nosuch'", err)
def test_mark_reviewed_refuses_what_it_cannot_tell_apart(self):
with tempfile.TemporaryDirectory() as tmp:
game, launcher = Path(tmp) / "game", Path(tmp) / "launcher"
game.mkdir()
launcher.mkdir()
write(game, "en_us.json", '{\n "a": "Apple"\n}\n')
write(game, "xx.json", '{\n "a": "Manzana"\n}\n')
write(launcher, "xx.json", '{\n "Apple": "Manzana"\n}\n')
common = ("--game-dir", str(game), "--launcher-dir", str(launcher),
"--review-dir", str(Path(tmp) / "review"))
for extra, wanted in [(("--lang", "xx"), "--game-only or --launcher-only"),
(("--lang", "xx", "en_us"), "exactly one --lang"),
(("--lang", "en_us"), "English reference"),
(("--lang", "xx", "--game-only"), "needs keys")]:
with self.subTest(extra=extra):
code, _, err = run(*common, *extra, "--mark-reviewed")
self.assertEqual(code, 2)
self.assertIn(wanted, err)
code, _, _ = run(*common, "--lang", "xx", "--launcher-only", "--mark-reviewed", "Apple")
self.assertEqual(code, 0)
self.assertTrue((Path(tmp) / "review" / "launcher" / "xx.json").exists())
def test_every_repository_record_belongs_to_a_lang_file_and_reads(self):
for kind, folder in (("game", lang_coverage.GAME), ("launcher", lang_coverage.LAUNCHER)):
for record in sorted((lang_coverage.REVIEW / kind).glob("*.json")):
with self.subTest(record=record.name, kind=kind):
self.assertTrue((folder / record.name).exists(), f"{record} names no lang file")
self.assertTrue(lang_coverage.reviewed(record))
if __name__ == "__main__":
unittest.main()
+9 -4
View File
@@ -15,6 +15,11 @@ REQUIRED = {
"horizon-macos-x64.zip", "horizon-server-linux-x86_64.tar.gz", "horizon-source.tar.gz",
}
OPTIONAL = ("horizon-windows-arm64.zip", "horizon-linux-musl-x86_64.tar.gz")
# The bash on PATH, as in test_pr_ready: Windows' CreateProcess looks in System32 before PATH, so a bare "bash"
# there is WSL's, which gets none of the steps' env and none of the host's paths.
BASH = shutil.which("bash")
POSIX_BASH = BASH is not None and not (
os.name == "nt" and os.path.dirname(os.path.normcase(BASH)).endswith(os.path.normcase(r"\system32")))
def run_step(containing):
@@ -26,8 +31,8 @@ def run_step(containing):
raise AssertionError(f"no workflow step containing {containing}")
@unittest.skipUnless(shutil.which("bash") and shutil.which("openssl") and shutil.which("sha256sum"),
"release shell checks require bash, OpenSSL and sha256sum")
@unittest.skipUnless(POSIX_BASH and shutil.which("openssl") and shutil.which("sha256sum"),
"release shell checks require a bash that is not WSL's, OpenSSL and sha256sum")
class OptionalReleaseAssetsTests(unittest.TestCase):
def test_sign_and_publish_with_each_optional_archive_combination(self):
for mask in range(4):
@@ -46,7 +51,7 @@ class OptionalReleaseAssetsTests(unittest.TestCase):
env = dict(os.environ, EPHEMERAL="yes", SHA="0123456789abcdef", SHORT="0123456", DATE="2026-11-04",
ALSO_FOR="nightly-2026-11-04-0123456", SOURCE_TAG="nightly-test",
RELEASE_SIGNING_KEY="", RELEASE_SIGNING_KEY_NEXT="")
signed = subprocess.run(["bash", "-e", "-c", run_step("sign_release.sh nightly")],
signed = subprocess.run([BASH, "-e", "-c", run_step("sign_release.sh nightly")],
cwd=root, env=env, text=True, capture_output=True)
self.assertEqual(signed.returncode, 0, signed.stderr)
# `name` or `*name`: sha256sum on Windows (Git Bash) marks binary mode with `*`.
@@ -71,7 +76,7 @@ gh() {
done
}
'''
published = subprocess.run(["bash", "-e", "-c", stubs + run_step("gh release create nightly")],
published = subprocess.run([BASH, "-e", "-c", stubs + run_step("gh release create nightly")],
cwd=root, env=env, text=True, capture_output=True)
self.assertEqual(published.returncode, 0, published.stderr)
files = {Path(name).name for name in (root / "published").read_text().splitlines()}
+14 -2
View File
@@ -191,6 +191,17 @@ class StampTests(unittest.TestCase):
self.assertFalse(stamp.stamp(False))
self.assertEqual(path.read_text(), "/// Since API 8.\n")
def test_stamped_files_are_read_and_written_as_utf8(self):
# ” is UTF-8 that Windows' code page cannot decode (0x9d), as in files the repository's fragments stamp.
self.git("add", ".")
self.git("commit", "-qm", "base")
self.fragment("quote")
path = self.root / stamp.MOD_API
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes("/// “Since” API @API:quote@.\n".encode())
self.assertFalse(stamp.stamp(False))
self.assertEqual(path.read_text(encoding="utf-8"), "/// “Since” API 8.\n")
def test_stamping_leaves_stale_generated_copies_alone(self):
# Old checkouts may still have these files; only the generator's inputs are stamped now.
generated = (
@@ -226,7 +237,8 @@ class StampTests(unittest.TestCase):
# succeed and leave no marker, or the stamp workflow fails or ships unnumbered docs.
repo = self.original
fragments = [p.relative_to(repo) for p in (repo / stamp.FRAGMENTS).glob("*.json")]
comments = {Path(c) for p in fragments for c in json.loads((repo / p).read_text()).get("comments", [])}
comments = {Path(c) for p in fragments
for c in json.loads((repo / p).read_text(encoding="utf-8")).get("comments", [])}
stamped = [stamp.SOURCE, stamp.DOCS, stamp.MOD_API, *comments]
for path in [stamp.CATALOG, *stamped, *fragments]:
if (repo / path).exists():
@@ -235,7 +247,7 @@ class StampTests(unittest.TestCase):
self.git("add", ".")
self.git("commit", "-qm", "base")
self.assertFalse(stamp.stamp(False))
left = {str(p): re.findall(r"@API:\w+@", (self.root / p).read_text())
left = {str(p): re.findall(r"@API:\w+@", (self.root / p).read_text(encoding="utf-8"))
for p in stamped if (self.root / p).exists()}
self.assertEqual({p: m for p, m in left.items() if m}, {})
+111 -6
View File
@@ -6,11 +6,23 @@ stay the same. A renamed file is compared with its old name, and a new file coun
third_party/ and files that say they are generated (`@generated` in their first lines) are not checked. There is no
list of files: nothing to update when a file shrinks.
Inline test modules don't count, on the base or now: a normal build doesn't compile them. A test module is a
`#[cfg(test)]` line directly followed by `mod name {` (or `pub mod`, `pub(crate) mod`...) at the same indentation,
through its closing `}`. Other `#[cfg(test)]` items count. When in doubt the module counts, so the rule can't skip
real code. A small lexer tells code from strings, raw strings, chars and (nested) comments, and two readings of the
end must agree:
- braces: the module ends on the line where the braces in its code close;
- layout: rustfmt indents everything in the module deeper than `mod` and puts the closing `}` alone at its
indentation, so the first later line that starts in code at that indentation or less must be that bare `}`.
Unformatted code, or a lexer mistake, makes them disagree, and the module counts. The two start lines must begin in
code too, so Rust source quoted in a string is no module.
python3 tools/check_file_size.py --base origin/main
"""
import argparse
from pathlib import Path
import re
import subprocess
import sys
@@ -18,16 +30,106 @@ ROOT = Path(__file__).resolve().parent.parent
LIMIT = 3000
DOC = "docs/layers.md#file-size"
EXEMPT_PREFIXES = ("third_party/",)
CFG_TEST = re.compile(r"([ \t]*)#\[cfg\(test\)\]\s*")
TEST_MOD = re.compile(r"([ \t]*)(?:pub(?:\([a-z]+\))? )?mod \w+ \{\s*")
# Where code may stop: a newline, a comment, a raw string (with its b or c prefix), a string, a quote.
TOKEN = re.compile(r"\n|//|/\*|(?<!\w)[bc]?r(#*)\"|\"|'")
COMMENT_EDGE = re.compile(r"/\*|\*/")
BRACE = re.compile(r"[{}]")
STRING_REST = re.compile(r'(?:[^"\\]|\\.)*"', re.S)
def git(*args):
return subprocess.run(["git", *args], capture_output=True, text=True, check=True, cwd=ROOT).stdout
return subprocess.run(["git", *args], capture_output=True, encoding="utf-8", check=True, cwd=ROOT).stdout
def lines(text):
return text.count("\n") + (1 if text and not text.endswith("\n") else 0)
def lex(text):
"""For each line of `text`: whether it starts in code rather than inside a string or a comment, the `{` minus the
`}` in its code, and the lowest that count reaches along the line (`} mod live {` is 0 but dips to -1)."""
starts, braces, lows, pos, end = [True], [0], [0], 0, len(text)
def code(stop):
for brace in BRACE.finditer(text, pos, stop):
braces[-1] += 1 if brace.group() == "{" else -1
lows[-1] = min(lows[-1], braces[-1])
def skip_to(stop):
newlines = text.count("\n", pos, stop)
starts.extend([False] * newlines)
braces.extend([0] * newlines)
lows.extend([0] * newlines)
return stop
while token := TOKEN.search(text, pos):
code(token.start())
kind, pos = token.group(), token.end()
if kind == "\n":
starts.append(True)
braces.append(0)
lows.append(0)
elif kind == "//":
stop = text.find("\n", pos)
pos = end if stop < 0 else stop
elif kind == "/*":
depth, stop = 1, pos
while depth and (edge := COMMENT_EDGE.search(text, stop)):
depth, stop = depth + (1 if edge.group() == "/*" else -1), edge.end()
pos = skip_to(end if depth else stop)
elif kind == "'":
# A char literal ('x', '{', '\'', '\u{7b}') is skipped; a lifetime or label ('a) is not.
if text.startswith("\\", pos):
stop = text.find("'", pos + 2)
pos = end if stop < 0 else stop + 1
elif text.startswith("'", pos + 1):
pos += 2
elif kind == '"':
rest = STRING_REST.match(text, pos)
pos = skip_to(rest.end() if rest else end)
else:
stop = text.find('"' + token.group(1), pos)
pos = skip_to(end if stop < 0 else stop + 1 + len(token.group(1)))
code(end)
return starts, braces, lows
def test_module_end(rows, starts, braces, lows, i):
"""The last line of the test module whose `#[cfg(test)]` is on line `i`, or None where the readings disagree."""
indent, depth = TEST_MOD.fullmatch(rows[i + 1]).group(1), 1 # `mod name {` opens the one brace
for j in range(i + 2, len(rows)):
closed = depth + lows[j] <= 0
depth += braces[j]
if starts[j] and rows[j].strip() and len(rows[j]) - len(rows[j].lstrip()) <= len(indent):
return j if depth == 0 and rows[j].rstrip() == indent + "}" else None
if closed:
return None
return None
def test_module_lines(text):
"""How many lines of `text` are inline `#[cfg(test)] mod name { ... }` modules (see the module docstring)."""
rows, (starts, braces, lows) = text.split("\n"), lex(text)
skipped, i = 0, 0
while i + 1 < len(rows):
cfg, module = CFG_TEST.fullmatch(rows[i]), TEST_MOD.fullmatch(rows[i + 1])
if cfg and module and cfg.group(1) == module.group(1) and starts[i] and starts[i + 1]:
end = test_module_end(rows, starts, braces, lows, i)
if end is not None:
skipped += end - i + 1
i = end + 1
continue
i += 1
return skipped
def code_lines(text):
"""The lines that count toward the limit: all but inline test modules."""
return lines(text) - test_module_lines(text)
def generated(text):
return "@generated" in "\n".join(text.split("\n", 5)[:5])
@@ -57,8 +159,10 @@ def problems(entries, limit=LIMIT):
continue
if now > limit and now > before:
was = f"{before} on the base" if before else "a new file"
out.append(f"{path}: {now} lines ({was}). A file over {limit} lines may not grow: move code out to a "
f"new module ({DOC})")
tests = path.rsplit("/", 1)[-1].removesuffix(".rs") + "_tests.rs"
out.append(f"{path}: {now} lines without test modules ({was}). A file over {limit} lines may not grow. "
f"New tests go into a sibling {tests} (`#[path = \"{tests}\"] mod tests;`); code moves out "
f"to a new module in a pure-move PR first, not inside a fix ({DOC})")
return out
@@ -76,15 +180,16 @@ def main(argv=None):
if not file.exists():
continue
text = file.read_text(encoding="utf-8", errors="replace")
before = lines(git("show", f"{merge_base}:{old}")) if old else 0
entries.append((path, before, lines(text), generated(text)))
before = code_lines(git("show", f"{merge_base}:{old}")) if old else 0
entries.append((path, before, code_lines(text), generated(text)))
found = problems(entries)
if found:
print(f"File size check failed ({DOC}):")
for problem in found:
print(f" {problem}")
return 1
print(f"File size check passed: {len(entries)} changed Rust files, none over {LIMIT} lines grew.")
print(f"File size check passed: {len(entries)} changed Rust files, none over {LIMIT} lines grew "
f"(test modules not counted).")
return 0
+138 -14
View File
@@ -13,6 +13,12 @@ so a file is complete by construction (`launcher/src/i18n/tests.rs` checks every
the code asks for). What it can still hold is a value identical to its English key, and a
`{hole}` the value dropped or added.
Most translations were written by machine. A translated entry is *unreviewed* until a person
who speaks the language has read it: `tools/lang_review/{game,launcher}/<code>.json` records
the text a person wrote or checked, key for key, and any other entry, or one whose text changed
since, counts as unreviewed. Players see no difference; the record is for reviewers
(docs/translations.md).
python3 tools/lang_coverage.py # every file, both folders, most complete first
python3 tools/lang_coverage.py --lang es_es es # only those file stems
python3 tools/lang_coverage.py --game-only
@@ -20,8 +26,12 @@ the code asks for). What it can still hold is a value identical to its English k
python3 tools/lang_coverage.py --show-keys # the keys behind every count
python3 tools/lang_coverage.py --json # the same, for a script
python3 tools/lang_coverage.py --fail-under 95 # exit 1 under that cover (for CI, later)
python3 tools/lang_coverage.py --unreviewed --lang fr_fr # what a French reviewer reads
python3 tools/lang_coverage.py --lang fr_fr --mark-reviewed KEY # a person checked KEY's text
python3 tools/lang_coverage.py --lang fr --mark-reviewed --keys-file checked.txt
It reads files and prints; it writes nothing and needs no `.local/`.
It needs no `.local/`, and writes nothing but the review record, and that only with
`--mark-reviewed`.
"""
import argparse
@@ -34,6 +44,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
GAME = ROOT / "crates/horizon-client/assets/horizon/lang"
LAUNCHER = ROOT / "launcher/lang"
REVIEW = ROOT / "tools/lang_review"
REFERENCE = "en_us.json"
@@ -121,8 +132,27 @@ class LangFile:
"two spaces of indentation")
def game_rows(directory: Path, codes: set[str]) -> tuple[LangFile, list[dict]]:
"""Every language in `directory` against `en_us.json`, most complete first."""
def reviewed(path: Path) -> dict[str, str]:
"""The record of what a person wrote or checked in one lang file: key to the text as it
was checked. No record means no entry is checked yet."""
if not path.exists():
return {}
record = LangFile(path)
if record.problem:
raise ValueError(f"{shown(path)}: {record.problem}")
return record.entries
def unreviewed(entries: dict[str, str], record: dict[str, str]) -> list[str]:
"""The entries no person has checked in their present text. An empty value is a gap, not a
translation, so it is never one."""
return sorted(key for key, value in entries.items() if value.strip() and record.get(key) != value)
def game_rows(directory: Path, codes: set[str],
review: Path | None = None) -> tuple[LangFile, list[dict]]:
"""Every language in `directory` against `en_us.json`, most complete first. With `review`
(a `lang_review/game` folder), each row also says which entries no person checked."""
reference = LangFile(directory / REFERENCE)
if reference.problem:
raise ValueError(f"{shown(reference.path)}: {reference.problem}")
@@ -154,15 +184,18 @@ def game_rows(directory: Path, codes: set[str]) -> tuple[LangFile, list[dict]]:
"placeholders": changed,
"empty": sorted(key for key, value in entries.items() if not value.strip()),
"same": same,
"unreviewed": [] if lang.code == "en_us" else unreviewed(
entries, reviewed(review / path.name) if review else {}),
"problem": lang.problem,
})
rows.sort(key=lambda row: (-row["coverage"], row["code"]))
return reference, rows
def launcher_rows(directory: Path, codes: set[str]) -> list[dict]:
"""Every launcher language: the values still identical to their English key, and the
`{holes}` a value does not match it on. Its key set is the code's job (`i18n/tests.rs`)."""
def launcher_rows(directory: Path, codes: set[str], review: Path | None = None) -> list[dict]:
"""Every launcher language: the values still identical to their English key, the
`{holes}` a value does not match it on, and with `review` (a `lang_review/launcher` folder)
the entries no person checked. Its key set is the code's job (`i18n/tests.rs`)."""
rows = []
for path in sorted(directory.glob("*.json")):
if codes and path.stem not in codes:
@@ -183,6 +216,7 @@ def launcher_rows(directory: Path, codes: set[str]) -> list[dict]:
"same": same,
"holes": changed,
"empty": sorted(key for key, value in lang.entries.items() if not value.strip()),
"unreviewed": unreviewed(lang.entries, reviewed(review / path.name) if review else {}),
"problem": lang.problem,
})
return rows
@@ -197,21 +231,22 @@ def print_problems(rows: list[dict]) -> None:
def print_game(reference: LangFile, directory: Path, rows: list[dict]) -> None:
print(f"GAME {shown(directory)} ({reference.code}: {len(reference.entries)} keys, the reference)")
print()
print(f" {'code':<10}{'keys':>6}{'cover':>8}{'missing':>9}{'extra':>7}{'empty':>7}{'same':>6}")
print(f" {'code':<10}{'keys':>6}{'cover':>8}{'missing':>9}{'extra':>7}{'empty':>7}{'same':>6}"
f"{'unreviewed':>12}")
for row in rows:
print(f" {row['code']:<10}{row['keys']:>6}{100 * row['coverage']:>7.1f}%"
f"{len(row['missing']):>9}{len(row['extra']):>7}{len(row['empty']):>7}"
f"{len(row['same']):>6}{' *' if row['problem'] else ''}")
f"{len(row['same']):>6}{len(row['unreviewed']):>12}{' *' if row['problem'] else ''}")
print_problems(rows)
def print_launcher(directory: Path, rows: list[dict]) -> None:
print(f"LAUNCHER {shown(directory)} (the English sentence is the key)")
print()
print(f" {'code':<10}{'keys':>6}{'same':>7}{'holes':>7}{'empty':>7}")
print(f" {'code':<10}{'keys':>6}{'same':>7}{'holes':>7}{'empty':>7}{'unreviewed':>12}")
for row in rows:
print(f" {row['code']:<10}{row['keys']:>6}{len(row['same']):>7}{len(row['holes']):>7}"
f"{len(row['empty']):>7}{' *' if row['problem'] else ''}")
f"{len(row['empty']):>7}{len(row['unreviewed']):>12}{' *' if row['problem'] else ''}")
print_problems(rows)
@@ -248,17 +283,51 @@ def print_game_summary(reference: LangFile, rows: list[dict]) -> None:
line = (f"GAME {row['code']} {row['keys']}/{len(reference.entries)} keys, "
f"missing {len(row['missing'])}, extra {len(row['extra'])}, "
f"placeholders {len(row['placeholders'])}, empty {len(row['empty'])}, "
f"same {len(row['same'])}")
f"same {len(row['same'])}, unreviewed {len(row['unreviewed'])}")
print(line + (f", {row['problem']}" if row["problem"] else ""))
def print_launcher_summary(rows: list[dict]) -> None:
for row in rows:
line = (f"LAUNCHER {row['code']} {row['keys']} keys, same {len(row['same'])}, "
f"holes {len(row['holes'])}, empty {len(row['empty'])}")
f"holes {len(row['holes'])}, empty {len(row['empty'])}, "
f"unreviewed {len(row['unreviewed'])}")
print(line + (f", {row['problem']}" if row["problem"] else ""))
def print_unreviewed(kind: str, rows: list[dict], english: dict[str, str] | None) -> None:
"""The entries a reviewer reads, with the English beside them (a launcher key is its
English)."""
for row in rows:
if not row["unreviewed"]:
continue
print(f"\n{kind} {row['code']}: {len(row['unreviewed'])} unreviewed ({row['path']})")
text = LangFile(ROOT / row["path"]).entries # ROOT / an absolute path is that path
for key in row["unreviewed"]:
print(f" {key}")
if english is not None:
print(f" en_us: {english.get(key, '')}")
print(f" {row['code']}: {text.get(key, '')}")
def mark_reviewed(lang: Path, record_path: Path, keys: list[str]) -> list[str]:
"""Records `keys` of `lang` as checked by a person, in their present text, and drops the
record's keys the file no longer has. Returns the keys `lang` does not have."""
entries = LangFile(lang).entries
record = {key: text for key, text in reviewed(record_path).items() if key in entries}
unknown = [key for key in keys if key not in entries]
for key in keys:
if key in entries:
record[key] = entries[key]
ordered = {key: record[key] for key in entries if key in record}
if ordered:
record_path.parent.mkdir(parents=True, exist_ok=True)
record_path.write_text(json.dumps(ordered, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
elif record_path.exists():
record_path.unlink()
return unknown
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
@@ -276,12 +345,22 @@ def main(argv: list[str] | None = None) -> int:
parser.add_argument("--json", action="store_true", help="machine output")
parser.add_argument("--fail-under", type=float, metavar="PERCENT",
help="exit 1 when a game language covers less than this")
parser.add_argument("--review-dir", type=Path, default=REVIEW,
help="the record of checked entries, game/ and launcher/ (default: %(default)s)")
parser.add_argument("--unreviewed", action="store_true",
help="list the entries no person checked yet, with their English")
parser.add_argument("--mark-reviewed", nargs="*", metavar="KEY",
help="record these keys of the one --lang file as checked by a person")
parser.add_argument("--keys-file", type=Path,
help="with --mark-reviewed: more keys, one per line (launcher keys are sentences)")
args = parser.parse_args(argv)
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
codes = set(args.lang or ())
if args.mark_reviewed is not None:
return mark_command(args, codes)
if args.fail_under is not None and args.launcher_only:
print("--fail-under measures the game's languages; --launcher-only has none", file=sys.stderr)
return 2
@@ -293,7 +372,7 @@ def main(argv: list[str] | None = None) -> int:
print(f"{args.game_dir}: no such folder", file=sys.stderr)
return 2
try:
reference, game = game_rows(args.game_dir, codes)
reference, game = game_rows(args.game_dir, codes, args.review_dir / "game")
except ValueError as error:
print(error, file=sys.stderr)
return 2
@@ -303,7 +382,11 @@ def main(argv: list[str] | None = None) -> int:
if not args.launcher_dir.is_dir():
print(f"{args.launcher_dir}: no such folder", file=sys.stderr)
return 2
launcher = launcher_rows(args.launcher_dir, codes)
try:
launcher = launcher_rows(args.launcher_dir, codes, args.review_dir / "launcher")
except ValueError as error:
print(error, file=sys.stderr)
return 2
result["launcher"] = {"files": launcher}
if codes and not game and not launcher:
print(f"no lang file is named {' or '.join(sorted(codes))}", file=sys.stderr)
@@ -311,6 +394,10 @@ def main(argv: list[str] | None = None) -> int:
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
elif args.unreviewed:
if reference:
print_unreviewed("GAME", game, reference.entries)
print_unreviewed("LAUNCHER", launcher, None)
elif args.summary:
if reference:
print_game_summary(reference, game)
@@ -337,5 +424,42 @@ def main(argv: list[str] | None = None) -> int:
return 0
def mark_command(args, codes: set[str]) -> int:
"""`--mark-reviewed`: one file, named by `--lang`; --game-only or --launcher-only picks the
folder when the code is in both."""
if len(codes) != 1:
print("--mark-reviewed needs exactly one --lang code", file=sys.stderr)
return 2
code = next(iter(codes))
if code == "en_us":
print("en_us is the English reference, not a translation", file=sys.stderr)
return 2
found = [(kind, folder / f"{code}.json") for kind, folder, skip in (
("game", args.game_dir, args.launcher_only), ("launcher", args.launcher_dir, args.game_only))
if not skip and (folder / f"{code}.json").exists()]
if not found:
print(f"no lang file is named {code}", file=sys.stderr)
return 2
if len(found) > 1:
print(f"{code} is a game and a launcher file: add --game-only or --launcher-only", file=sys.stderr)
return 2
kind, lang = found[0]
keys = list(args.mark_reviewed)
if args.keys_file:
keys += [line for line in args.keys_file.read_text(encoding="utf-8").splitlines() if line.strip()]
if not keys:
print("--mark-reviewed needs keys, on the command line or in --keys-file", file=sys.stderr)
return 2
try:
unknown = mark_reviewed(lang, args.review_dir / kind / lang.name, keys)
except ValueError as error:
print(error, file=sys.stderr)
return 2
for key in unknown:
print(f"{shown(lang)} has no key {key!r}", file=sys.stderr)
print(f"{len(keys) - len(unknown)} entries of {shown(lang)} recorded as reviewed")
return 1 if unknown else 0
if __name__ == "__main__":
raise SystemExit(main())
+3
View File
@@ -0,0 +1,3 @@
{
"horizon.options.three_d_skin_layers": "3D-Skin-Schichten"
}
+6
View File
@@ -0,0 +1,6 @@
{
"Particle Culling": "Отсечение частиц",
"Draws every particle.": "Рисует каждую частицу.",
"Skips offscreen, subpixel and terrain-hidden particles.": "Пропускает частицы за экраном, субпиксельные и скрытые ландшафтом.",
"Skips offscreen, subpixel and terrain-hidden particles with less render-thread work.": "Пропускает частицы за экраном, субпиксельные и скрытые ландшафтом с меньшей нагрузкой на поток рендеринга."
}
+179
View File
@@ -0,0 +1,179 @@
{
"horizon.options.controller": "Kontrolcü",
"horizon.options.vibration": "Titreşim",
"horizon.options.adaptive_triggers": "Uyarlanabilir Tetikleyiciler",
"horizon.options.lightbar": "Işık Çubuğu",
"horizon.options.look_sensitivity": "Bakış Hassasiyeti",
"horizon.options.invert_x": "X Eksenini Ters Çevir",
"horizon.options.invert_y": "Y Eksenini Ters Çevir",
"horizon.options.sneak_button": "Eğilme Tuşu",
"horizon.options.move_deadzone": "Hareket Ölü Bölgesi",
"horizon.options.look_deadzone": "Bakış Ölü Bölgesi",
"horizon.options.button_glyphs": "Düğme Simgeleri",
"horizon.options.on_screen_keyboard": "Ekran Klavyesi",
"horizon.options.chat_delay": "Sohbet Gecikmesi",
"horizon.options.lightbar.health": "Can",
"horizon.options.lightbar.dimension": "Boyut",
"horizon.options.glyphs.auto": "Otomatik",
"horizon.options.glyphs.xbox": "Xbox",
"horizon.options.glyphs.playstation": "PlayStation",
"horizon.options.glyphs.nintendo": "Nintendo",
"horizon.options.keyboard.auto": "Otomatik",
"horizon.options.keyboard.horizon": "Horizon",
"horizon.options.keyboard.steam": "Steam",
"horizon.options.unavailable": "Bu özellik henüz Horizon'da mevcut değil.",
"horizon.options.search": "Ayarlarda ara...",
"horizon.pages.mods": "Modlar",
"horizon.pages.shader_packs": "Gölgelendirici Paketleri",
"horizon.pages.controller": "Kontrolcü Ayarları",
"horizon.pages.controller_binds": "Kontrolcü Tuş Atamaları",
"horizon.pages.extras": "Horizon Ekstraları",
"horizon.pages.settings": "Horizon Ayarları",
"horizon.pages.agent": "Yapay Zekâ Asistanı",
"horizon.pages.settings.tooltip": "Horizon'a özel ayarlar: grafikler, kontrolcüler, ekstralar ve modlar.",
"horizon.pages.graphics.tooltip": "Grafik ayarları. Her ayarın performansa ne kadar yük bindirdiği belirtilir.",
"horizon.pages.graphics_advanced.tooltip": "Horizon'a özel olanlar da dahil tüm grafik ayarları ve bunların performansa etkileri.",
"horizon.pages.controller.tooltip": "Kontrolcüler: düğmeler, analog çubuklar, titreşim ve menü gezinmesi.",
"horizon.pages.extras.tooltip": "İsteğe bağlı özellikler. Varsayılan olarak hepsi kapalıdır; etkinleştirene kadar oyun vanilla hâlinde kalır.",
"horizon.pages.agent.tooltip": "Kendi yapay zekâ asistanınızın oyunu oynamasını sağlayın: Bu bilgisayardaki horizon-ctl ve MCP istemcileri.",
"horizon.pages.shader_packs.tooltip": "shaderpacks klasöründeki gölgelendirici paketleri: Horizon'a özel olanlar ve shaders/ biçimindekiler. Aynı anda yalnızca bir paket çalışabilir.",
"horizon.pages.mods.tooltip": "horizon-mods klasöründeki Horizon modları. Her mod yalıtılmış bir ortamda çalışır ve siz etkinleştirene kadar kapalı kalır.",
"horizon.pages.open.graphics": "Grafikler...",
"horizon.pages.open.graphics_advanced": "Gelişmiş Grafikler...",
"horizon.pages.open.shader_packs": "Gölgelendirici Paketleri...",
"horizon.pages.open.controller": "Kontrolcü Ayarları...",
"horizon.pages.open.extras": "Ekstralar...",
"horizon.pages.open.mods": "Modlar...",
"horizon.pages.open.agent": "Yapay Zekâ Asistanı...",
"horizon.pages.open.horizon_graphics": "Horizon Grafikleri...",
"horizon.pages.open.controller_binds": "Kontrolcü Tuş Atamaları...",
"horizon.pages.open.horizon_settings": "Horizon Ayarları...",
"horizon.mods.empty": "Henüz mod yok. .hmod dosyalarını horizon-mods klasörüne yerleştirin.",
"horizon.mods.stopped": " (Durduruldu)",
"horizon.mods.not_built": " (Derlenmedi)",
"horizon.mods.dev": " (Geliştirici)",
"horizon.mods.settings": "Ayarlar",
"horizon.mods.open_folder": "Mod Klasörünü Aç",
"horizon.agent.copy_details": "Bağlantı Bilgilerini Kopyala",
"horizon.multiplayer.join": "Katıl",
"horizon.multiplayer.cancel_reconnecting": "Yeniden Bağlanmayı İptal Et",
"horizon.multiplayer.join_friend": "Bir Arkadaşa Katıl",
"horizon.multiplayer.invite_code": "Davet Kodu",
"horizon.multiplayer.relay": "Aktarıcı (İsteğe Bağlı)",
"Preset": "Hazır Ayar",
"LOD Distance": "LOD Mesafesi",
"Far Detail": "Uzak Mesafe Ayrıntısı",
"Cave Culling": "Mağara Eleme",
"Entity Culling": "Varlık Eleme",
"Chunk Updates": "Parça Güncellemeleri",
"Upload Budget": "Yükleme Bütçesi",
"Sky": "Gökyüzü",
"Fog": "Sis",
"Weather": "Hava Durumu",
"Animated Textures": "Hareketli Dokular",
"Present Mode": "Sunum Modu",
"Frame Pacing": "Kare Zamanlaması",
"Worker Threads": "İş Parçacıkları",
"Resolution Scale": "Çözünürlük Ölçeği",
"Anti-aliasing": "Kenar Yumuşatma",
"Upscaling": "Yükseltme",
"Paper White": "Referans Beyaz",
"Peak Brightness": "Tepe Parlaklığı",
"Black Level": "Siyah Seviyesi",
"UI Brightness": "Arayüz Parlaklığı",
"Battery Mode": "Pil Modu",
"Graphics API": "Grafik API'si",
"Performance Overlay": "Performans Katmanı",
"Full Detail Distance": "Tam Ayrıntı Mesafesi",
"On": "Açık",
"Off": "Kapalı",
"Unlimited": "Sınırsız",
"Auto": "Otomatik",
"Custom": "Özel",
"Performance": "Performans",
"Balanced": "Dengeli",
"Quality": "Kalite",
"Ultra": "Ultra",
"Ultra Performance": "Ultra Performans",
"Native": "Yerel",
"Full": "Tam",
"Reduced": "Düşük",
"Efficient": "Verimli",
"Precise": "Hassas",
"Auto (VSync)": "Otomatik (VSync)",
"FIFO": "FIFO",
"Mailbox": "Mailbox",
"Immediate": "Anında",
"Vulkan": "Vulkan",
"DirectX 12": "DirectX 12",
"OpenGL": "OpenGL",
"FPS and Graph": "FPS ve Grafik",
"FPS": "FPS",
"TAA": "TAA",
"FXAA": "FXAA",
"No cost": "Performans Etkisi Yok",
"Low cost": "Düşük Performans Maliyeti",
"Medium cost": "Orta Performans Maliyeti",
"High cost": "Yüksek Performans Maliyeti",
"Saves time": "Zaman Kazandırır",
"HDR": "HDR",
"Graphics": "Grafikler",
"Done": "Tamam",
"This feature is not yet available in Horizon.": "Bu özellik henüz Horizon'da mevcut değil.",
"Your display or system doesn't offer HDR output.": "Ekranınız veya sisteminiz HDR çıkışını desteklemiyor.",
"Turn HDR on to use it.": "Bu özelliği kullanmak için HDR'yi etkinleştirin.",
"%s chunks": "%s parça",
"%s per frame": "Kare başına %s",
"%s MiB/frame": "Kare başına %s MiB",
"%s KiB/frame": "Kare başına %s KiB",
"Auto (%s)": "Otomatik (%s)",
"Full-detail terrain range. LOD Distance can extend local worlds farther.": "Arazinin tam ayrıntıyla görüntülendiği mesafe. LOD Mesafesi, yerel dünyalarda bu mesafeyi daha da artırabilir.",
"Coarse distant terrain in local worlds. Integrated GPUs stop at 128 chunks.": "Yerel dünyalarda uzaktaki arazileri daha düşük ayrıntıyla görüntüler. Tümleşik GPU'larda sınır 128 parçadır.",
"Reduced draws far terrain coarser: faster on this integrated GPU.": "Düşük ayarı, uzak arazileri daha düşük ayrıntıyla görüntüler. Bu tümleşik GPU'da daha yüksek performans sağlar.",
"Reduced draws far terrain coarser, for GPUs it slows down.": "Düşük ayarı, uzak arazileri daha düşük ayrıntıyla görüntüler. Bazı GPU'larda performansı düşürebilir.",
"Field of view. Wider shows more terrain at once.": "Görüş alanı. Daha geniş bir açı, aynı anda daha fazla arazi görmenizi sağlar.",
"Caps frames per second; a cap saves power and heat.": "Saniyedeki kare sayısını sınırlar. Bu sınır, güç tüketimini ve ısınmayı azaltır.",
"Waits for the display's refresh: no tearing, a little latency.": "Ekranın yenileme hızını bekler. Görüntü yırtılmasını önler ancak az miktarda gecikme ekler.",
"Size of menus and the HUD.": "Menülerin ve HUD'un boyutu.",
"Sets the options below together. Changing one makes it Custom.": "Aşağıdaki seçenekleri topluca ayarlar. Bu seçeneklerden birini değiştirmeniz durumunda ayar Özel olarak değiştirilir.",
"Lava, fire and sunlight shine brighter than white. Needs an HDR display.": "Lav, ateş ve güneş ışığı beyazdan daha parlak görünür. HDR destekli ekran gerektirir.",
"How bright highlights get. Auto: the peak your system reports.": "Parlak alanların ne kadar parlak görüneceğini belirler. Otomatik: Sisteminizin bildirdiği en yüksek parlaklık değerini kullanır.",
"How bright the HUD and menus are, against paper white.": "HUD ve menülerin, referans beyaz seviyesine kıyasla ne kadar parlak olacağını belirler.",
"How far mobs, crops and machines keep working. CPU time.": "Yaratıkların, ekinlerin ve makinelerin ne kadar uzaktan çalışmaya devam edeceğini belirler. İşlemci gücü kullanır.",
"How far away mobs and items are drawn, as a share of normal.": "Yaratıkların ve eşyaların ne kadar uzaktan görüntüleneceğini, normal mesafeye oranla belirler.",
"Skips terrain hidden behind closed sections. Off costs a lot.": "Kapalı alanların arkasında kalan arazilerin çizilmesini önler. Kapalı olması ciddi performans kaybına neden olur.",
"Meshed sections uploaded per frame. Lower is smoother while loading.": "Her karede GPU'ya yüklenen oluşturulmuş bölüm sayısı. Daha düşük değerler yükleme sırasında daha akıcı bir deneyim sağlar.",
"Soft shadows in corners. Costs chunk building time only.": "Köşelerde yumuşak gölgeler oluşturur. Yalnızca parça oluşturma süresini etkiler.",
"Smaller copies of block textures for distant terrain: less shimmer.": "Uzak arazilerde blok dokularının daha küçük kopyalarını kullanır. Titreşimli görüntüyü azaltır.",
"Blends grass and water colours across biomes when building chunks.": "Parçalar oluşturulurken biyomlar arasındaki çimen ve su renklerini harmanlar.",
"Cloud layer: off, flat (fast) or 3D (fancy).": "Bulut katmanı: Kapalı, düz (hızlı) veya 3B (gelişmiş).",
"How far clouds reach, in chunks.": "Bulutların kaç parça uzağa kadar görüntüleneceğini belirler.",
"Draws the sky, sun, moon and stars.": "Gökyüzünü, Güneş'i, Ay'ı ve yıldızları görüntüler.",
"Distance fog. Off shows the edge of the loaded world.": "Uzaklık sisi. Kapalı olduğunda yüklenen dünyanın sınırları görünür.",
"Draws rain and snow.": "Yağmuru ve karı görüntüler.",
"How far rain and snow fall around you.": "Yağmurun ve karın çevrenizde ne kadar uzağa kadar görüntüleneceğini belirler.",
"How many particles effects spawn.": "Oluşturulacak parçacık efektlerinin miktarını belirler.",
"Round shadows under mobs and items.": "Yaratıkların ve eşyaların altında yuvarlak gölgeler oluşturur.",
"Leaves with see-through gaps. Off draws them solid.": "Yaprakları aralarında boşluklar olacak şekilde görüntüler. Kapalı olduğunda yapraklar tamamen dolu görünür.",
"Sorts water and glass more exactly behind each other.": "Su ve cam yüzeylerin birbirlerinin arkasında doğru sırayla görüntülenmesini sağlar.",
"Which chunk rebuilds wait for the frame: none, nearby or yours.": "Kare tamamlanana kadar hangi parça yeniden oluşturma işlemlerinin bekleyeceğini belirler: Hiçbiri, yakındakiler veya bulunduğunuz parçalar.",
"Animates water, lava, fire and other moving textures.": "Su, lav, ateş ve diğer hareketli dokuları canlandırır.",
"World pixels per screen pixel. Below 100% is faster on the GPU.": "Ekrandaki her piksele karşılık gelen dünya pikseli sayısını belirler. %100'ün altındaki değerler GPU performansını artırır.",
"Smooths jagged edges. FXAA: softer. TAA: smoothest, moving things may trail.": "Pürüzlü kenarları yumuşatır. FXAA: Daha yumuşak görüntü. TAA: En iyi yumuşatma, ancak hareketli nesnelerin arkasında iz bırakabilir.",
"Renders the world smaller and rebuilds it with TAA. Faster, softer.": "Dünyayı daha düşük çözünürlükte işler ve TAA kullanarak yeniden oluşturur. Daha hızlı ancak daha yumuşak bir görüntü sağlar.",
"HDR output on an HDR display: lava, fire and sunlight shine brighter.": "HDR destekli bir ekranda HDR görüntü çıkışı sağlar. Lav, ateş ve güneş ışığı daha parlak görünür.",
"How frames reach the screen while VSync is off.": "VSync kapalıyken karelerin ekrana nasıl aktarılacağını belirler.",
"Efficient sleeps until the next frame; Precise spins, like vanilla.": "Verimli modu bir sonraki kareye kadar bekler. Hassas modu ise vanilla Minecraft gibi aktif olarak bekleme döngüsü kullanır.",
"Fills the screen (borderless).": "Ekranı tamamen kaplar (kenarlıksız).",
"Brightens dark places.": "Karanlık alanları aydınlatır.",
"Lowers the frame rate when minimised, or idle too.": "Oyun simge durumuna küçültüldüğünde veya boşta kaldığında FPS'yi düşürür.",
"Sways the view while walking.": "Yürürken görüş açısını sallandırır.",
"Fades newly loaded chunks in.": "Yeni yüklenen parçaların aşamalı olarak görünmesini sağlar.",
"Darkens the screen's edges.": "Ekranın kenarlarını karartır.",
"Shows when the world saves.": "Dünyanın ne zaman kaydedildiğini gösterir.",
"Shows attack cooldown.": "Saldırı bekleme süresini gösterir.",
"60 FPS cap (30 in menus or unfocused), half the workers: less power.": "FPS sınırı 60'tır (menülerde veya pencere odakta değilken 30). İş parçacıklarının yarısını kullanır ve daha az güç tüketir.",
"Threads that generate, light and mesh chunks. Next start.": "Parçaları oluşturan, aydınlatan ve ağ verilerini hazırlayan iş parçacıklarının sayısı. Değişiklikler sonraki başlatmada uygulanır.",
"Vulkan, then DirectX 12 (Windows) or OpenGL (Linux) where it fails. Next start.": "Önce Vulkan kullanılır. Çalışmazsa Windows'ta DirectX 12, Linux'ta OpenGL denenir. Değişiklikler sonraki başlatmada uygulanır.",
"Mesh data sent to the GPU per frame. Lower is smoother while loading.": "Her karede GPU'ya gönderilen ağ verisi miktarı. Daha düşük değerler yükleme sırasında daha akıcı bir deneyim sağlar."
}
+250
View File
@@ -0,0 +1,250 @@
{
"Its blocks and items came over and passed Horizon's checks; the mod's own code is not run.": "Její bloky a předměty byly přeneseny a prošly kontrolami Horizonu; vlastní kód modu se nespouští.",
"Part of it came over, but something is left out, differs from the original or has not been checked yet; the technical detail says what.": "Část byla přenesena, ale něco chybí, liší se od originálu nebo dosud nebylo zkontrolováno; podrobnosti najdete v technickém detailu.",
"It does not come over yet.": "Tato položka se zatím nepřenáší.",
"Nothing to port: Horizon has it built in or does not need it.": "Není co přenášet: Horizon má tuto funkci integrovanou, nebo ji nevyžaduje.",
"Horizon cannot tell yet what this mod does (it needs its jar or a reviewed entry), so it cannot say whether it will come over.": "Horizon zatím nedokáže určit, co tento mod dělá (vyžaduje jeho soubor .jar nebo schválený záznam), proto nelze říci, zda jej půjde přenést.",
"It joins other mods together, so it waits until those are ported.": "Propojuje ostatní mody mezi sebou, takže čeká, až budou přeneseny i ty.",
"Content only (blocks, items, structures). Its jar is here, but it has not been carried over yet.": "Pouze obsah (bloky, předměty, struktury). Soubor .jar je k dispozici, ale dosud nebyl přenesen.",
"Content only (blocks, items, structures). It can come over once its jar is here.": "Pouze obsah (bloky, předměty, struktury). Lze jej přenést, jakmile bude k dispozici jeho soubor .jar.",
"Needs what blocks and items do when used, placed, broken or ticked, which Horizon's mod API cannot run from a mod yet, so it does not come over.": "Vyžaduje chování bloků a předmětů při použití, položení, zničení nebo aktualizaci (tiku), které modovací API Horizonu zatím z modu neumí spustit, proto jej nelze přenést.",
"Needs machine-style features (block entities, menus, item, fluid and energy handling, networking) that Horizon's mod API does not have yet, so it does not come over.": "Vyžaduje strojové funkce (blokové entity, nabídky, manipulaci s předměty, tekutinami a energií, síťovou komunikaci), které modovací API Horizonu dosud nemá, proto jej nelze přenést.",
"Needs mobs, custom rendering and models, dimensions or new game hooks that Horizon's mod API does not have yet, so it does not come over.": "Vyžaduje moby, vlastní vykreslování a modely, dimenze nebo nové herní hooky, které modovací API Horizonu dosud nemá, proto jej nelze přenést.",
"Needs the game's client interfaces (keys, targeting, inventory reading, interface, rendering), so it does not come over yet.": "Vyžaduje klientská rozhraní hry (klávesy, zaměřování, čtení inventáře, uživatelské rozhraní, vykreslování), proto jej zatím nelze přenést.",
"Horizon has not worked out what this mod needs yet.": "Horizon dosud nezjistil, co tento mod vyžaduje.",
"A library other mods build on. Horizon's mod API and SDK take its place, so there is nothing to port.": "Knihovna, na které staví ostatní mody. Její místo zastupuje modovací API a SDK Horizonu, takže není co přenášet.",
"It makes the game faster. Horizon's own engine does that, so there is nothing to port.": "Zrychluje hru. O to se stará vlastní engine Horizonu, takže není co přenášet.",
"Pack tooling or a game feature that Horizon already has, so there is nothing to port.": "Nástroj pro balíčky nebo herní funkce, kterou Horizon již obsahuje, takže není co přenášet.",
"Built into Horizon already, so there is nothing to port.": "Již integrováno v Horizonu, takže není co přenášet.",
"Nothing to port.": "Není co přenášet.",
"Made for Minecraft 1.20 or older: Horizon ports mods made for 1.21 and later.": "Vytvořeno pro Minecraft 1.20 nebo starší: Horizon přenáší pouze mody vytvořené pro verzi 1.21 a novější.",
"Shader packs are not converted yet.": "Balíčky stínovačů (shadery) zatím nelze převést.",
"Copied as a resource pack; Horizon has not checked how it renders. Turn it on in the game's Resource Packs screen.": "Zkopírováno jako balíček zdrojů; Horizon zatím neověřil způsob vykreslování. Aktivujte jej v nabídce Balíčky zdrojů přímo ve hře.",
"Not downloaded yet; Port gets it.": "Dosud nezataženo; funkce Převést jej stáhne.",
"Horizon could not use this resource pack yet; the technical detail says why.": "Horizon tento balíček zdrojů zatím nemohl použít; důvod najdete v technickém detailu.",
"It also waits for 1 mod it depends on that is not ported yet.": "Čeká také na 1 mod, na kterém závisí a který dosud nebyl přenesen.",
"It also waits for {count} mods it depends on that are not ported yet.": "Čeká také na {count} modů, na kterých závisí a které dosud nebyly přeneseny.",
"{carried} of {total} mods that need porting come over as content (blocks, items and structures); their own code does not run.": "{carried} z {total} modů vyžadujících přenesení se převedou jako obsah (bloky, předměty a struktury); jejich vlastní kód se nespouští.",
"Not yet: {ready} are content mods Port can carry over, {blocked} need mod support Horizon does not have yet (machines, menus, behaviour, mobs, dimensions, client features), {later} are not sorted yet (not identified, or waiting on other mods or other work).": "Zatím ne: {ready} jsou obsahové mody, které funkce Převést dokáže přenést, {blocked} vyžadují podporu modů, kterou Horizon dosud nemá (stroje, nabídky, chování, moby, dimenze, klientské funkce) a {later} dosud nejsou roztříděny (nejsou identifikovány nebo čekají na jiné mody či další práce).",
"Partial means part of a mod came over, but something is left out, differs from the original or has not been checked yet.": "Částečně znamená, že část modu byla přenesena, ale něco chybí, liší se od originálu nebo dosud nebylo zkontrolováno.",
"Performance mode": "Režim výkonu",
"Smoother on older computers: a lower-resolution sky, a cheaper blur, window resizes baked in the background and no frame per mouse move.": "Plynulejší chod na starších počítačích: obloha v nižším rozlišení, méně náročné rozostření, změna velikosti okna předpočítávaná na pozadí a vynechání vykreslování snímku při každém pohybu myší.",
"Enter a cache limit in GiB; 0 disables automatic cleanup.": "Zadat limit mezipaměti v GiB; 0 vypne automatické čištění.",
"Building {name}'s post-build step": "Krok sestavení {name} po sestavení",
"Prepare": "Připravit",
"Builds from GitHub. Each one installs into its own folder.": "Sestavuje se z GitHubu. Každý se nainstaluje do své vlastní složky.",
"Play with {count} mods": "Hrajete s {count} mody",
"Open Tailscale": "Otevřít Tailscale",
"INSTANCES": "INSTANCE",
"Show everything": "Zobrazit vše",
"Add a native mod from GitHub": "Přidat nativní mod z GitHubu",
"Restart now": "Restartovat nyní",
"Mods and shader packs made for Horizon from its own reviewed registry, and resource packs from Modrinth. Each file is checked against its source's checksum before it reaches your game folder.": "Mody a balíčky shaderů vytvořené pro Horizon z jeho vlastního revidovaného registru a balíčky zdrojů od Modrinth. Každý soubor je zkontrolován podle kontrolního součtu zdroje, než se dostane do vaší herní složky.",
"Working…": "Pracuji…",
"Prepare again": "Připravit znovu",
"{file} is on your desktop: it plays {name}.": "Soubor {file} je na vaší ploše: spouští {name}.",
"About": "O aplikaci",
"Working": "Pracuji",
"Your own agent (Claude Code, Codex, Cursor or any MCP client) can drive the launcher and the game it starts, with the same commands as the terminal. It runs on your computer and talks to Horizon there; it asks before deleting anything.": "Váš vlastní agent (Claude Code, Codex, Cursor nebo jakýkoli klient MCP) může ovládat launcher i spuštěnou hru pomocí stejných příkazů jako terminál. Běží přímo ve vašem počítači, kde komunikuje s Horizonem, a před smazáním čehokoli se vždy zeptá.",
"Building": "Sestavování",
"Check again": "Zkontrolovat znovu",
"Mesa's DRI_PRIME=1, so Vulkan picks the other card.": "Mesa má DRI_PRIME=1, takže Vulkan vybere jinou kartu.",
"This moves the instance to Horizon {version}. It compiles the game from scratch, so it takes longer than a normal rebuild: up to 30 minutes.": "Tím se instance přesune do Horizonu {version}. Kompiluje hru od začátku, takže trvá déle než normální přestavba: až 30 minut.",
"All": "Vše",
"Dev build": "Vývojové sestavení",
"Release build": "Vydané sestavení",
"For example nice -n 5": "Například nice -n 5",
"Jars: {summary}.": "Soubory JAR: {summary}.",
"Not prepared. Horizon can download Minecraft {version} from Mojang for your account, or use your own install.": "Není připraven. Horizon si může stáhnout Minecraft {version} od Mojangu pro váš účet nebo použít vlastní instalaci.",
"{instance} is building: one build at a time.": "{instance} se sestavuje: lze provádět pouze jedno sestavení naráz.",
"These mods were made for different Horizon versions": "Tyto mody byly vytvořeny pro jiné verze Horizonu.",
"Rebuild on Horizon {version}": "Opětovné sestavení na Horizonu {version}",
"Page": "Stránka",
"Delete it": "Smazat",
"HIDE": "SKRÝT",
"Open logs folder": "Otevřít složku s logy",
"Support the work below": "Podpořte tvorbu níže",
"Skin…": "Vzhled…",
"Running": "Spuštěno",
"Account": "Účet",
"Show the opening": "Zobrazit úvodní obrazovku",
"Not built": "Nesestaveno",
"My instance": "Moje instance",
"Join": "Připojit se",
"Nothing found that runs in Horizon. Show everything lists the rest.": "Nebylo nalezeno nic, co by běželo v Horizonu. Zobrazit vše uvádí zbytek.",
"Horizon never turns on Funnel: the server isn't put on the public internet, only shared with the people you invite.": "Horizon nikdy nezapne Funnel: server není umístěn na veřejný internet, je sdílen pouze s lidmi, které pozvete.",
"Port gets the jars of the {count} mods this computer can port today (content with data and assets only) from the pack's own links, Modrinth, or your folder, and carries them over. The ports stay here, in {folder}.": "Funkce Převést stáhne soubory .jar celkem {count} modů, které lze dnes na tomto počítači přenést (pouze obsah s daty a texturami), z vlastních odkazů balíčku, z Modrinthu nebo z vaší složky a převede je. Přenesené mody zůstanou zde, ve složce {folder}.",
"Checking its jar…": "Kontrola JAR souboru…",
"Use these jars": "Použijte tyto JAR soubory",
"Retry": "Zkusit znovu",
"Choose": "Vybrat",
"Building horizon-mixins (once)": "Budování horizon-mixins (jednou)",
"Needs rebuild": "Potřebuje přestavět",
"The source at commit {commit}.": "Zdroj v commitu {commit}.",
"Saved: the next Play {what}.": "Uloženo: příští hraní {what}.",
"Mods": "Mody",
"Website": "Webové stránky",
"Copy link": "Kopírovat odkaz",
"Open folder": "Otevřít složku",
"Open the web panel": "Otevřít webový panel",
"None needed: Horizon downloads Minecraft from Mojang for your account.": "Není potřeba: Horizon stáhne Minecraft od Mojangu pro váš účet.",
"Patch notes": "Poznámky k záplatám",
"Downloading Minecraft {version} from Mojang · {done} of {total} files": "Stahování Minecraft {version} od Mojangu · {done} z {total} souborů",
"Plays {build}, built on Horizon {version}.": "Hraje {build}, postavený na Horizonu {version}.",
"Horizon's violet by default. Every colour keeps text as easy to read.": "Horizon je standardně fialová. Každá barva udržuje text tak snadno čitelný.",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Krajina Horizonu. Vyberte si vlastní obrázek PNG, JPEG nebo WebP.",
"Run the game through": "Spustit hru přes",
"Saved: the next Play uses at most {mb} MB.": "Uloženo: příští hraní zabere maximálně {mb} MB.",
"Restart": "Restartovat",
"Open the folder": "Otevřít složku",
"Release": "Vydání",
"{instance} doesn't play its native mods yet: build the game with them.": "{instance} zatím nehraje své nativní mody: sestavte hru s nimi.",
"Use jars from my instance…": "Použijte JAR soubory z mé instance…",
"Not prepared. Play downloads Minecraft {version} from Mojang for your account and prepares it.": "Není připraven. Play stáhne Minecraft {version} od Mojangu pro váš účet a připraví jej.",
"Copy report": "Kopírovat zprávu",
"Open backups": "Otevřít zálohy",
"Check now": "Zkontrolovat nyní",
"Start": "Spustit",
"Check for updates": "Zkontrolovat aktualizace",
"Stop": "Ukončit",
"Modded": "Modifikované",
"Stay": "Zůstat",
"Update this launcher with your package manager.": "Aktualizujte tento launcher pomocí správce balíčků.",
"Close this launcher, move it to {folder}, and open it there to enable updates.": "Pro zapnutí aktualizací zavřete tento launcher, přesuňte jej do {folder} a tam jej znovu otevřete.",
"Forget": "Zapomenout",
"Follow": "Sledovat",
"Port {count} mods": "Přenést {count} modů",
"the port": "port",
"Build anyway": "Přesto sestavit",
"Open the admin console": "Otevřít administrátorskou konzoli",
"From Horizon's registry": "Z registru Horizonu",
"Vanilla": "Vanilla",
"Downloading from Mojang · {done} of {total} files": "Stahování od Mojangu · {done} z {total} souborů",
"A link to a repository (its latest release), a release, a branch or tag, or a commit. The launcher installs one exact release file or commit, never a branch that moves.": "Odkaz na úložiště (jeho nejnovější vydání), vydání, větev nebo značku nebo commit. Spouštěč nainstaluje přesně jeden soubor vydání nebo commit, nikdy se nepohybuje větev.",
"Add mods": "Přidat mody",
"Back up": "Zálohovat",
"Build": "Sestavit",
"Kick": "Vyhodit",
"Use a number from 1024 to 65535 (25565 is Minecraft's own).": "Použít číslo od 1024 do 65535 (25565 je vlastní Minecraft).",
"Built in {duration} ({profile}, {plan})": "Sestaveno {duration} ({profile}, {plan})",
"first build": "první sestavení",
"combat": "boj",
"economy": "ekonomika",
"equipment": "vybavení",
"management": "správa",
"mobs": "mobové",
"storage": "úložiště",
"vanilla like": "podobné vanilla",
"{path} already exists. Move it away, or build into another folder.": "{path} již existuje. Přesuňte jej nebo sestavte do jiné složky.",
"{path} is not a folder to build into.": "{path} není složka pro sestavení.",
"Unsigned: you picked these programs yourself, and Horizon can't check where they came from. Updates never replace them.": "Nepodepsáno: tyto programy jste si vybrali sami a Horizon nemůže zkontrolovat, odkud pocházejí. Aktualizace je nikdy nenahradí.",
"Launch Horizon": "Spustit Horizon",
"Join the Discord": "Připojit se k Discordu",
"Create a desktop shortcut": "Vytvořit zástupce na ploše",
"Remove the desktop shortcut": "Odebrat zástupce na ploše",
"Copy the launch command": "Kopírovat příkaz ke spuštění",
"Open the instance folder": "Otevřít složku instance",
"Open the screenshots folder": "Otevřít složku snímků obrazovky",
"Playing for {time} now. Have fun!": "Hrajete už {time}. Bavte se!",
"Patch log": "Protokol záplat",
"Resource pack · on": "Balíček zdrojů · zapnuto",
"Resource pack · off": "Balíček zdrojů · vypnuto",
"Shader pack · on": "Balíček shaderů · zapnuto",
"Shader pack · off": "Balíček shaderů · vypnuto",
"1 pack in {name}": "1 balíček v {name}",
"For older packs": "Pro starší balíčky",
"Horizon": "Horizon",
"Save changes": "Uložit změny",
"Take it off": "Sundat",
"Show my cosmetics": "Zobrazit moji kosmetiku",
"Others look like plain players to you. Also in the game's Horizon settings.": "Ostatní vám připadají jako obyčejní hráči. Také v nastavení Horizonu hry.",
"Off by default. Also in the game's Horizon settings.": "Ve výchozím nastavení vypnuto. Také v nastavení Horizonu hry.",
"Open the logs": "Otevřít protokoly",
"Prepare the game files": "Připravit soubory hry",
"Read it": "Přečíst",
"Horizon's Nightly builds, newest first. Your worlds live in the game folder, so removing a version never touches them.": "Noční sestavení Horizonu, nejnovější. Vaše světy žijí ve složce hry, takže odstranění verze se jich nikdy nedotkne.",
"Use": "Použít",
"Could not open the data folder: {error}": "Nepodařilo se otevřít složku s daty: {error}",
"Choose an empty folder for Horizon's data.": "Vyberte prázdnou složku pro data Horizonu.",
"Choose data folder": "Vybrat složku s daty",
"Close the game and servers, and wait for downloads, builds, imports and sign-in to finish before moving data.": "Před přesunem dat zavřete hru i servery a počkejte na dokončení stahování, sestavování, importů a přihlášení.",
"Could not choose data folder: {error}": "Složku s daty se nepodařilo vybrat: {error}",
"Could not start data move: {error}": "Přesun dat se nepodařilo zahájit: {error}",
"Data folders are set by HORIZON_LAUNCHER_HOME or HORIZON_HOME. Remove these environment overrides and restart the launcher before moving data here.": "Datové složky jsou určeny proměnnými HORIZON_LAUNCHER_HOME nebo HORIZON_HOME. Před přesunem dat sem odstraňte tato přepsání prostředí a restartujte launcher.",
"Data move failed: {error}": "Přesun dat selhal: {error}",
"Data moved. Old folders still use disk space: {paths}. Verify your worlds and instances before removing them yourself.": "Data byla přesunuta. Původní složky stále zabírají místo na disku: {paths}. Před jejich ručním smazáním raději ověřte své světy a instance.",
"Move all data to {path}?": "Přesunout všechna data do {path}?",
"Move and restart": "Přesunout a restartovat",
"Moves all launcher, game and build data, including settings, worlds, instances and installed versions. Choose an empty folder with enough free space.": "Přesune veškerá data launcheru, hry a sestavení, včetně nastavení, světů, instancí a nainstalovaných verzí. Vyberte prázdnou složku s dostatkem volného místa.",
"Move…": "Přesunout…",
"The launcher closes while your data is copied and verified, then reopens. Old folders are kept and still use disk space until you verify the move and remove them yourself.": "Během kopírování a ověřování dat se launcher zavře a poté znovu otevře. Původní složky zůstanou zachovány a budou zabírat místo na disku, dokud přesun neověříte a nesmažete je ručně.",
"Automatic follows this launcher's architecture. On Windows ARM, choose x64 or ARM64. Releases without ARM64 use the signed x64 build.": "Možnost „Automaticky“ se řídí architekturou tohoto launcheru. Na systému Windows ARM zvolte x64 nebo ARM64. Vydání bez podpory ARM64 využijí podepsané sestavení x64.",
"Game architecture": "Architektura hry",
"Install selected build": "Nainstalovat vybrané sestavení",
"Manual game architectures are available on Windows ARM only.": "Ruční volba architektury hry je k dispozici pouze na systému Windows ARM.",
"Native mods are not supported by the Windows ARM64 launcher yet; its managed compiler is not available.": "Nativní mody zatím nejsou launcherem pro Windows ARM64 podporovány; jeho spravovaný překladač není k dispozici.",
"Native mods cannot build for the selected ARM64 game yet. The available compiler builds x64 only.": "Nativní mody zatím nelze sestavit pro vybranou ARM64 hru. Dostupný překladač dokáže sestavovat pouze pro x64.",
"Native mods cannot build for the selected x64 game yet. This ARM64 launcher has no matching compiler.": "Nativní mody zatím nelze sestavit pro vybranou x64 hru. Tento launcher pro ARM64 nemá odpovídající překladač.",
"Say auto, x64 or arm64 for the game architecture.": "Zadejte „auto“, „x64“ nebo „arm64“ pro architekturu hry.",
"This version has a {installed} game build, but Settings selects {selected}. Install the selected build from Versions before launching.": "Tato verze má nainstalováno sestavení hry {installed}, ale v Nastavení je vybráno {selected}. Před spuštěním nainstalujte vybrané sestavení ze záložky Verze.",
"Automatic": "Automaticky",
"{tag} has no build for the selected game architecture.": "{tag} nemá žádné sestavení pro vybranou architekturu hry.",
"Unknown": "Neznámé",
"Run `horizon-launcher install latest` to get one.": "Pro získání spusťte příkaz `horizon-launcher install latest`.",
"There is already an instance called {name}. Choose another name.": "Instance s názvem {name} již existuje. Zvolte jiný název.",
"Open Settings and sign in under Account.": "Otevřete Nastavení a přihlaste se v sekci Účet.",
"Sign in with `horizon-launcher account sign-in`.": "Přihlaste se pomocí příkazu `horizon-launcher account sign-in`.",
"The Founder Set is yours.": "Zakladatelská sada je vaše.",
"{file} unpacks to more than {limit}: it isn't imported.": "Soubor {file} se rozbalí na více než {limit}, proto nebyl importován.",
"Horizon's Vulkan renderer crashed while starting on this graphics card, so the game now uses {api}. Press Play to start it again. Video Settings > Graphics API switches back.": "Vykreslování přes Vulkan v Horizonu při spouštění na této grafické kartě selhalo, proto hra nyní používá {api}. Stiskněte Spustit pro opětovné spuštění. Přepnout zpět můžete v Nastavení videa > Grafické API.",
"This Nightly's title ({title}) doesn't name its date and commit as Horizon signs them, so it can't be checked: it was not installed.": "Název tohoto Nightly sestavení ({title}) neobsahuje datum a commit ve formátu podepsaném Horizonem, takže jej nelze ověřit. Sestavení nebylo nainstalováno.",
"{tag} is older than {newest}, which is installed: installing it as a rollback. The selection only moves to it when you pick it.": "Verze {tag} je starší než nainstalovaná verze {newest}: probíhá instalace jako návrat k starší verzi (rollback). Výběr se na ni přepne až po jejím zvolení.",
"Native mods require supported Windows architectures that match. Game: {game_arch}; build toolchain: {toolchain_arch}.": "Nativní mody vyžadují podporované a odpovídající architektury Windows. Hra: {game_arch}; nástroje pro sestavení (toolchain): {toolchain_arch}.",
"Update registry mods automatically": "Automaticky aktualizovat mody z registru",
"At start and before you play, what you installed from Horizon's registry moves to its newest version for this game. Never inside a modpack, and an update that would remove blocks or items waits for you.": "Při spuštění a před hraním se obsah nainstalovaný z registru Horizonu aktualizuje na nejnovější verzi pro tuto hru. K tomu nikdy nedochází uvnitř modpacku a aktualizace, které by odstranily bloky nebo předměty, počkají na váš souhlas.",
"Updated automatically: {list}.": "Automaticky aktualizováno: {list}.",
"{title} wasn't updated: {error}": "Položku {title} se nepodařilo aktualizovat: {error}",
"Builds on the Horizon you play": "Sestavuje se na verzi Horizonu, kterou hrajete",
"Needs native hook points v{needs}, the Horizon you play has v{has}: update Horizon first": "Jsou vyžadovány nativní hook body verze {needs}, ale vámi hraný Horizon má verzi {has}: nejdříve aktualizujte Horizon.",
"Made for Horizon {pin}, but {mods} pin another commit: the build will ask you to Build anyway and may not compile": "Vytvořeno pro Horizon {pin}, ale {mods} vyžadují jiný commit: sestavení se vás zeptá, zda přesto chcete sestavit, a možná se zkompilování nezdaří.",
"{title} {version} waits for you: it no longer has {ids}, which your worlds may use. Update it on the Mods page when you're ready.": "Čeká na vás {title} {version}: již neobsahuje {ids}, které mohou vaše světy využívat. Až budete připraveni, aktualizujte ji na záložce Mody.",
"Windows' Smart App Control is blocking the build (os error 4551). It only lets signed programs run, and a native build runs programs no one has signed: the ones it compiles on this computer and some of the compiler's own. To build native mods, turn it off in Windows Security > App & browser control > Smart App Control settings, then build again. On some Windows versions it can't be turned back on without reinstalling Windows. Content mods (.hmod) need no build and work with it on.": "Chytré řízení aplikací (Smart App Control) ve Windows blokuje sestavení (systémová chyba 4551). Povoluje pouze spouštění podepsaných programů, ale nativní sestavení spouští i nepodepsané programy: ty, které sám zkompiluje na tomto počítači, a také některé součásti samotného překladače. Pro sestavení nativních modů tuto funkci vypněte v nabídce Zabezpečení Windows > Řízení aplikací a prohlížeče > Nastavení Chytrého řízení aplikací a poté sestavení zopakujte. V některých verzích Windows nelze tuto funkci znovu zapnout bez reinstalace systému. Obsahové mody (.hmod) sestavení nevyžadují a fungují i s zapnutým řízením.",
"Claim the founder set": "Nárokovat zakladatelskou sadu",
"Doesn't build on the Horizon you play ({code}): a build leaves it out until its author updates it": "Nesestaví se na Horizonu, který hrajete ({code}): sestavení ho vynechá, dokud ho autor neaktualizuje",
"{source} isn't set up.": "Zdroj {source} není nastaven.",
"Installed {mods}, checksums checked.": "Nainstalováno: {mods}, kontrolní součty ověřeny.",
"Pick the world the data pack goes into first.": "Nejprve vyberte svět, do kterého má být datový balíček vložen.",
"Put {pack} {version} in {world}'s data packs, checksum checked: the game turns it on when the world opens. {coverage}": "Balíček {pack} {version} byl vložen do datových balíčků světa {world}, kontrolní součet ověřen: hra jej aktivuje při otevření světa. {coverage}",
"Installed {pack} {version} as its own profile with {mods}, each checked. Play it from the list above.": "Balíček {pack} {version} byl nainstalován jako samostatný profil s {mods}, vše bylo ověřeno. Spusťte jej ze seznamu výše.",
"Play it from the list above.": "Spusťte jej ze seznamu výše.",
"Installed {mods} into {instance}. Build to play with it.": "Mody {mods} byly nainstalovány do instance {instance}. Pro hraní proveďte sestavení.",
"Installed {mods} into horizon-mods and turned it on, checksum checked (Options > Mods in the game).": "Mody {mods} byly nainstalovány do horizon-mods a aktivovány, kontrolní součet ověřen (v nabídce Možnosti > Mody ve hře).",
"Ported {mods} into horizon-mods and turned on (Options > Mods in the game).": "Mody {mods} byly přeneseny do horizon-mods a aktivovány (v nabídce Možnosti > Mody ve hře).",
"What didn't port is listed under Installed.": "Položky, které se nepodařilo přenést, jsou uvedeny v sekci Nainstalované.",
"Installed {mods}, checksums checked. Turn it on in the game: Options > Resource Packs.": "Nainstalováno: {mods}, kontrolní součty ověřeny. Aktivujte ve hře: Možnosti > Balíčky zdrojů.",
"Glass": "Sklo",
"How light the frosted panels and menus are.": "Určuje, jak moc jsou matné panely a nabídky světlé.",
"Blur": "Rozostření",
"How frosted the panels are.": "Určuje míru matného vzhledu panelů.",
"Gather, build and stay alive": "Sbírejte, stavte a přežijte",
"Fly and build with every block": "Létejte a tvořte s každým blokem",
"Play a map the way it was made": "Hrajte mapu tak, jak byla vytvořena",
"Type the computer: user@address, or a Host name from your ~/.ssh/config.": "Zadejte počítač ve tvaru: uzivatel@adresa, případně název hostitele (Host name) ze souboru ~/.ssh/config.",
"{host} isn't a computer ssh can connect to.": "K počítači {host} se nelze přes SSH připojit.",
"Type the server's folder on that computer, like /srv/minecraft.": "Zadejte složku serveru na tomto počítači, například /srv/minecraft.",
"This computer has no ssh program. It comes with Windows 10 and later (Settings › Optional features › OpenSSH Client), macOS and Linux.": "Tento počítač neobsahuje program SSH. Je součástí systémů Windows 10 a novějších (Nastavení › Volitelné funkce › Klient OpenSSH), macOS a Linux.",
"Couldn't run ssh: {error}": "Nepodařilo se spustit SSH: {error}",
"horizon-server on {host} answered something unexpected.": "Služba horizon-server na hostiteli {host} vrátila neočekávanou odpověď.",
"There's no server in {dir} on {host}. Check the folder, or set one up there with horizon-server init.": "Ve složce {dir} na {host} se nenachází žádný server. Zkontrolujte složku, nebo v ní vytvořte nový server pomocí příkazu horizon-server init.",
"{host} didn't accept your SSH key. Set one up once with ssh-copy-id {host} (or your host's control panel), then try again.": "Hostitel {host} nepřiřadil váš klíč SSH. Nastavte jej jednorázově pomocí `ssh-copy-id {host}` (nebo v ovládacím panelu vašeho hostingu) a zkusíte to znovu.",
"ssh doesn't know {host}'s key yet, or it changed. Connect once in a terminal with ssh {host} to check and accept it.": "Nástroj ssh dosud nezná klíč hostitele {host}, nebo došlo k jeho změně. Připojte se jednou v terminálu pomocí `ssh {host}` pro jeho kontrolu a schválení.",
"There's no computer called {host}. Check the address.": "Počítač s názvem {host} neexistuje. Zkontrolujte adresu.",
"Can't reach {host}. It may be off, or its SSH port closed.": "Hostitel {host} je nedostupný. Možná je vypnutý, nebo má zavřený SSH port.",
"{host} has no horizon-server. Install it there first, as the hosting guide says.": "Na hostiteli {host} není nainstalován horizon-server. Nejprve jej tam nainstalujte podle návodu k hostingu.",
"Couldn't connect to {host}: {reason}": "K hostiteli {host} se nepodařilo připojit: {reason}",
"Couldn't connect to {host}.": "K hostiteli {host} se nepodařilo připojit.",
"horizon-server on {host} failed in {dir}.": "Služba horizon-server na hostiteli {host} ve složce {dir} selhala.",
"Horizon runs a data pack's functions, function tags, predicates, item modifiers, recipes, loot tables, advancements, villager trades, damage types, most tags and most worldgen (biomes, features, structures, dimensions). Other files, such as world presets, are not read yet.": "Horizon spouští z datového balíčku funkce, značky funkcí, predikáty, modifikátory předmětů, recepty, tabulky kořisti (loot tables), pokroky, obchody s vesničany, typy poškození, většinu značek a většinu generování světa (biomy, prvky, struktury, dimenze). Ostatní soubory, jako jsou předvolby světa, zatím načítány nejsou.",
"{title} would change nothing in Horizon yet: Horizon doesn't read its {files}.": "Položka {title} by v Horizonu zatím nic nezměnila: Horizon totiž nenačítá její soubory ({files}).",
"{mod}: using version {version}, made for the Horizon you play": "{mod}: používá se verze {version}, vytvořená pro Horizon, který hrajete"
}
+100
View File
@@ -0,0 +1,100 @@
{
"Choose a version": "Version auswählen",
"Accounts, updates, and the launcher itself.": "Konten, Aktualisierungen und der Launcher selbst.",
"Microsoft account · plays": "Microsoft-Konto · spielt",
"Sign in with a Microsoft account that owns Minecraft: Java Edition to play": "Melde dich mit einem Microsoft-Konto an, das Minecraft: Java Edition besitzt, um zu spielen",
"These mods will run with full access to your computer": "Diese Mods erhalten vollen Zugriff auf deinen Computer",
"Launcher": "Launcher",
"Prepare again": "Erneut vorbereiten",
"About": "Über",
"Your picture, behind Home, cropped to fill it.": "Dein Bild hinter der Startseite, zugeschnitten, um sie auszufüllen.",
"System": "System",
"Open {link} in your browser and enter this code:": "Öffne {link} in deinem Browser und gib diesen Code ein:",
"From my install": "Aus meiner Installation",
"the update failed: {error}": "das Update ist fehlgeschlagen: {error}",
"{time} in total · last played {when}": "{time} insgesamt · zuletzt gespielt {when}",
"The launcher stays open while you play.": "Der Launcher bleibt geöffnet, während du spielst.",
"{version} available": "{version} verfügbar",
"Not prepared. Horizon can download Minecraft {version} from Mojang for your account, or use your own install.": "Nicht vorbereitet. Horizon kann Minecraft {version} von Mojang für dein Konto herunterladen oder deine eigene Installation verwenden.",
"downloading {version}, {percent}%": "lade {version} herunter, {percent}%",
"Paste an invite code": "Einladungscode einfügen",
"Dark by default. System follows your computer's light or dark.": "Standardmäßig dunkel. System folgt dem hellen oder dunklen Modus deines Geräts.",
"Open logs folder": "Protokollordner öffnen",
"Skin…": "Skin…",
"Play offline": "Offline spielen",
"crash reports beside it": "absturzberichte daneben",
"Running": "Läuft",
"Show the opening": "Öffnungsanimation anzeigen",
"Open report": "Bericht öffnen",
"The launcher hides when the game starts, and comes back when it closes.": "Der Launcher blendet sich beim Start des Spiels aus und kommt zurück, wenn es schließt.",
"Join": "Beitreten",
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Nicht vorbereitet. Horizon benötigt die Dateien deiner eigenen Minecraft-{version}.",
"The launcher goes down when the game starts, and comes back when it closes.": "Der Launcher minimiert sich beim Start des Spiels und kommt zurück, wenn es schließt.",
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing.": "Wenn du es auf Discord meldest, hänge die Absturzbericht-Datei an (oder füge sie mit \"Bericht kopieren\" ein) und beschreibe, was du getan hast.",
"Get my server": "Meinen Server holen",
"Minecraft folder": "Minecraft-Ordner",
"15% off": "15 % Rabatt",
"Install new versions automatically": "Neue Versionen automatisch installieren",
"Copy code": "Code kopieren",
"Choose a picture…": "Bild auswählen…",
"Settings": "Einstellungen",
"Open folder": "Ordner öffnen",
"None needed: Horizon downloads Minecraft from Mojang for your account.": "Nicht erforderlich: Horizon lädt Minecraft für dein Konto von Mojang herunter.",
"Horizon crashed": "Horizon ist abgestürzt",
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing.": "Das Spiel wurde ohne Absturzbericht beendet. Wenn du es auf Discord meldest, hänge die latest.log aus dem Protokollordner an und beschreibe, was du getan hast.",
"Your Minecraft server, {online}.": "Dein Minecraft-Server, {online}.",
"Another account": "Anderes Konto",
"Microsoft account": "Microsoft-Konto",
"under a minute": "weniger als eine Minute",
"Starting the build": "Starte den Build",
"{days} days ago": "vor {days} Tagen",
"Prepared for Minecraft {version}": "Für Minecraft {version} vorbereitet",
"Start the game, or run a server for others to join.": "Starte das Spiel oder betreibe einen Server, dem andere beitreten können.",
"Close launch": "Start schließen",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Horizons Landschaft. Wähle ein eigenes PNG-, JPEG- oder WebP-Bild aus.",
"This build of the launcher has no Microsoft sign-in (HORIZON_MSA_CLIENT_ID), so it can't start the game.": "Diese Version des Launchers hat keine Microsoft-Anmeldung (HORIZON_MSA_CLIENT_ID). Das Spiel kann daher nicht gestartet werden.",
"Offline name · plays": "Offline-Name · spielt",
"Your worlds, offline names and settings stay where they are.": "Deine Welten, Offline-Namen und Einstellungen bleiben erhalten.",
"A 64×64 (or 64×32) skin PNG and a 64×32 cape PNG, kept by the launcher and given to the game when this name plays. The game draws the default skins for now; it uses these once it can. A Microsoft account's skin and cape: coming soon.": "Ein 64×64-Skin-PNG (oder 64×32) und ein 64×32-Umhang-PNG, die vom Launcher gespeichert und dem Spiel übergeben werden, wenn dieser Name spielt. Das Spiel zeigt vorerst die Standard-Skins an; es wird diese verwenden, sobald es möglich ist. Skin und Umhang eines Microsoft-Kontos: demnächst verfügbar.",
"Play offline under another name, and switch from the top bar. Online servers don't take offline names.": "Spiele offline unter einem anderen Namen und wechsel über die obere Leiste. Online-Server akzeptieren keine Offline-Namen.",
"Report on Discord": "Auf Discord melden",
"No version installed yet.": "Noch keine Version installiert.",
"Applications menu": "Anwendungsmenü",
"Not prepared. Play downloads Minecraft {version} from Mojang for your account and prepares it.": "Nicht vorbereitet. „Spielen“ lädt Minecraft {version} von Mojang für dein Konto herunter und bereitet es vor.",
"Join a friend": "Einem Freund beitreten",
"Not found. Choose the folder where your launcher keeps Minecraft.": "Nicht gefunden. Wähle den Ordner aus, in dem dein Launcher Minecraft speichert.",
"Not prepared. Sign in with the Microsoft account that owns Minecraft {version} first.": "Nicht vorbereitet. Melde dich zuerst mit dem Microsoft-Konto an, das Minecraft {version} besitzt.",
"Copy report": "Bericht kopieren",
"Accent colour": "Akzentfarbe",
"Preparing…": "Vorbereitung…",
"Choose a cape…": "Umhang auswählen…",
"Ready in under a minute": "In unter einer Minute bereit",
"Horizon and its icon are in your desktop's menu.": "Horizon und sein Symbol befinden sich im Menü deines Desktops.",
"Download and prepare": "Herunterladen und vorbereiten",
"The wordmark, while the release list loads.": "Das Logo, während die Versionsliste lädt.",
"Playing offline": "Offline spielen",
"Online servers need a Microsoft account.": "Online-Server benötigen ein Microsoft-Konto.",
"While playing": "Während des Spielens",
"{name}'s cape is kept.": "{name}s Umhang wird beibehalten.",
"Asking Microsoft…": "Frage Microsoft…",
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Für Minecraft {version} ohne Sound vorbereitet: das Spiel bleibt stumm. Bereite es erneut vor, um die Sounds von deiner Minecraft-Installation zu kopieren.",
"Import a modpack…": "Ein Modpack importieren…",
"{version} is not installed": "{version} ist nicht installiert",
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "Sie können deine Dateien lesen, ändern oder löschen und deine Konten sowie dein Netzwerk nutzen wie jedes andere Programm, das du ausführst. Kompiliere sie nur, wenn du ihren Autoren vertraust: Die Mods-Seite nennt sie.",
"Home background": "Startseiten-Hintergrund",
"Manage accounts": "Konten verwalten",
"Your selected version only changes if you were playing the newest one.": "Deine ausgewählte Version ändert sich nur, wenn du die neueste gespielt hast.",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers.": "Horizon benötigt wie andere Launcher ein Microsoft-Konto, das Minecraft: Java Edition besitzt.",
"Not in your desktop's menu.": "Nicht im Menü deines Desktops.",
"A name, 3 to 16 letters": "Name, 3 bis 16 Buchstaben",
"Close export": "Export schließen",
"Offline, the game's name": "Offline-Spiel",
"From Horizon's registry": "Aus der eigenen geprüften Registry",
"Appearance": "Erscheinungsbild",
"Up to date": "Auf dem neuesten Stand",
"Not played yet": "Noch nicht gespielt",
"{name}'s skin is kept.": "{name}s Skin wird beibehalten.",
"Remove shortcut": "Verknüpfung entfernen",
"Default skin": "Standard-Skin",
"Needs preparing": "Vorbereitung erforderlich"
}
+453
View File
@@ -0,0 +1,453 @@
{
"Home": "ホーム",
"Servers": "サーバー",
"Versions": "バージョン",
"Patch notes": "更新履歴",
"Settings": "設定",
"Accounts, updates, and the launcher itself.": "アカウント、アップデート、ランチャーの設定",
"Account": "アカウント",
"Updates": "アップデート",
"Launcher": "ランチャー",
"Network": "ネットワーク",
"Game": "ゲーム",
"AI": "AI",
"About": "このアプリについて",
"Install new versions automatically": "新しいバージョンを自動的にインストールする",
"Your selected version only changes if you were playing the newest one.": "選択中のバージョンは、最新版をプレイしていた場合のみ変更されます。",
"Appearance": "外観",
"Dark": "ダーク",
"Light": "ライト",
"System": "システム",
"Dark by default. System follows your computer's light or dark.": "既定はダーク。システム設定に合わせてライト/ダークを自動で切り替えます。",
"Language": "言語",
"System follows your computer's language: {language} here. A string not translated yet shows in English.": "システム設定({language})に従います。未翻訳のテキストは英語で表示されます。",
"While playing": "プレイ中",
"Stay": "維持",
"Minimise": "最小化",
"Hide": "非表示",
"The launcher stays open while you play.": "プレイ中もランチャーを開いたままにします。",
"The launcher goes down when the game starts, and comes back when it closes.": "ゲーム起動時はランチャーを最小化し、終了時に再表示します。",
"The launcher hides when the game starts, and comes back when it closes.": "ゲーム起動時はランチャーを非表示にし、終了時に再表示します。",
"Show the opening": "起動アニメーションを表示",
"The wordmark, while the release list loads.": "リリース一覧の読み込み中にロゴアニメーションを表示します。",
"{version} available": "{version} が利用可能です",
"Up to date": "最新の状態です",
"Checking…": "確認中…",
"downloading {version}, {percent}%": "{version} をダウンロード中 ({percent}%)",
"the update failed: {error}": "アップデートに失敗しました: {error}",
"Install": "インストール",
"Restart": "再起動",
"Check now": "今すぐ確認",
"Open": "開く",
"Data folder": "データフォルダー",
"Logs": "ログ",
"crash reports beside it": "クラッシュレポートを含む",
"Applications menu": "アプリケーションメニュー",
"Horizon and its icon are in your desktop's menu.": "デスクトップのメニューに Horizon とアイコンを追加します。",
"Not in your desktop's menu.": "デスクトップのメニューには追加されません。",
"Microsoft account": "Microsoft アカウント",
"Another account": "別のアカウント",
"Microsoft account · plays": "Microsoft アカウント · プレイ中",
"Asking Microsoft…": "Microsoft に問い合わせ中…",
"Add another account.": "別のアカウントを追加します。",
"Copy code": "コードをコピー",
"Open link": "リンクを開く",
"Play as": "次の名前でプレイ",
"Play offline": "オフラインでプレイ",
"Sign out": "サインアウト",
"Offline": "オフライン",
"Microsoft": "Microsoft",
"Offline, the game's name": "オフライン、ゲーム名",
"Manage accounts": "アカウント管理",
"Offline name": "オフライン名",
"A name, 3 to 16 letters": "3〜16文字の名前",
"Play offline under another name, and switch from the top bar. Online servers don't take offline names.": "別の名前でオフラインプレイし、上部バーから切り替えられます。オンラインサーバーではオフライン名は使用できません。",
"Add": "追加",
"Skin…": "スキン…",
"Forget": "削除",
"{name}'s skin": "{name}のスキン",
"{name}'s skin is kept.": "{name}のスキンを保持します。",
"{name}'s cape is kept.": "{name}のケープを保持します。",
"Choose a skin…": "スキンを選択…",
"Default skin": "デフォルトスキン",
"Classic arms": "クラシック腕",
"Slim arms": "スリム腕",
"Choose a cape…": "ケープを選択…",
"No cape": "ケープなし",
"Default": "デフォルト",
"A 64×64 (or 64×32) skin PNG and a 64×32 cape PNG, kept by the launcher and given to the game when this name plays. The game draws the default skins for now; it uses these once it can. A Microsoft account's skin and cape: coming soon.": "64×64(または64×32)のスキンPNGと64×32のケープPNG。ランチャーに保存され、この名前でプレイ時にゲームに渡されます。現在はゲームがデフォルトスキンを表示しますが、対応後はこれらが使用されます。Microsoftアカウントのスキンとケープ:近日対応予定。",
"Accent colour": "アクセントカラー",
"Horizon's violet by default. Every colour keeps text as easy to read.": "デフォルトはHorizonのバイオレット。どの色でもテキストの読みやすさを維持します。",
"Choose": "選択",
"Choose a picture…": "画像を選択…",
"Landscape": "背景画像",
"Your picture, behind Home, cropped to fill it.": "ホーム画面の背景に表示される画像。自動的に切り抜かれて表示されます。",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Horizonの背景画像。PNG、JPEG、WebP形式の好きな画像を選べます。",
"Home background": "ホーム背景",
"Play": "プレイ",
"Running": "実行中",
"Stopped": "停止中",
"Start the game, or run a server for others to join.": "ゲームを起動するか、他の人が参加できるサーバーを実行します。",
"No version installed yet.": "まだバージョンがインストールされていません。",
"Choose a version": "バージョンを選択",
"Preparing…": "準備中…",
"Prepare": "準備",
"Prepare again": "再準備",
"None needed: Horizon downloads Minecraft from Mojang for your account.": "不要:Horizonがあなたのアカウント用にMojangからMinecraftをダウンロードします。",
"Not found. Choose the folder where your launcher keeps Minecraft.": "見つかりません。ランチャーがMinecraftを保存しているフォルダを選択してください。",
"Resources": "リソース",
"Minecraft folder": "Minecraftフォルダ",
"Choose…": "選択…",
"Playing offline": "オフラインプレイ中",
"Online servers need a Microsoft account.": "オンラインサーバーにはMicrosoftアカウントが必要です。",
"Accounts…": "アカウント…",
"Horizon crashed": "Horizonがクラッシュしました",
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing.": "Discordで報告する際は、クラッシュレポートファイルを添付(または「レポートをコピー」で貼り付け)し、何をしていたかを説明してください。",
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing.": "ゲームがクラッシュレポートなしで終了しました。Discordで報告する際は、logsフォルダ内のlatest.logを添付し、何をしていたかを説明してください。",
"Open report": "レポートを開く",
"Copied": "コピーしました",
"Copy report": "レポートをコピー",
"Open logs folder": "ログフォルダを開く",
"Report on Discord": "Discordで報告",
"Dismiss": "閉じる",
"INSTANCES": "インスタンス",
"SERVERS": "サーバー",
"HIDE": "非表示",
"Sponsor": "スポンサー",
"Website": "ウェブサイト",
"Ready": "準備完了",
"Needs preparing": "準備が必要",
"{version} is not installed": "{version} はインストールされていません",
"No version installed": "バージョン未インストール",
"Modpack": "Modpack",
"From Horizon's registry": "Horizonのレジストリから",
"Instance": "インスタンス",
"Edit…": "編集…",
"Copy…": "コピー…",
"Export…": "エクスポート…",
"Close export": "エクスポートを閉じる",
"Launch…": "起動…",
"Close launch": "起動を閉じる",
"Open folder": "フォルダを開く",
"Join a friend": "フレンドに参加",
"Paste an invite code": "招待コードを貼り付け",
"Join": "参加",
"Shortcut": "ショートカット",
"Remove shortcut": "ショートカットを削除",
"today": "今日",
"yesterday": "昨日",
"{days} days ago": "{days}日前",
"{time} in total · last played {when}": "合計 {time} · 最終プレイ {when}",
"Not played yet": "未プレイ",
"under a minute": "1分未満",
"PARTNER": "パートナー",
"Your Minecraft server, {online}.": "あなたのMinecraftサーバー、{online}。",
"24/7 online": "24時間365日オンライン",
"Ready in under a minute": "1分以内に準備完了",
"Get my server": "サーバーを取得",
"15% off": "15%オフ",
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "未準備です。Horizonには、あなた自身のMinecraft {version}のファイルが必要です。",
"Prepared for Minecraft {version}": "Minecraft {version}用に準備済み",
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Minecraft {version}用に準備済みですが、サウンドなしです。ゲームは無音になります。Minecraftインストールからコピーするには、再度準備してください。",
"This build of the launcher has no Microsoft sign-in (HORIZON_MSA_CLIENT_ID), so it can't start the game.": "このランチャービルドにはMicrosoftサインイン機能(HORIZON_MSA_CLIENT_ID)がないため、ゲームを起動できません。",
"Sign in with a Microsoft account that owns Minecraft: Java Edition to play": "プレイするには、Minecraft: Java Editionを所有するMicrosoftアカウントでサインインしてください",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers. It also plays online servers with your name and skin.": "Horizonは他のランチャーと同様に、Minecraft: Java Editionを所有するMicrosoftアカウントが必要です。これにより、あなたの名前とスキンでオンラインサーバーに接続できます。",
"Sign in with Microsoft": "Microsoftでサインイン",
"Not now": "今はしない",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers.": "Horizonは他のランチャーと同様に、Minecraft: Java Editionを所有するMicrosoftアカウントが必要です。",
"Your worlds, offline names and settings stay where they are.": "ワールド、オフライン名、設定はそのまま維持されます。",
"Open {link} in your browser and enter this code:": "ブラウザで {link} を開き、このコードを入力してください:",
"From my install": "自分のインストールから",
"Download and prepare": "ダウンロードして準備",
"Not prepared. Play downloads Minecraft {version} from Mojang for your account and prepares it.": "未準備です。「プレイ」を押すと、あなたのアカウント用にMojangからMinecraft {version}をダウンロードして準備します。",
"Not prepared. Horizon can download Minecraft {version} from Mojang for your account, or use your own install.": "未準備です。Horizonはあなたのアカウント用にMojangからMinecraft {version}をダウンロードするか、自分のインストールを使用できます。",
"Not prepared. Sign in with the Microsoft account that owns Minecraft {version} first.": "未準備です。まずMinecraft {version}を所有するMicrosoftアカウントでサインインしてください。",
"Cancel": "キャンセル",
"Worlds": "ワールド",
"Packs": "パック",
"Install Horizon first.": "まずHorizonをインストールしてください。",
"Starting the build": "ビルドを開始中",
"These mods will run with full access to your computer": "これらのModはコンピューターへのフルアクセス権限を持って実行されます",
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "実行する他のプログラムと同様に、ファイルの読み取り・変更・削除や、アカウント・ネットワークの利用が可能です。作者を信頼できる場合のみビルドしてください。Modsページに作者名が記載されています。",
"Mods: {mods}": "Mod:{mods}",
"Import a modpack…": "Modpackをインポート…",
"The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.": "ビルドではゲームをコンパイルします。初回はRustのダウンロードを含むため20〜30分と数GBのディスク容量が必要ですが、以降は数分で完了します。",
"Delete old versions automatically": "古いバージョンを自動削除",
"Keeps the {count} newest, the one in use and the ones your instances use. Deleted versions install again from the Versions page.": "最新の{count}個、現在使用中のもの、およびインスタンスで使用されているものを保持します。削除されたバージョンは「バージョン」ページから再インストールできます。",
"Installed versions use {size}.": "インストール済みのバージョンで {size} を使用しています。",
"{size} on disk": "ディスク上のサイズ:{size}",
"Minecraft {version} (Java Edition). Play downloads it from Mojang for your account the first time, with the Java it needs.": "Minecraft {version}(Java Edition)。初回プレイ時に、必要なJavaとともにあなたのアカウント用にMojangからダウンロードされます。",
"Move {name} to the Trash, with its worlds and settings? You can get it back from there.": "{name} をワールドと設定ごとゴミ箱に移動しますか?そこから復元できます。",
"Delete…": "削除…",
"Keep": "保持",
"Move to Trash": "ゴミ箱へ移動",
"These mods were made for different Horizon versions": "これらのModは異なるHorizonバージョン向けに作成されました",
"They often still work together. If the build fails, install versions made for the same Nightly.": "多くの場合、一緒に動作します。ビルドが失敗する場合は、同じNightly向けに作成されたバージョンをインストールしてください。",
"Build anyway": "それでもビルドする",
"Folder": "フォルダ",
"Version": "バージョン",
"Vanilla": "バニラ",
"Modded": "Modあり",
"Minecraft version": "Minecraftバージョン",
"Search versions": "バージョンを検索",
"Snapshots": "スナップショット",
"Old": "旧バージョン",
"Asking Mojang for its versions…": "Mojangからバージョン情報を取得中…",
"No version matches.": "一致するバージョンはありません。",
"Release": "リリース版",
"Snapshot": "スナップショット",
"Beta": "ベータ",
"Alpha": "アルファ",
"Loader": "ローダー",
"newest stable version": "最新の安定版",
"Play downloads the game from Mojang for your account the first time, with the Java it needs.": "初回プレイ時に、必要なJavaとともにあなたのアカウント用にMojangからゲームをダウンロードします。",
"Mojang's version list isn't here yet.": "Mojangのバージョンリストはまだ取得できていません。",
"Sign in to play": "サインインしてプレイ",
"No Minecraft install needed: Horizon downloads it from Mojang for your account.": "Minecraftのインストールは不要です。Horizonがあなたのアカウント用にMojangからダウンロードします。",
"Waiting for Microsoft… the window goes on by itself.": "Microsoftを待機中… ウィンドウは自動的に進行します。",
"Downloading Minecraft {version} from Mojang · {done} of {total} files": "MojangからMinecraft {version}をダウンロード中 · {done}/{total} ファイル",
"Downloading from Mojang · {done} of {total} files": "Mojangからダウンロード中 · {done}/{total} ファイル",
"Getting ready…": "準備中…",
"about a minute left": "残り約1分",
"about {n} min left": "残り約{n}分",
"Game files": "ゲームファイル",
"Sounds": "サウンド",
"Overview": "概要",
"Multiplayer": "マルチプレイヤー",
"Screenshots": "スクリーンショット",
"Console": "コンソール",
"Update all ({count})": "すべて更新 ({count})",
"Add mods": "Modを追加",
"{loader} mods": "{loader}用Mod",
"Horizon mods": "Horizon用Mod",
"Filter installed": "インストール済みをフィルタ",
"Reading the mods…": "Modを読み込み中…",
"No mods yet: Add mods finds some for this instance.": "まだModはありません。「Modを追加」でこのインスタンス用のModを検索できます。",
"No mod matches.": "一致するModはありません。",
"{source} · needed by another mod": "{source} · 他のModが必要としています",
"Local file · {file}": "ローカルファイル · {file}",
"Update {version}": "{version} に更新",
"Choose image…": "画像を選択…",
"Mods and shader packs made for Horizon from its own reviewed registry, and resource packs from Modrinth. Each file is checked against its source's checksum before it reaches your game folder.": "Horizon独自の審査済みレジストリにあるHorizon用Modおよびシェーダーパック、そしてModrinthのリソースパックです。各ファイルはゲームフォルダに配置される前に、送信元のチェックサムと照合されます。",
"Modpacks": "Modpack",
"Resource packs": "リソースパック",
"Shader packs": "シェーダーパック",
"Data packs": "データパック",
"Show everything": "すべて表示",
"CurseForge (coming soon)": "CurseForge(近日対応)",
"Best match": "最適な一致",
"Relevance": "関連性",
"Downloads": "ダウンロード数",
"Follows": "フォロー数",
"Newest": "最新",
"Updated": "更新日",
"Filters": "フィルター",
"Filters ({count})": "フィルター ({count})",
"Loading filters": "フィルターを読み込み中",
"Game version": "ゲームバージョン",
"Environment": "環境",
"Client": "クライアント",
"Server": "サーバー",
"Categories": "カテゴリ",
"Clear filters": "フィルターをクリア",
"Browse modpacks": "Modpackを閲覧",
"Installs into {instance}: pick another instance on the left.": "{instance} にインストールされます:左側から別のインスタンスを選択してください。",
"A data pack goes into a world, and {instance} has none yet: create one in the game, then come back.": "データパックはワールド内に配置されますが、{instance} にはまだワールドがありません。ゲーム内でワールドを作成してから戻ってください。",
"Installs into a world of {instance}; the game turns the pack on when the world opens.": "{instance} のワールド内にインストールされます。ワールドを開くとゲームがパックを有効化します。",
"Not checked: {error}": "未確認:{error}",
"Checking its jar…": "JARファイルを確認中…",
"No ratings yet": "評価なし",
"Updating": "更新中",
"Installing": "インストール中",
"Installed": "インストール済み",
"Checking": "確認中",
"Works": "動作確認済み",
"Partly": "一部対応",
"Doesn't run": "動作不可",
"Update to {version}": "{version} に更新",
"Removed {name}.": "{name} を削除しました。",
"On": "有効",
"Off": "無効",
"from {source}": "{source} から",
"needed by another pack": "他のパックが必要としています",
"Not ported: {item}": "未移植:{item}",
"checked: {checksum}": "確認済み:{checksum}",
"Searching…": "検索中…",
"Nothing found for {target}.": "{target} に関する結果が見つかりません。",
"Nothing in Horizon's registry matches yet.": "Horizonのレジストリにはまだ一致するものがありません。",
"Nothing found.": "見つかりません。",
"Nothing found that runs in Horizon. Show everything lists the rest.": "Horizonで動作するものは見つかりませんでした。「すべて表示」で残りを一覧表示します。",
"Page": "ページ",
"More": "もっと見る",
"by {author}": "作者:{author}",
"{count} downloads": "{count} 回ダウンロード",
"{count} followers": "{count} フォロワー",
"updated {date}": "{date} に更新",
"Back to results": "結果に戻る",
"Sign in with GitHub to rate": "GitHubでサインインして評価",
"Ports": "移植済み",
"Doesn't port": "未移植",
"{source} page": "{source} ページ",
"Description": "説明",
"Gallery": "ギャラリー",
"Loading…": "読み込み中…",
"No gallery images.": "ギャラリー画像はありません。",
"Add from GitHub": "GitHubから追加",
"Add a native mod from GitHub": "GitHubからネイティブModを追加",
"A link to a repository (its latest release), a release, a branch or tag, or a commit. The launcher installs one exact release file or commit, never a branch that moves.": "リポジトリ(最新リリース)、リリース、ブランチまたはタグ、コミットへのリンクです。ランチャーは特定のリリースファイルまたはコミットのみをインストールし、移動するブランチはインストールしません。",
"{id} is up to date.": "{id} は最新です。",
"Asking GitHub about {owner}/{repo}…": "GitHubで {owner}/{repo} を確認中…",
"Downloading {source}…": "{source} をダウンロード中…",
"Installing…": "インストール中…",
"The release file {file} ({source}).": "リリースファイル {file} ({source})。",
"The source of {source}. Its release ships {file}, a content mod: installing those from GitHub is not supported yet.": "{source} のソースコードです。このリリースにはコンテンツModである {file} が含まれていますが、GitHubからの此类Modのインストールは現在サポートされていません。",
"The source at commit {commit}.": "コミット {commit} のソースコード。",
"Not reviewed by the Horizon registry": "Horizonレジストリで未審査",
"Native mods run with full access to this computer. Read the source before you install it.": "ネイティブModはこのコンピューターへのフルアクセス権限を持って実行されます。インストール前にソースコードを確認してください。",
"Open the source": "ソースコードを開く",
"I read it and trust it: install": "確認済みで信頼します:インストール",
"Install all": "すべてインストール",
"{name} holds {count} mods. Install which?": "{name} には {count} 個のModが含まれています。どれをインストールしますか?",
"Check for updates": "更新を確認",
"Source": "ソース",
"from GitHub: {source}": "GitHubから:{source}",
"No AI": "AI無効",
"On: every AI feature is hidden and off. The game opens no control socket, and horizon-ctl and the MCP servers refuse.": "有効:すべてのAI機能は非表示かつ無効になります。ゲームは制御ソケットを開かず、horizon-ctlおよびMCPサーバーも接続を拒否します。",
"Hides and turns off every AI feature: agent control of the launcher and the game, horizon-ctl and the MCP servers.": "ランチャーとゲームのエージェント制御、horizon-ctl、MCPサーバーなど、すべてのAI機能を非表示にして無効化します。",
"Connect an AI agent": "AIエージェントを接続",
"Your own agent (Claude Code, Codex, Cursor or any MCP client) can drive the launcher and the game it starts, with the same commands as the terminal. It runs on your computer and talks to Horizon there; it asks before deleting anything.": "あなた自身のエージェント(Claude Code、Codex、Cursor、または任意のMCPクライアント)が、ターミナルと同じコマンドでランチャーと起動したゲームを操作できます。これはあなたのコンピューター上で動作し、Horizonと通信します。削除などの操作を行う前には必ず確認を行います。",
"Any MCP client": "任意のMCPクライアント",
"~/.claude.json (or .mcp.json in a project)": "~/.claude.json(またはプロジェクト内の.mcp.json)",
"its MCP settings (a stdio server)": "そのMCP設定(stdioサーバー)",
"Add to {client}": "{client} に追加",
"Running…": "実行中…",
"Copy": "コピー",
"{client} adds it with this command (it runs only when you click):": "{client} は以下のコマンドで追加します(クリック時のみ実行):",
"Or put this in {file}:": "またはこれを {file} に配置:",
"Put this in {file}:": "これを {file} に配置:",
"Test the connection": "接続をテスト",
"The MCP server answered with {count} tools. Now ask your agent: \"Say hello from Horizon\".": "MCPサーバーから {count} 個のツールが返されました。次に、エージェントに「Say hello from Horizon」と尋ねてみてください。",
"The MCP server did not answer: {error}": "MCPサーバーから応答がありません:{error}",
"What an agent can do, and the commands": "エージェントができることとコマンド一覧",
"Added. Restart {program} if it is open, then ask it: \"Say hello from Horizon\".": "追加しました。{program} が開いている場合は再起動し、「Say hello from Horizon」と尋ねてください。",
"{program} said: {output}": "{program} の回答:{output}",
"{program} is not installed here (or not on the PATH): copy the config instead.": "{program} がインストールされていないか、PATHに含まれていません。代わりに設定ファイルをコピーしてください。",
"Could not run {program}: {error}": "{program} を実行できませんでした:{error}",
"Always on": "常に有効",
"Could not save your choice: {error}": "選択を保存できませんでした:{error}",
"Data collection": "データ収集",
"Help make Horizon faster and steadier?": "Horizonの高速化と安定化に協力しますか?",
"Horizon can send a few things to its developers. Each switch is off until you turn it on, and Settings can change it any time.": "Horizonは開発者にいくつかのデータを送信できます。各スイッチは手動で有効にするまでオフであり、設定でいつでも変更できます。",
"Hide what would be sent": "送信内容を表示しない",
"View what would be sent": "送信内容を表示",
"Send nothing": "何も送信しない",
"Save choices": "選択を保存",
"No install ID: nothing has been collected": "インストールIDなし:データは収集されていません",
"Install ID {prefix}…{suffix}": "インストールID {prefix}…{suffix}",
"{count} waiting to be sent": "{count} 件が送信待ち",
"Sending is turned off here (HORIZON_TELEMETRY_URL=off)": "ここでは送信が無効になっています(HORIZON_TELEMETRY_URL=off)",
"Delete everything?": "すべて削除しますか?",
"Delete my data": "私のデータを削除",
"Your data": "あなたのデータ",
"Each switch is off unless you turn it on. What it sends is below it; the privacy page on playhorizon.dev says how long it is kept.": "各スイッチは手動で有効にするまでオフです。送信される内容は下部に記載されています。保持期間については playhorizon.dev のプライバシーページをご覧ください。",
"Performance statistics": "パフォーマンス統計",
"After each session: frame-time percentiles and a 14-bucket histogram, tick times, peak memory and startup times, with your machine (OS, CPU and its cores and clocks, memory size, GPU, its memory and driver, drive type, screen size and refresh rate, laptop or desktop, on battery or not) and the settings that change performance.": "各セッション後:フレーム時間のパーセンタイルと14ビンのヒストグラム、ティック時間、ピークメモリ使用量、起動時間。さらにマシンスペック(OS、CPUとそのコア数・クロック数、メモリ容量、GPUとそのメモリ・ドライバー、ドライブ種類、画面サイズ・リフレッシュレート、ノートPCかデスクトップか、バッテリー駆動かどうか)およびパフォーマンスに影響する設定情報を含みます。",
"Crash reports": "クラッシュレポート",
"When the game crashes: the error, where in Horizon's code it happened, your graphics adapter and settings. No log, no world or server, no user name in paths.": "ゲームがクラッシュした際:エラー内容、Horizonコード内の発生箇所、グラフィックスアダプターと設定情報。ログ、ワールド、サーバー、パス内のユーザー名は含まれません。",
"Bug reports": "バグレポート",
"Only the reports you write and send yourself, with the game's log if you add it after reading it.": "あなたが自分で作成して送信したレポートのみ。読んだ後にゲームのログを追加した場合、それも含まれます。",
"Benchmark uploads": "ベンチマークアップロード",
"The results of benchmarks you run, for the community results on playhorizon.dev.": "あなたが実行したベンチマークの結果。playhorizon.dev のコミュニティ結果用に使用されます。",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "送信されない情報:アカウント名またはUUID、ワールド名、サーバーアドレス、チャット内容、IPアドレス。各レポートは日付単位で記録され、削除可能なランダムなインストールIDの下で管理されます。",
"\"I'm playing\" ping": "「プレイ中」ピング",
"While you play, the game sends an anonymous \"I'm playing\" ping once a minute, so the launcher can show how many people play Horizon right now. It holds: a random number made when the game starts and forgotten when it closes, the Horizon version, your OS (Linux, macOS or Windows), and whether you have a world open to LAN or friends. It doesn't hold: your account, your install ID, world or server names, or anything you do in the game, and the server doesn't keep your IP address. It is sent whatever the switches say.": "プレイ中、ゲームは毎分匿名の「プレイ中」ピングを送信し、ランチャーが現在のHorizonプレイヤー数を表示できるようにします。含まれる情報:ゲーム開始時に生成され終了時に破棄されるランダム番号、Horizonバージョン、OS(Linux、macOS、Windows)、LANまたはフレンド向けにワールドを公開しているかどうか。含まれない情報:アカウント、インストールID、ワールド名、サーバー名、ゲーム内での行動。また、サーバーはIPアドレスを保持しません。この送信はスイッチの設定に関わらず行われます。",
"All": "すべて",
"Ported": "移植済み",
"Partial": "一部対応",
"Not yet": "未対応",
"Not needed": "不要",
"Import": "インポート",
"Use these jars": "これらのJARを使用",
"Import a pack file…": "パックファイルをインポート…",
"Import an instance folder…": "インスタンスフォルダをインポート…",
".mrpack (Modrinth), .zip (CurseForge), or a Prism, MultiMC or CurseForge instance. Horizon ports what it can on this computer, from your own downloads, and plays the pack as its own profile. Nothing is uploaded or shared.": ".mrpack(Modrinth)、.zip(CurseForge)、またはPrism、MultiMC、CurseForgeのインスタンス。Horizonはこのコンピューターで可能な範囲で、あなたのダウンロード元からModを移植し、独自のプロファイルとしてパックを実行します。データはアップロードされず、共有もされません。",
"Your modpacks": "あなたのModpack",
"No modpack imported yet: import one, or find one below.": "まだModpackがインポートされていません。下に一覧から探すか、インポートしてください。",
"Remove for good": "完全に削除",
"Remove": "削除",
"{version} from Horizon's registry, its own profile: {mods}": "Horizonレジストリの {version}、独自プロファイル:{mods}",
"Working": "処理中",
"Done": "完了",
"Play with {count} mods": "{count} 個のModでプレイ",
"Port {count} mods": "{count} 個のModを移植",
"Use jars from my instance…": "自分のインスタンスからJARを使用…",
"Port gets the jars of the {count} mods this computer can port today (content with data and assets only) from the pack's own links, Modrinth, or your folder, and carries them over. The ports stay here, in {folder}.": "移植機能は、このコンピューターで現在移植可能な {count} 個のMod(データとアセットのみを含むコンテンツMod)のJARを、パック元のリンク、Modrinth、またはあなたのフォルダから取得して移行します。移植されたファイルは {folder} に保存されます。",
"Left off: {line}.": "中断箇所:{line}。",
"Playing needs a prepared Horizon version (Play page).": "プレイするには、準備済みのHorizonバージョンが必要です(プレイページ参照)。",
"{count} files": "{count} ファイル",
"downloaded": "ダウンロード済み",
"copied from your folder": "あなたのフォルダからコピー",
"already here": "既に存在",
"only on CurseForge (CurseForge downloads coming soon: choose your installed instance instead)": "CurseForgeのみ利用可能(CurseForgeからのダウンロードは近日対応予定:代わりにインストール済みのインスタンスを選択してください)",
"not found": "見つかりません",
"failed": "失敗",
"Jars: {summary}.": "JAR:{summary}。",
"Host a world for your friends on this computer. Create one, press Start, and give them the address.": "このコンピューターでフレンド向けのワールドをホストします。サーバーを作成し、開始ボタンを押して、アドレスを教えてあげてください。",
"Create a server": "サーバーを作成",
"Add a server folder…": "サーバーフォルダを追加…",
"No server yet. Create one: it takes a few seconds, and with the defaults (tick the EULA, press Create) you get a friends-only survival world.": "まだサーバーがありません。作成してみましょう:数秒で完了し、デフォルト設定(EULAに同意して「作成」を押す)で、フレンド限定のサバイバルワールドが手に入ります。",
"{online} of {max} players": "{online}/{max} プレイヤー",
"← All servers": "← すべてのサーバー",
"Start": "開始",
"Stop": "停止",
"Open the web panel": "Webパネルを開く",
"Open the folder": "フォルダを開く",
"Saved. Restart to apply: {settings}.": "保存しました。適用するには再起動してください:{settings}。",
"Restart now": "今すぐ再起動",
"Players": "プレイヤー",
"Backups": "バックアップ",
"Players online": "オンラインプレイヤー",
"TPS · 20 is full speed": "TPS · 20が最大速度",
"MSPT · tick time": "MSPT · ティック時間",
"Address on this network": "このネットワーク上のアドレス",
"Friends on the same Wi-Fi: Multiplayer, Add Server, then paste this address.": "同じWi-Fi内のフレンド向け:マルチプレイヤー > サーバー追加 で、このアドレスを貼り付けてください。",
"Friends elsewhere can't reach it as it is: use Tailscale (free, nothing to change on your router, only people you invite), or forward TCP port {port} on your router to this computer.": "外部のフレンドは現状接続できません。Tailscale(無料、ルーター設定不要、招待した人だけ接続可能)を使用するか、ルーターでTCPポート {port} をこのコンピューターに転送してください。",
"Online now": "現在オンライン",
"Waiting for the server's log…": "サーバーログを待機中…",
"The server's log shows here while it runs.": "サーバー実行中、ここにログが表示されます。",
"Start the server to type commands": "コマンドを入力するにはサーバーを開始してください",
"Updating: {step}": "更新中:{step}",
"Running · restarted after a crash": "実行中 · クラッシュ後再起動",
"Running · paused while nobody plays": "実行中 · プレイヤー不在のため一時停止",
"Starting": "起動中",
"Restarting": "再起動中",
"Stopping": "停止中",
"the message in the server list": "サーバーリストのメッセージ",
"the port": "ポート",
"most players": "最大プレイヤー数",
"view distance": "描画距離",
"simulation distance": "シミュレーション距離",
"account checks": "アカウント確認",
"Friends over the internet · Tailscale": "インターネット経由のフレンド · Tailscale",
"Check again": "再確認",
"Looking for Tailscale…": "Tailscaleを検索中…",
"Tailscale lets friends anywhere join without opening your router to the internet. It's free: install it, sign in, then invite them here.": "Tailscaleを使えば、ルーターをインターネットに公開せずに、どこからでもフレンドを参加させられます。無料なので、インストールしてサインインし、ここで招待してください。",
"Install Tailscale": "Tailscaleをインストール",
"Open Tailscale": "Tailscaleを開く",
"Tailscale is installed but not running. Open it and switch it on.": "Tailscaleはインストールされていますが、実行されていません。開いて有効にしてください。",
"Sign in to Tailscale": "Tailscaleにサインイン",
"Tailscale is installed. Sign in with your own account in Tailscale (Horizon never sees it), then come back here.": "Tailscaleはインストール済みです。Tailscaleで自分のアカウントにサインイン(Horizonはこの情報を知りません)し、ここに戻ってください。",
"On Linux, run `{cmd}` in a terminal and open the link it prints.": "Linuxでは、ターミナルで `{cmd}` を実行し、表示されたリンクを開いてください。",
"Or {address}. Friends type it in Multiplayer, Add Server, once this computer is shared with them and Tailscale runs on theirs.": "または {address}。このコンピューターが共有され、相手の環境でもTailscaleが実行されていれば、フレンドはマルチプレイヤー > サーバー追加 でこのアドレスを入力できます。",
"Friends type it in Multiplayer, Add Server, once this computer is shared with them and Tailscale runs on theirs.": "このコンピューターが共有され、相手の環境でもTailscaleが実行されていれば、フレンドはマルチプレイヤー > サーバー追加 でこのアドレスを入力できます。",
"Invite a friend": "フレンドを招待",
"Horizon never turns on Funnel: the server isn't put on the public internet, only shared with the people you invite.": "HorizonはFunnel機能を有効にしません。サーバーは公的なインターネット上に公開されず、招待した人とのみ共有されます。",
"Share this computer with a friend: they get it in their own Tailscale, and nothing else of yours. Do it in Tailscale's admin console (Machines, this computer, Share), or here with an access token you make there.": "このコンピューターをフレンドと共有しましょう。相手は自分のTailscaleでこれを確認でき、他の情報は見えません。Tailscaleの管理コンソール(Machines > このコンピューター > Share)で行うか、そこで生成したアクセストークンを使ってここで行えます。",
"Open the admin console": "管理コンソールを開く",
"Save": "保存",
"Make a token": "トークンを作成",
"Settings, Keys, Generate access token. It's kept in this computer's keychain and only sent to Tailscale.": "設定 > キー > アクセストークン生成。これはこのコンピューターのキーチェーンに保存され、Tailscaleにのみ送信されます。",
"Use an access token…": "アクセストークンを使用…",
"Make an invitation": "招待状を作成",
"One invitation per friend. With an email Tailscale sends it; without, send them the link. They accept it in their own Tailscale account.": "フレンドごとに1つの招待状が必要です。メールアドレスがあればTailscaleが送信し、なければリンクを送ってください。相手は自分のTailscaleアカウントで承認します。"
}
+336
View File
@@ -0,0 +1,336 @@
{
"Home": "Início",
"Mods": "Mods",
"Servers": "Servidores",
"Versions": "Versões",
"Patch notes": "Notas da atualização",
"Settings": "Configurações",
"Accounts, updates, and the launcher itself.": "Contas, atualizações e o próprio launcher.",
"Account": "Conta",
"Updates": "Atualizações",
"Launcher": "Launcher",
"Network": "Rede",
"Game": "Jogo",
"AI": "IA",
"About": "Sobre",
"Install new versions automatically": "Instalar novas versões automaticamente",
"Your selected version only changes if you were playing the newest one.": "Sua versão selecionada só será alterada se você estiver jogando na versão mais recente.",
"Appearance": "Aparência",
"Dark": "Escuro",
"Light": "Claro",
"System": "Sistema",
"Dark by default. System follows your computer's light or dark.": "O tema escuro é usado por padrão. A opção Sistema acompanha o tema claro ou escuro do seu computador.",
"Language": "Idioma",
"System follows your computer's language: {language} here. A string not translated yet shows in English.": "O sistema segue o idioma do seu computador: {language} neste caso. Textos ainda não traduzidos serão exibidos em inglês.",
"While playing": "Durante o jogo",
"Stay": "Manter aberto",
"Minimise": "Minimizar",
"Hide": "Ocultar",
"The launcher stays open while you play.": "O launcher permanece aberto enquanto você joga.",
"The launcher goes down when the game starts, and comes back when it closes.": "O launcher é minimizado quando o jogo inicia e volta quando ele é fechado.",
"The launcher hides when the game starts, and comes back when it closes.": "O launcher é ocultado quando o jogo inicia e volta quando ele é fechado.",
"Show the opening": "Mostrar animação de abertura",
"The wordmark, while the release list loads.": "Exibe o logotipo enquanto a lista de versões é carregada.",
"{version} available": "{version} disponível",
"Up to date": "Atualizado",
"up to date": "atualizado",
"Checking…": "Verificando…",
"downloading {version}, {percent}%": "baixando {version}, {percent}%",
"the update failed: {error}": "Falha ao atualizar: {error}",
"Install": "Instalar",
"Restart": "Reiniciar",
"Check now": "Verificar agora",
"Open": "Abrir",
"Data folder": "Pasta de dados",
"crash reports beside it": "relatórios de crash na mesma pasta",
"Applications menu": "Menu de aplicativos",
"Horizon and its icon are in your desktop's menu.": "O Horizon e seu ícone estão no menu de aplicativos do seu computador.",
"Not in your desktop's menu.": "Não está no menu de aplicativos do seu computador.",
"Microsoft account": "Conta Microsoft",
"Another account": "Outra conta",
"Microsoft account · plays": "Conta Microsoft · jogando",
"Asking Microsoft…": "Conectando à Microsoft…",
"Add another account.": "Adicionar outra conta.",
"Copy code": "Copiar código",
"Open link": "Abrir link",
"Play as": "Jogar como",
"Play offline": "Jogar offline",
"Sign out": "Sair da conta",
"Offline": "Offline",
"Microsoft": "Microsoft",
"Offline, the game's name": "Offline, o nome usado no jogo",
"Manage accounts": "Gerenciar contas",
"Offline name": "Nome offline",
"Offline name · plays": "Nome offline · jogando",
"A name, 3 to 16 letters": "Um nome de 3 a 16 caracteres",
"Play offline under another name, and switch from the top bar. Online servers don't take offline names.": "Jogue offline com outro nome e alterne entre eles pela barra superior. Servidores online não aceitam nomes offline.",
"Add": "Adicionar",
"Skin…": "Skin…",
"Forget": "Remover",
"{name}'s skin": "Skin de {name}",
"{name}'s skin is kept.": "A skin de {name} será mantida.",
"{name}'s cape is kept.": "A capa de {name} será mantida.",
"Choose a skin…": "Escolher uma skin…",
"Default skin": "Skin padrão",
"Classic arms": "Braços clássicos",
"Slim arms": "Braços finos",
"Choose a cape…": "Escolher uma capa…",
"No cape": "Sem capa",
"Default": "Padrão",
"A 64×64 (or 64×32) skin PNG and a 64×32 cape PNG, kept by the launcher and given to the game when this name plays. The game draws the default skins for now; it uses these once it can. A Microsoft account's skin and cape: coming soon.": "Uma imagem PNG de skin nos formatos 64×64 (ou 64×32) e uma imagem PNG de capa no formato 64×32. Os arquivos ficam armazenados no launcher e são enviados ao jogo quando esse nome é utilizado. Por enquanto, o jogo continua exibindo as skins padrão; utilizará essas imagens assim que possível. Suporte às skins e capas de contas Microsoft: em breve.",
"Accent colour": "Cor de destaque",
"Horizon's violet by default. Every colour keeps text as easy to read.": "Violeta do Horizon por padrão. Todas as cores mantêm os textos fáceis de ler.",
"Choose": "Escolher",
"Choose a picture…": "Escolher uma imagem…",
"Landscape": "Plano de fundo",
"Your picture, behind Home, cropped to fill it.": "Sua imagem aparece ao fundo da tela inicial, redimensionada para preencher todo o espaço.",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Imagem de fundo padrão do Horizon. Você também pode escolher uma imagem própria nos formatos PNG, JPEG ou WebP.",
"Home background": "Plano de fundo da tela inicial",
"Play": "Jogar",
"Running": "Em execução",
"Stopped": "Parado",
"Start the game, or run a server for others to join.": "Inicie o jogo ou execute um servidor para que outras pessoas possam entrar.",
"No version installed yet.": "Nenhuma versão instalada ainda.",
"Choose a version": "Escolher uma versão",
"Preparing…": "Preparando…",
"Prepare": "Preparar",
"Prepare again": "Preparar novamente",
"None needed: Horizon downloads Minecraft from Mojang for your account.": "Não é necessário: o Horizon baixa o Minecraft diretamente da Mojang para sua conta.",
"Not found. Choose the folder where your launcher keeps Minecraft.": "Não encontrado. Escolha a pasta onde seu launcher armazena o Minecraft.",
"Resources": "Recursos",
"Minecraft folder": "Pasta do Minecraft",
"Choose…": "Escolher…",
"Playing offline": "Jogando offline",
"Online servers need a Microsoft account.": "Servidores online exigem uma conta Microsoft.",
"Accounts…": "Contas…",
"Horizon crashed": "O Horizon travou",
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing.": "Ao relatar o problema no Discord, anexe o relatório de crash (ou cole seu conteúdo usando Copiar relatório) e conte o que estava fazendo.",
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing.": "O jogo foi encerrado sem gerar um relatório de crash. Ao relatar o problema no Discord, anexe o arquivo latest.log da pasta de logs e conte o que estava fazendo.",
"Open report": "Abrir relatório",
"Copied": "Copiado",
"Copy report": "Copiar relatório",
"Open logs folder": "Abrir pasta de logs",
"Report on Discord": "Reportar no Discord",
"Dismiss": "Dispensar",
"INSTANCES": "INSTÂNCIAS",
"SERVERS": "SERVIDORES",
"HIDE": "OCULTAR",
"Sponsor": "Patrocinar",
"Website": "Site",
"Ready": "Pronto",
"Needs preparing": "Precisa ser preparado",
"{version} is not installed": "{version} não está instalada",
"No version installed": "Nenhuma versão instalada",
"Modpack": "Modpack",
"From Horizon's registry": "Do catálogo do Horizon",
"Instance": "Instância",
"Edit…": "Editar…",
"Copy…": "Copiar…",
"Export…": "Exportar…",
"Close export": "Fechar exportação",
"Launch…": "Iniciar…",
"Close launch": "Fechar inicialização",
"Open folder": "Abrir pasta",
"Join a friend": "Entrar na sessão de um amigo",
"Paste an invite code": "Cole um código de convite",
"Join": "Entrar",
"Shortcut": "Atalho",
"Remove shortcut": "Remover atalho",
"today": "hoje",
"yesterday": "ontem",
"{days} days ago": "há {days} dias",
"{time} in total · last played {when}": "{time} no total · jogado pela última vez {when}",
"Not played yet": "Ainda não jogado",
"under a minute": "menos de um minuto",
"PARTNER": "PARCEIRO",
"Your Minecraft server, {online}.": "Seu servidor de Minecraft, {online}.",
"24/7 online": "Online 24 horas por dia",
"Ready in under a minute": "Pronto em menos de um minuto",
"Get my server": "Obter meu servidor",
"15% off": "15% de desconto",
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Não preparado. O Horizon precisa dos arquivos da sua própria instalação do Minecraft {version}.",
"Prepared for Minecraft {version}": "Preparado para o Minecraft {version}",
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Preparado para o Minecraft {version}, mas sem os sons: o jogo ficará sem áudio. Prepare novamente para copiá-los da sua instalação do Minecraft.",
"This build of the launcher has no Microsoft sign-in (HORIZON_MSA_CLIENT_ID), so it can't start the game.": "Esta versão do launcher não possui autenticação Microsoft (HORIZON_MSA_CLIENT_ID), portanto não pode iniciar o jogo.",
"Sign in with a Microsoft account that owns Minecraft: Java Edition to play": "Entre com uma conta Microsoft que possua Minecraft: Java Edition para jogar",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers. It also plays online servers with your name and skin.": "Assim como outros launchers, o Horizon precisa de uma conta Microsoft que possua Minecraft: Java Edition. Ela também permite jogar em servidores online usando seu nome e sua skin.",
"Sign in with Microsoft": "Entrar com a Microsoft",
"Not now": "Agora não",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers.": "Assim como outros launchers, o Horizon precisa de uma conta Microsoft que possua Minecraft: Java Edition.",
"Your worlds, offline names and settings stay where they are.": "Seus mundos, nomes offline e configurações permanecerão intactos.",
"Open {link} in your browser and enter this code:": "Acesse {link} pelo navegador e insira este código:",
"From my install": "Da minha instalação",
"Download and prepare": "Baixar e preparar",
"Not prepared. Play downloads Minecraft {version} from Mojang for your account and prepares it.": "Não preparado. Ao clicar em Jogar, o Minecraft {version} será baixado da Mojang para sua conta e preparado.",
"Not prepared. Horizon can download Minecraft {version} from Mojang for your account, or use your own install.": "Não preparado. O Horizon pode baixar o Minecraft {version} diretamente da Mojang para sua conta ou utilizar sua própria instalação.",
"Not prepared. Sign in with the Microsoft account that owns Minecraft {version} first.": "Não preparado. Primeiro, entre com a conta Microsoft que possui o Minecraft {version}.",
"Cancel": "Cancelar",
"Install Horizon first.": "Instale o Horizon primeiro.",
"Starting the build": "Iniciando a compilação",
"These mods will run with full access to your computer": "Estes mods terão acesso total ao seu computador",
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "Eles podem ler, modificar ou excluir seus arquivos, além de utilizar suas contas e sua rede, assim como qualquer programa executado no computador. Compile-os apenas se confiar em seus criadores: os autores são identificados na página Mods.",
"Mods: {mods}": "Mods: {mods}",
"Delete old versions automatically": "Excluir versões antigas automaticamente",
"Keeps the {count} newest, the one in use and the ones your instances use. Deleted versions install again from the Versions page.": "Mantém as {count} versões mais recentes, a versão em uso e aquelas utilizadas pelas suas instâncias. As versões excluídas podem ser instaladas novamente pela página Versões.",
"Installed versions use {size}.": "As versões instaladas ocupam {size}.",
"{size} on disk": "{size} em disco",
"The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.": "A compilação do jogo leva de 20 a 30 minutos na primeira vez e ocupa alguns GB de espaço em disco (pois também baixa o Rust). Nas próximas vezes, leva apenas alguns minutos.",
"Delete…": "Excluir…",
"Keep": "Manter",
"Move to Trash": "Mover para a lixeira",
"Move {name} to the Trash, with its worlds and settings? You can get it back from there.": "Deseja mover {name} para a lixeira junto com seus mundos e configurações? Você poderá restaurá-lo posteriormente.",
"Minecraft {version} (Java Edition). Play downloads it from Mojang for your account the first time, with the Java it needs.": "Minecraft {version} (Java Edition). Na primeira vez, ao clicar em Jogar, o jogo será baixado da Mojang para sua conta junto com a versão do Java necessária.",
"These mods were made for different Horizon versions": "Estes mods foram criados para versões diferentes do Horizon",
"They often still work together. If the build fails, install versions made for the same Nightly.": "Mesmo assim, eles geralmente funcionam juntos. Se a compilação falhar, instale versões criadas para a mesma versão Nightly.",
"Build anyway": "Compilar mesmo assim",
"Folder": "Pasta",
"Version": "Versão",
"Import a modpack…": "Importar um modpack…",
"Vanilla": "Vanilla",
"Modded": "Modificado",
"Minecraft version": "Versão do Minecraft",
"Search versions": "Pesquisar versões",
"Snapshots": "Snapshots",
"Old": "Antigas",
"Asking Mojang for its versions…": "Consultando as versões disponíveis na Mojang…",
"No version matches.": "Nenhuma versão encontrada.",
"Release": "Lançamento",
"Snapshot": "Snapshot",
"Beta": "Beta",
"Alpha": "Alfa",
"Loader": "Carregador",
"newest stable version": "versão estável mais recente",
"Play downloads the game from Mojang for your account the first time, with the Java it needs.": "Na primeira vez, ao clicar em Jogar, o jogo será baixado da Mojang para sua conta junto com a versão do Java necessária.",
"Mojang's version list isn't here yet.": "A lista de versões da Mojang ainda não está disponível.",
"Sign in to play": "Entre para jogar",
"No Minecraft install needed: Horizon downloads it from Mojang for your account.": "Não é necessário ter o Minecraft instalado: o Horizon o baixa diretamente da Mojang para sua conta.",
"Waiting for Microsoft… the window goes on by itself.": "Aguardando a Microsoft… a janela continuará automaticamente.",
"Downloading Minecraft {version} from Mojang · {done} of {total} files": "Baixando Minecraft {version} da Mojang · {done} de {total} arquivos",
"Downloading from Mojang · {done} of {total} files": "Baixando da Mojang · {done} de {total} arquivos",
"Getting ready…": "Preparando…",
"about a minute left": "falta aproximadamente um minuto",
"about {n} min left": "faltam aproximadamente {n} min",
"Game files": "Arquivos do jogo",
"Sounds": "Sons",
"Overview": "Visão geral",
"Worlds": "Mundos",
"Packs": "Pacotes",
"Screenshots": "Capturas de tela",
"Console": "Console",
"Update all ({count})": "Atualizar tudo ({count})",
"Add mods": "Adicionar mods",
"{loader} mods": "Mods do {loader}",
"Horizon mods": "Mods do Horizon",
"Filter installed": "Filtrar instalados",
"Reading the mods…": "Carregando mods…",
"No mods yet: Add mods finds some for this instance.": "Nenhum mod instalado ainda: use Adicionar mods para encontrar mods para esta instância.",
"No mod matches.": "Nenhum mod encontrado.",
"{source} · needed by another mod": "{source} · necessário para outro mod",
"Local file · {file}": "Arquivo local · {file}",
"Partly": "Parcialmente compatível",
"Doesn't run": "Não funciona",
"On": "Ativado",
"Off": "Desativado",
"Nothing in Horizon's registry matches yet.": "Ainda não há resultados correspondentes no catálogo do Horizon.",
"Nothing found that runs in Horizon. Show everything lists the rest.": "Nenhum resultado compatível com o Horizon. Use Mostrar tudo para ver os demais.",
"{source} page": "Página de {source}",
"Always on": "Sempre ativo",
"Help make Horizon faster and steadier?": "Quer ajudar a deixar o Horizon mais rápido e estável?",
"Horizon can send a few things to its developers. Each switch is off until you turn it on, and Settings can change it any time.": "O Horizon pode enviar algumas informações aos desenvolvedores. Todas as opções ficam desativadas por padrão, e você pode alterá-las a qualquer momento nas Configurações.",
"Each switch is off unless you turn it on. What it sends is below it; the privacy page on playhorizon.dev says how long it is kept.": "Todas as opções ficam desativadas por padrão. As informações enviadas são descritas logo abaixo de cada uma, e a página de privacidade em playhorizon.dev informa por quanto tempo elas são armazenadas.",
"After each session: frame-time percentiles and a 14-bucket histogram, tick times, peak memory and startup times, with your machine (OS, CPU and its cores and clocks, memory size, GPU, its memory and driver, drive type, screen size and refresh rate, laptop or desktop, on battery or not) and the settings that change performance.": "Após cada sessão: percentis do tempo de quadro e um histograma de 14 faixas, tempos de tick, pico de uso de memória e tempos de inicialização. Esses dados são enviados junto com informações sobre seu computador (sistema operacional, CPU, número de núcleos e frequências, quantidade de memória, GPU, memória e driver da GPU, tipo de unidade de armazenamento, tamanho e taxa de atualização da tela, se é notebook ou desktop e se está usando bateria) e as configurações que afetam o desempenho.",
"Crash reports": "Relatórios de crash",
"When the game crashes: the error, where in Horizon's code it happened, your graphics adapter and settings. No log, no world or server, no user name in paths.": "Quando o jogo sofre um crash: o erro, onde isso aconteceu no código do Horizon, sua placa de vídeo e suas configurações. Nenhum log, mundo ou servidor, nem nome de usuário nos caminhos.",
"The results of benchmarks you run, for the community results on playhorizon.dev.": "Os resultados dos benchmarks que você executar serão enviados para compor os resultados da comunidade em playhorizon.dev.",
"\"I'm playing\" ping": "\"Estou jogando\" ping",
"While you play, the game sends an anonymous \"I'm playing\" ping once a minute, so the launcher can show how many people play Horizon right now. It holds: a random number made when the game starts and forgotten when it closes, the Horizon version, your OS (Linux, macOS or Windows), and whether you have a world open to LAN or friends. It doesn't hold: your account, your install ID, world or server names, or anything you do in the game, and the server doesn't keep your IP address. It is sent whatever the switches say.": "Enquanto você joga, o jogo envia um ping anônimo de \"Estou jogando\" uma vez por minuto. Assim, o launcher pode mostrar quantas pessoas estão jogando Horizon naquele momento. Esse ping contém: um número aleatório criado quando o jogo é iniciado e descartado quando ele é fechado, a versão do Horizon, seu sistema operacional (Linux, macOS ou Windows) e se há um mundo aberto para LAN ou amigos. Ele não contém: informações da sua conta, seu ID de instalação, nomes de mundos ou servidores, nem qualquer atividade sua dentro do jogo. O servidor também não armazena seu endereço IP. Esse ping é enviado independentemente das opções selecionadas.",
".mrpack (Modrinth), .zip (CurseForge), or a Prism, MultiMC or CurseForge instance. Horizon ports what it can on this computer, from your own downloads, and plays the pack as its own profile. Nothing is uploaded or shared.": ".mrpack (Modrinth), .zip (CurseForge) ou uma instância do Prism, MultiMC ou CurseForge. O Horizon porta o que for possível neste computador, usando seus próprios downloads, e executa o modpack como um perfil separado. Nada é enviado ou compartilhado.",
"Add a server folder…": "Adicionar uma pasta de servidor…",
"Address on this network": "Endereço nesta rede",
"All": "Todos",
"Backups": "Backups",
"Check again": "Verificar novamente",
"Create a server": "Criar um servidor",
"Done": "Concluído",
"Forget the access token": "Esquecer o token de acesso",
"Friends elsewhere can't reach it as it is: use Tailscale (free, nothing to change on your router, only people you invite), or forward TCP port {port} on your router to this computer.": "Amigos que estão em outras redes não conseguem acessá-lo dessa forma: use o Tailscale (gratuito, sem precisar alterar as configurações do roteador e acessível apenas a quem você convidar) ou redirecione a porta TCP {port} do roteador para este computador.",
"Friends on the same Wi-Fi: Multiplayer, Add Server, then paste this address.": "Amigos na mesma rede Wi-Fi: acessem Multiplayer, Adicionar servidor e colem este endereço.",
"Friends over the internet · Tailscale": "Amigos pela internet · Tailscale",
"Friends type it in Multiplayer, Add Server, once this computer is shared with them and Tailscale runs on theirs.": "Depois que este computador for compartilhado com eles e o Tailscale estiver em execução nos computadores deles, seus amigos devem inserir o endereço em Multiplayer > Adicionar servidor.",
"Horizon never turns on Funnel: the server isn't put on the public internet, only shared with the people you invite.": "O Horizon nunca ativa o Funnel: o servidor não fica exposto à internet pública, sendo compartilhado apenas com as pessoas que você convidar.",
"Host a world for your friends on this computer. Create one, press Start, and give them the address.": "Hospede um mundo para seus amigos neste computador. Crie um mundo, clique em Iniciar e passe o endereço para eles.",
"Import": "Importar",
"Import a pack file…": "Importar um arquivo de modpack…",
"Import an instance folder…": "Importar uma pasta de instância…",
"Install Tailscale": "Instalar o Tailscale",
"Invite a friend": "Convidar um amigo",
"Jars: {summary}.": "JARs: {summary}.",
"Left off: {line}.": "Desativado: {line}.",
"Looking for Tailscale…": "Procurando pelo Tailscale…",
"MSPT · tick time": "MSPT · tempo de tick",
"Make a token": "Criar um token",
"Make an invitation": "Criar um convite",
"No modpack imported yet: import one, or find one below.": "Nenhum modpack foi importado ainda: importe um ou encontre um abaixo.",
"No server yet. Create one: it takes a few seconds, and with the defaults (tick the EULA, press Create) you get a friends-only survival world.": "Ainda não há servidores. Crie um: leva apenas alguns segundos e, com as opções padrão (marque a EULA e clique em Criar), você terá um mundo de sobrevivência acessível apenas aos seus amigos.",
"Not needed": "Não necessário",
"Not yet": "Ainda não",
"On Linux, run `{cmd}` in a terminal and open the link it prints.": "No Linux, execute `{cmd}` em um terminal e abra o link exibido.",
"One invitation per friend. With an email Tailscale sends it; without, send them the link. They accept it in their own Tailscale account.": "Use um convite para cada amigo. Se houver um e-mail, o Tailscale enviará o convite; caso contrário, envie o link. Eles deverão aceitá-lo na própria conta do Tailscale.",
"Online now": "Online agora",
"Open Tailscale": "Abrir o Tailscale",
"Open the admin console": "Abrir o console de administração",
"Open the folder": "Abrir a pasta",
"Open the web panel": "Abrir o painel web",
"Or {address}. Friends type it in Multiplayer, Add Server, once this computer is shared with them and Tailscale runs on theirs.": "Ou {address}. Depois que este computador for compartilhado com eles e o Tailscale estiver em execução nos computadores deles, seus amigos devem inserir o endereço em Multiplayer > Adicionar servidor.",
"Partial": "Parcial",
"Play with {count} mods": "Jogar com {count} mods",
"Players": "Jogadores",
"Players online": "Jogadores online",
"Playing needs a prepared Horizon version (Play page).": "Para jogar, é necessária uma versão do Horizon preparada (na página Jogar).",
"Port gets the jars of the {count} mods this computer can port today (content with data and assets only) from the pack's own links, Modrinth, or your folder, and carries them over. The ports stay here, in {folder}.": "O recurso Portar busca os arquivos JAR dos {count} mods que este computador consegue portar no momento (apenas conteúdo com dados e recursos), usando os links do próprio modpack, o Modrinth ou sua pasta, e os transfere. Os mods portados ficam aqui, em {folder}.",
"Port {count} mods": "Portar {count} mods",
"Ported": "Portado",
"Remove": "Remover",
"Remove for good": "Excluir permanentemente",
"Restart now": "Reiniciar agora",
"Restarting": "Reiniciando",
"Running · paused while nobody plays": "Em execução · pausado quando não há jogadores",
"Running · restarted after a crash": "Em execução · reiniciado após um crash",
"Save": "Salvar",
"Saved. Restart to apply: {settings}.": "Salvo. Reinicie para aplicar: {settings}.",
"Settings, Keys, Generate access token. It's kept in this computer's keychain and only sent to Tailscale.": "Configurações, Chaves, Gerar token de acesso. Ele fica armazenado no chaveiro deste computador e é enviado apenas ao Tailscale.",
"Share this computer with a friend: they get it in their own Tailscale, and nothing else of yours. Do it in Tailscale's admin console (Machines, this computer, Share), or here with an access token you make there.": "Compartilhe este computador com um amigo: ele terá acesso a este dispositivo pelo próprio Tailscale, sem acesso a nenhum outro conteúdo seu. Faça isso no console de administração do Tailscale (Máquinas, este computador, Compartilhar) ou aqui, usando um token de acesso gerado lá.",
"Sign in to Tailscale": "Entrar no Tailscale",
"Start": "Iniciar",
"Start the server to type commands": "Inicie o servidor para digitar comandos",
"Starting": "Iniciando",
"Stop": "Parar",
"Stopping": "Parando",
"TPS · 20 is full speed": "TPS · 20 é a velocidade máxima",
"Tailscale is installed but not running. Open it and switch it on.": "O Tailscale está instalado, mas não está em execução. Abra-o e ative-o.",
"Tailscale is installed. Sign in with your own account in Tailscale (Horizon never sees it), then come back here.": "O Tailscale está instalado. Entre com sua própria conta no Tailscale (o Horizon nunca tem acesso a ela) e depois volte aqui.",
"Tailscale lets friends anywhere join without opening your router to the internet. It's free: install it, sign in, then invite them here.": "O Tailscale permite que amigos de qualquer lugar entrem sem expor seu roteador à internet. É gratuito: instale-o, entre na sua conta e convide seus amigos por aqui.",
"The server's log shows here while it runs.": "O log do servidor aparece aqui enquanto ele está em execução.",
"Updating: {step}": "Atualizando: {step}",
"Use an access token…": "Usar um token de acesso…",
"Use jars from my instance…": "Usar arquivos JAR da minha instância…",
"Use these jars": "Usar estes arquivos JAR",
"Waiting for the server's log…": "Aguardando o log do servidor…",
"Working": "Em andamento",
"Your modpacks": "Seus modpacks",
"account checks": "verificações da conta",
"already here": "já está aqui",
"copied from your folder": "copiado da sua pasta",
"downloaded": "baixado",
"failed": "falhou",
"most players": "maioria dos jogadores",
"not found": "não encontrado",
"only on CurseForge (CurseForge downloads coming soon: choose your installed instance instead)": "somente no CurseForge (downloads pelo CurseForge em breve: escolha uma instância já instalada)",
"simulation distance": "distância de simulação",
"the message in the server list": "a mensagem na lista de servidores",
"the port": "a porta",
"view distance": "distância de renderização",
"{count} files": "{count} arquivos",
"{online} of {max} players": "{online} de {max} jogadores",
"{version} from Horizon's registry, its own profile: {mods}": "{version} do catálogo do Horizon, em seu próprio perfil: {mods}"
}
+151
View File
@@ -0,0 +1,151 @@
{
"Its blocks and items came over and passed Horizon's checks; the mod's own code is not run.": "Её блоки и предметы прошли проверки Horizon; код самого мода не запускается.",
"Part of it came over, but something is left out, differs from the original or has not been checked yet; the technical detail says what.": "Её часть перенесена, но чего-то не хватает, что-то отличается от оригинала или ещё не проверено; техническая подробность говорит, что именно.",
"It does not come over yet.": "Ещё не перенесено.",
"Nothing to port: Horizon has it built in or does not need it.": "Нечего портировать: Horizon уже имеет это встроенным или в этом не нуждается.",
"Horizon cannot tell yet what this mod does (it needs its jar or a reviewed entry), so it cannot say whether it will come over.": "Horizon пока не знает, что делает этот мод (нужен его jar или проверенная запись), поэтому не может сказать, перенесётся ли он.",
"It joins other mods together, so it waits until those are ported.": "Он объединяет другие моды, поэтому ждёт, пока они будут перенесены.",
"Content only (blocks, items, structures). Its jar is here, but it has not been carried over yet.": "Только контент (блоки, предметы, структуры). Его jar на месте, но ещё не перенесён.",
"Content only (blocks, items, structures). It can come over once its jar is here.": "Только контент (блоки, предметы, структуры). Может быть перенесён, как только его jar появится.",
"Needs what blocks and items do when used, placed, broken or ticked, which Horizon's mod API cannot run from a mod yet, so it does not come over.": "Нужно то, что делают блоки и предметы при использовании, размещении, ломании или тике, чего API модов Horizon ещё не может запускать из мода, поэтому он не переносится.",
"Needs machine-style features (block entities, menus, item, fluid and energy handling, networking) that Horizon's mod API does not have yet, so it does not come over.": "Нужны машинные функции (блок-сущности, меню, обработка предметов, жидкостей и энергии, сети), которых ещё нет в API модов Horizon, поэтому он не переносится.",
"Needs mobs, custom rendering and models, dimensions or new game hooks that Horizon's mod API does not have yet, so it does not come over.": "Нужны мобы, пользовательский рендеринг и модели, измерения или новые хуки игры, которых ещё нет в API модов Horizon, поэтому он не переносится.",
"Needs the game's client interfaces (keys, targeting, inventory reading, interface, rendering), so it does not come over yet.": "Нужны клиентские интерфейсы игры (клавиши, прицеливание, чтение инвентаря, интерфейс, рендеринг), поэтому он ещё не переносится.",
"Horizon has not worked out what this mod needs yet.": "Horizon ещё не определил, что нужно этому моду.",
"A library other mods build on. Horizon's mod API and SDK take its place, so there is nothing to port.": "Библиотека, на которой строятся другие моды. API модов и SDK Horizon заменяют её, поэтому нечего портировать.",
"It makes the game faster. Horizon's own engine does that, so there is nothing to port.": "Она ускоряет игру. Собственный движок Horizon делает это, поэтому нечего портировать.",
"Pack tooling or a game feature that Horizon already has, so there is nothing to port.": "Инструменты для наборов или функция игры, которую Horizon уже имеет, поэтому нечего портировать.",
"Built into Horizon already, so there is nothing to port.": "Уже встроено в Horizon, поэтому нечего портировать.",
"Nothing to port.": "Нечего портировать.",
"Made for Minecraft 1.20 or older: Horizon ports mods made for 1.21 and later.": "Сделан для Minecraft 1.20 или старше: Horizon портирует моды для 1.21 и новее.",
"Shader packs are not converted yet.": "Наборы шейдеров ещё не конвертируются.",
"Copied as a resource pack; Horizon has not checked how it renders. Turn it on in the game's Resource Packs screen.": "Скопировано как набор ресурсов; Horizon ещё не проверял, как он рендерится. Включите его на экране «Наборы ресурсов» в игре.",
"Not downloaded yet; Port gets it.": "Ещё не скачано; Port загрузит его.",
"Horizon could not use this resource pack yet; the technical detail says why.": "Horizon пока не может использовать этот набор ресурсов; техническая подробность объясняет, почему.",
"It also waits for 1 mod it depends on that is not ported yet.": "Также ждёт 1 мод, от которого зависит, который ещё не перенесён.",
"It also waits for {count} mods it depends on that are not ported yet.": "Также ждёт {count} модов, от которых зависит, которые ещё не перенесены.",
"{carried} of {total} mods that need porting come over as content (blocks, items and structures); their own code does not run.": "{carried} из {total} модов, требующих переноса, переносятся как контент (блоки, предметы и структуры); их собственный код не запускается.",
"Not yet: {ready} are content mods Port can carry over, {blocked} need mod support Horizon does not have yet (machines, menus, behaviour, mobs, dimensions, client features), {later} are not sorted yet (not identified, or waiting on other mods or other work).": "Ещё нет: {ready} — контентные моды, которые может перенести Port, {blocked} нуждаются в поддержке модов, которой у Horizon ещё нет (машины, меню, поведение, мобы, измерения, клиентские функции), {later} ещё не разобраны (не идентифицированы или ожидают других модов или работ).",
"Partial means part of a mod came over, but something is left out, differs from the original or has not been checked yet.": "Частично означает, что часть мода перенесена, но чего-то не хватает, что-то отличается от оригинала или ещё не проверено.",
"Performance mode": "Режим производительности",
"Smoother on older computers: a lower-resolution sky, a cheaper blur, window resizes baked in the background and no frame per mouse move.": "Плавнее на старых компьютерах: небо с более низким разрешением, более дешёвое размытие, изменение размера окна, заранее вычисленное в фоновом режиме, и нет кадра на каждое движение мыши.",
"Play with {count} mods": "Играть с {count} модами",
"What an agent can do, and the commands": "Что умеет агент и его команды",
"not found": "не найдено",
"Remove for good": "Удалить безвозвратно",
"Import": "Импорт",
"Working": "Работает",
"Install ID {prefix}…{suffix}": "Идентификатор установки {prefix}…{suffix}",
"Only the reports you write and send yourself, with the game's log if you add it after reading it.": "Отправляются только отчёты, которые вы пишете и отправляете сами, а также лог игры, если добавите его после прочтения.",
"Performance statistics": "Статистика производительности",
"Your own agent (Claude Code, Codex, Cursor or any MCP client) can drive the launcher and the game it starts, with the same commands as the terminal. It runs on your computer and talks to Horizon there; it asks before deleting anything.": "Ваш собственный агент (Claude Code, Codex, Cursor или любой клиент MCP) может управлять лаунчером и игрой, которую он запускает, теми же командами, что и в терминале. Он работает на вашем компьютере и общается с Horizon там; он спрашивает перед удалением чего-либо.",
"The MCP server did not answer: {error}": "MCP-сервер не ответил: {error}",
"Added. Restart {program} if it is open, then ask it: \"Say hello from Horizon\".": "Добавлено. Перезапустите {program}, если он открыт, затем спросите его: «Привет от Horizon».",
"All": "Все",
"Jars: {summary}.": "JAR-файлы: {summary}.",
"Always on": "Всегда включено",
"Delete everything?": "Удалить всё?",
"Add to {client}": "Добавить в {client}",
"Import an instance folder…": "Импортировать папку сборки…",
"its MCP settings (a stdio server)": "её настройки MCP (сервер stdio)",
"Your modpacks": "Ваши модпаки",
"Port gets the jars of the {count} mods this computer can port today (content with data and assets only) from the pack's own links, Modrinth, or your folder, and carries them over. The ports stay here, in {folder}.": "Port загружает jar-файлы {count} модов, которые этот компьютер может перенести сегодня (только контент с данными и ресурсами), из собственных ссылок набора, Modrinth или вашей папки, и переносит их. Переносы остаются здесь, в {folder}.",
"View what would be sent": "Посмотреть, что будет отправлено",
"Delete my data": "Удалить мои данные",
"Use these jars": "Использовать эти jar-файлы",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Никогда не отправляется: имя учётной записи или UUID, названия миров, адреса серверов, чат или ваш IP-адрес. Каждый отчёт датирован днём, под случайным идентификатором установки, который можно удалить.",
"Benchmark uploads": "Загрузки результатов тестов",
".mrpack (Modrinth), .zip (CurseForge), or a Prism, MultiMC or CurseForge instance. Horizon ports what it can on this computer, from your own downloads, and plays the pack as its own profile. Nothing is uploaded or shared.": ".mrpack (Modrinth), .zip (CurseForge), или экземпляр Prism, MultiMC или CurseForge. Horizon портирует то, что может, на этом компьютере, из ваших загрузок, и запускает набор как свой профиль. Ничего не загружается и не передаётся.",
"{version} from Horizon's registry, its own profile: {mods}": "{version} из реестра Horizon, его собственный профиль: {mods}",
"Crash reports": "Отчёты о сбоях",
"Could not save your choice: {error}": "Не удалось сохранить ваш выбор: {error}",
"Not yet": "Ещё нет",
"{client} adds it with this command (it runs only when you click):": "{client} добавляет его этой командой (она выполняется только при нажатии):",
"Running…": "Запуск…",
"No install ID: nothing has been collected": "Нет идентификатора установки: ничего не собрано",
"Bug reports": "Сообщения об ошибках",
"Not needed": "Не нужно",
"Could not run {program}: {error}": "Не удалось запустить {program}: {error}",
"Send nothing": "Не отправлять ничего",
"Use jars from my instance…": "Использовать JAR из моей сборки…",
"Hide what would be sent": "Скрыть, что будет отправлено",
"{program} said: {output}": "{program} выдал: {output}",
"Help make Horizon faster and steadier?": "Помогите сделать Horizon быстрее и стабильнее?",
"Ported": "Перенесено",
"Partial": "Частично",
"Connect an AI agent": "Подключить ИИ-агента",
"Each switch is off unless you turn it on. What it sends is below it; the privacy page on playhorizon.dev says how long it is kept.": "Каждый переключатель выключен, пока вы его не включите. Что именно отправляется, написано под ним; страница конфиденциальности на playhorizon.dev говорит, как долго это хранится.",
"The MCP server answered with {count} tools. Now ask your agent: \"Say hello from Horizon\".": "MCP-сервер ответил {count} инструментами. Теперь спросите своего агента: «Привет от Horizon».",
"Sending is turned off here (HORIZON_TELEMETRY_URL=off)": "Отправка здесь отключена (HORIZON_TELEMETRY_URL=off)",
"{count} files": "{count} файлов",
"downloaded": "скачано",
"Any MCP client": "Любой MCP-клиент",
"failed": "сбой",
"Left off: {line}.": "Остановлено на: {line}.",
"After each session: frame-time percentiles and a 14-bucket histogram, tick times, peak memory and startup times, with your machine (OS, CPU and its cores and clocks, memory size, GPU, its memory and driver, drive type, screen size and refresh rate, laptop or desktop, on battery or not) and the settings that change performance.": "После каждой сессии: процентили времени кадра и гистограмма из 14 корзин, время тиков, пиковая память и время запуска, а также характеристики вашего компьютера (ОС, процессор и его ядра и тактовые частоты, объём памяти, видеокарта, её память и драйвер, тип диска, размер и частота экрана, ноутбук или стационарный компьютер, питание от сети или от батареи) и настройки, влияющие на производительность.",
"Save choices": "Сохранить выбор",
"Put this in {file}:": "Поместите это в {file}:",
"Import a pack file…": "Импортировать файл набора…",
"No modpack imported yet: import one, or find one below.": "Модпаков пока нет: импортируйте один или найдите ниже.",
"only on CurseForge (CurseForge downloads coming soon: choose your installed instance instead)": "только на CurseForge (загрузки с CurseForge скоро: выберите вместо этого свою установленную сборку)",
"No AI": "Без ИИ",
"{count} waiting to be sent": "{count} ожидают отправки",
"Or put this in {file}:": "Или поместите это в {file}:",
"When the game crashes: the error, where in Horizon's code it happened, your graphics adapter and settings. No log, no world or server, no user name in paths.": "Когда игра вылетает: ошибка, место в коде Horizon, ваш видеоадаптер и настройки. Без лога, мира или сервера, без имён пользователей в путях.",
"Data collection": "Сбор данных",
"already here": "уже здесь",
"Port {count} mods": "Перенести {count} модов",
"copied from your folder": "скопировано из вашей папки",
"On: every AI feature is hidden and off. The game opens no control socket, and horizon-ctl and the MCP servers refuse.": "Вкл.: каждая функция ИИ скрыта и выключена. Игра не открывает сокет управления, horizon-ctl и MCP-серверы отказываются.",
"Done": "Готово",
"Playing needs a prepared Horizon version (Play page).": "Для игры нужна подготовленная версия Horizon (страница «Играть»).",
"Hides and turns off every AI feature: agent control of the launcher and the game, horizon-ctl and the MCP servers.": "Скрывает и выключает каждую функцию ИИ: управление агентом лаунчера и игры, horizon-ctl и MCP-серверы.",
"Your data": "Ваши данные",
"Horizon can send a few things to its developers. Each switch is off until you turn it on, and Settings can change it any time.": "Horizon может отправить несколько вещей своим разработчикам. Каждый переключатель выключен, пока вы его не включите, и Настройки могут изменить это в любое время.",
"Test the connection": "Проверить соединение",
"{program} is not installed here (or not on the PATH): copy the config instead.": "{program} не установлен здесь (или не в PATH): скопируйте конфиг вместо этого.",
"The results of benchmarks you run, for the community results on playhorizon.dev.": "Результаты ваших тестов, для общих результатов на playhorizon.dev.",
"Could not open the data folder: {error}": "Не удалось открыть папку данных: {error}",
"Run `horizon-launcher install latest` to get one.": "Запустите `horizon-launcher install latest`, чтобы получить его.",
"There is already an instance called {name}. Choose another name.": "Сборка с именем {name} уже существует. Выберите другое имя.",
"Open Settings and sign in under Account.": "Откройте Настройки и войдите в разделе «Учётная запись».",
"Sign in with `horizon-launcher account sign-in`.": "Войдите через `horizon-launcher account sign-in`.",
"{file} unpacks to more than {limit}: it isn't imported.": "{file} распаковывается в более чем {limit}: он не импортируется.",
"Horizon's Vulkan renderer crashed while starting on this graphics card, so the game now uses {api}. Press Play to start it again. Video Settings > Graphics API switches back.": "Визуализатор Vulkan Horizon не запустился на этой видеокарте, поэтому игра теперь использует {api}. Нажмите «Играть», чтобы запустить её снова. Видео настройки > Графический API переключает обратно.",
"This Nightly's title ({title}) doesn't name its date and commit as Horizon signs them, so it can't be checked: it was not installed.": "Заголовок этой ночной сборки ({title}) не указывает дату и коммит, как подписывает Horizon, поэтому его нельзя проверить: она не установлена.",
"{tag} is older than {newest}, which is installed: installing it as a rollback. The selection only moves to it when you pick it.": "{tag} старше {newest}, который установлен: устанавливается как откат. Выбор переходит на него только, когда вы выберете его.",
"Update registry mods automatically": "Автоматически обновлять моды из реестра",
"At start and before you play, what you installed from Horizon's registry moves to its newest version for this game. Never inside a modpack, and an update that would remove blocks or items waits for you.": "Перед стартом и до того, как вы начнёте играть, то, что вы установили из реестра Horizon, обновляется до самой новой версии для этой игры. Это никогда не происходит внутри модпака, а обновление, которое удалило бы блоки или предметы, будет ждать вашего подтверждения.",
"Updated automatically: {list}.": "Обновляется автоматически: {list}.",
"{title} wasn't updated: {error}": "{title} не был обновлён: {error}",
"{title} {version} waits for you: it no longer has {ids}, which your worlds may use. Update it on the Mods page when you're ready.": "{title} {version} ждет вас: в ней больше нет {ids}, которые могут использоваться в ваших мирах. Обновите её на странице модов, когда будете готовы.",
"{source} isn't set up.": "{source} не настроен.",
"Installed {mods}, checksums checked.": "{mods} установлены, контрольные суммы проверены.",
"Pick the world the data pack goes into first.": "Сначала выберите мир, в который будет добавлен датапак.",
"Put {pack} {version} in {world}'s data packs, checksum checked: the game turns it on when the world opens. {coverage}": "Поместил {pack} {version} в датапаки мира {world}, контрольная сумма проверена: игра включит его при открытии мира. {coverage}",
"Installed {pack} {version} as its own profile with {mods}, each checked. Play it from the list above.": "Установил {pack} {version} в отдельном профиле с {mods}, каждый из которых был проверен. Запустите игру из списка выше.",
"Play it from the list above.": "Запустите это из списка выше.",
"Installed {mods} into {instance}. Build to play with it.": "Моды {mods} установлены в {instance}. Соберите проект, чтобы поиграть.",
"Ported {mods} into horizon-mods and turned on (Options > Mods in the game).": "Портируйте {mods} в папку horizon-mods и включите их (Настройки > Моды в игре).",
"What didn't port is listed under Installed.": "То, что не перенеслось, указано в списке \"Установленные.\"",
"Installed {mods}, checksums checked. Turn it on in the game: Options > Resource Packs.": "Установлены {mods}, хеши проверены. Включите их в игре: Настройки > Наборы ресурсов.",
"Glass": "Панель",
"How light the frosted panels and menus are.": "Насколько осветлить матовые панели и меню.",
"Blur": "Размытие",
"How frosted the panels are.": "Насколько матовые панели.",
"Gather, build and stay alive": "Собирай, строй и выживай",
"Fly and build with every block": "Летай и строй из каждого блока",
"Play a map the way it was made": "Играйте на карте так, как она была задумана",
"Type the computer: user@address, or a Host name from your ~/.ssh/config.": "Введите адрес компьютера: user@address или имя хоста из файла ~/.ssh/config.",
"{host} isn't a computer ssh can connect to.": "{host} — это не компьютер, к которому ssh может подключиться.",
"Type the server's folder on that computer, like /srv/minecraft.": "Введите путь к папке сервера на этом компьютере, например /srv/minecraft.",
"This computer has no ssh program. It comes with Windows 10 and later (Settings › Optional features › OpenSSH Client), macOS and Linux.": "На этом компьютере нет программы SSH. Она входит в состав Windows 10 и более поздних версий (Настройки › Дополнительные функции › Клиент OpenSSH), а также в состав macOS и Linux.",
"Couldn't run ssh: {error}": "Ну удалось запустить ssh: {error}",
"horizon-server on {host} answered something unexpected.": "Сервер horizon на {host} ответил чем-то неожиданным.",
"There's no server in {dir} on {host}. Check the folder, or set one up there with horizon-server init.": "В папке {dir} на хосте {host} нет сервера. Проверьте папку или создайте там сервер с помощью команды horizon-server init.",
"{host} didn't accept your SSH key. Set one up once with ssh-copy-id {host} (or your host's control panel), then try again.": "{host} не принял ваш ключ SSH. Настройте его один раз с помощью команды ssh-copy-id {host} (или через панель управления вашего хоста), а затем попробуйте ещё раз.",
"ssh doesn't know {host}'s key yet, or it changed. Connect once in a terminal with ssh {host} to check and accept it.": "ssh пока не знает ключ {host}, либо он изменился. Подключитесь один раз в терминале с помощью команды ssh {host}, чтобы проверить и принять его.",
"There's no computer called {host}. Check the address.": "Компьютера с именем {host} не существует. Проверьте адрес.",
"Can't reach {host}. It may be off, or its SSH port closed.": "Не удается подключиться к {host}. Возможно, он выключен или его порт SSH закрыт.",
"{host} has no horizon-server. Install it there first, as the hosting guide says.": "На {host} отсутствует horizon-server. Сначала установите его как указано в руководстве по хостингу."
}
+457
View File
@@ -0,0 +1,457 @@
{
"Home": "Ana Sayfa",
"Mods": "Modlar",
"Servers": "Sunucular",
"Versions": "Sürümler",
"Patch notes": "Sürüm Notları",
"Settings": "Ayarlar",
"Accounts, updates, and the launcher itself.": "Hesaplar, güncellemeler ve başlatıcının kendisi.",
"Account": "Hesap",
"Updates": "Güncellemeler",
"Launcher": "Başlatıcı",
"Network": "Ağ",
"Game": "Oyun",
"AI": "Yapay Zeka",
"About": "Hakkında",
"Install new versions automatically": "Yeni sürümleri otomatik olarak yükle",
"Your selected version only changes if you were playing the newest one.": "Seçtiğiniz sürüm yalnızca en son sürümü oynadıysanız değişir.",
"Appearance": "Görünüm",
"Dark": "Koyu",
"Light": "Açık",
"System": "Sistem",
"Dark by default. System follows your computer's light or dark.": "Varsayılan olarak koyu. Sistem, bilgisayarınızın açık veya koyu modunu takip eder.",
"Language": "Dil",
"System follows your computer's language: {language} here. A string not translated yet shows in English.": "Sistem, bilgisayarınızın dilini takip eder: burada {language}. Henüz çevrilmemiş bir metin İngilizce olarak gösterilir.",
"While playing": "Oynarken",
"Stay": "Açık tut",
"Minimise": "Küçült",
"Hide": "Gizle",
"The launcher stays open while you play.": "Siz oynarken başlatıcı açık kalır.",
"The launcher goes down when the game starts, and comes back when it closes.": "Oyun başladığında başlatıcı simge durumuna küçültülür ve kapandığında geri döner.",
"The launcher hides when the game starts, and comes back when it closes.": "Oyun başladığında başlatıcı gizlenir ve kapandığında geri döner.",
"Show the opening": "Açılış animasyonunu göster",
"The wordmark, while the release list loads.": "Sürüm listesi yüklenirken logo.",
"{version} available": "{version} mevcut",
"Up to date": "Güncel",
"up to date": "güncel",
"Checking…": "Kontrol ediliyor…",
"downloading {version}, {percent}%": "{version} indiriliyor, %{percent}",
"the update failed: {error}": "güncelleme başarısız oldu: {error}",
"Install": "Yükle",
"Restart": "Yeniden Başlat",
"Check now": "Şimdi kontrol et",
"Open": "Aç",
"Data folder": "Veri klasörü",
"Logs": "Günlükler",
"crash reports beside it": "yanındaki çökme raporları",
"Applications menu": "Uygulamalar menüsü",
"Horizon and its icon are in your desktop's menu.": "Horizon ve simgesi masaüstü menünüzde yer alıyor.",
"Not in your desktop's menu.": "Masaüstü menünüzde değil.",
"Microsoft account": "Microsoft hesabı",
"Another account": "Başka bir hesap",
"Microsoft account · plays": "Microsoft hesabı · oynuyor",
"Asking Microsoft…": "Microsoft'a soruluyor…",
"Add another account.": "Başka bir hesap ekle.",
"Copy code": "Kodu kopyala",
"Open link": "Bağlantıyı aç",
"Play as": "Şu şekilde oyna",
"Play offline": "Çevrimdışı oyna",
"Sign out": "Oturumu kapat",
"Offline": "Çevrimdışı",
"Microsoft": "Microsoft",
"Offline, the game's name": "Çevrimdışı oyun",
"Manage accounts": "Hesapları yönet",
"Offline name": "Çevrimdışı ad",
"Offline name · plays": "Çevrimdışı ad · oynuyor",
"A name, 3 to 16 letters": "3 ila 16 harf arası bir ad",
"Play offline under another name, and switch from the top bar. Online servers don't take offline names.": "Başka bir adla çevrimdışı oynayın ve üst çubuktan geçiş yapın. Çevrimiçi sunucular çevrimdışı adları kabul etmez.",
"Add": "Ekle",
"Skin…": "Dış görünüm…",
"Forget": "Unut",
"{name}'s skin": "{name} adlı kişinin dış görünümü",
"{name}'s skin is kept.": "{name} adlı kişinin dış görünümü saklanıyor.",
"{name}'s cape is kept.": "{name} adlı kişinin pelerini saklanıyor.",
"Choose a skin…": "Bir dış görünüm seç…",
"Default skin": "Varsayılan dış görünüm",
"Classic arms": "Klasik kollar",
"Slim arms": "İnce kollar",
"Choose a cape…": "Bir pelerin seç…",
"No cape": "Pelerin yok",
"Default": "Varsayılan",
"A 64×64 (or 64×32) skin PNG and a 64×32 cape PNG, kept by the launcher and given to the game when this name plays. The game draws the default skins for now; it uses these once it can. A Microsoft account's skin and cape: coming soon.": "Başlatıcı tarafından saklanan ve bu adla oynandığında oyuna aktarılan 64×64 (veya 64×32) dış görünüm PNG'si ve 64×32 pelerin PNG'si. Oyun şimdilik varsayılan dış görünüşleri çizer; imkan bulduğunda bunları kullanacaktır. Microsoft hesabının dış görünümü ve pelerini: yakında.",
"Accent colour": "Vurgu rengi",
"Horizon's violet by default. Every colour keeps text as easy to read.": "Varsayılan olarak Horizon moru. Her renk, metnin kolayca okunabilmesini sağlar.",
"Choose": "Seç",
"Choose a picture…": "Bir resim seç…",
"Landscape": "Manzara",
"Your picture, behind Home, cropped to fill it.": "Ana sayfanın arkasında yer alan, ekranı dolduracak şekilde kırpılmış resminiz.",
"Horizon's landscape. Pick a PNG, JPEG or WebP picture of your own.": "Horizon'ın manzarası. Kendi PNG, JPEG veya WebP resminizi seçin.",
"Home background": "Ana sayfa arka planı",
"Play": "Oyna",
"Running": "Çalışıyor",
"Stopped": "Durduruldu",
"Start the game, or run a server for others to join.": "Oyunu başlatın veya diğerlerinin katılması için bir sunucu çalıştırın.",
"No version installed yet.": "Henüz yüklenmiş bir sürüm yok.",
"Choose a version": "Bir sürüm seç",
"Preparing…": "Hazırlanıyor…",
"Prepare": "Hazırla",
"Prepare again": "Tekrar hazırla",
"None needed: Horizon downloads Minecraft from Mojang for your account.": "Gerek yok: Horizon, hesabınız için Minecraft'ı Mojang'dan indirir.",
"Not found. Choose the folder where your launcher keeps Minecraft.": "Bulunamadı. Başlatıcınızın Minecraft'ı sakladığı klasörü seçin.",
"Resources": "Kaynaklar",
"Minecraft folder": "Minecraft klasörü",
"Choose…": "Seç…",
"Playing offline": "Çevrimdışı oynanıyor",
"Online servers need a Microsoft account.": "Çevrimiçi sunucular bir Microsoft hesabı gerektirir.",
"Accounts…": "Hesaplar…",
"Horizon crashed": "Horizon çöktü",
"When you report it on Discord, attach the crash report file (or paste it with Copy report) and say what you were doing.": "Bunu Discord'da bildirdiğinizde, çökme raporu dosyasını ekleyin (veya \"Raporu kopyala\" ile yapıştırın) ve ne yaptığınızı belirtin.",
"The game ended without a crash report. When you report it on Discord, attach latest.log from the logs folder and say what you were doing.": "Oyun bir çökme raporu olmaksızın sonlandı. Bunu Discord'da bildirdiğinizde, logs klasöründen latest.log dosyasını ekleyin ve ne yaptığınızı belirtin.",
"Open report": "Raporu aç",
"Copied": "Kopyalandı",
"Copy report": "Raporu kopyala",
"Open logs folder": "Günlükler klasörünü aç",
"Report on Discord": "Discord'da bildir",
"Dismiss": "Kapat",
"INSTANCES": "ÖRNEKLER",
"SERVERS": "SUNUCULAR",
"HIDE": "GİZLE",
"Sponsor": "Destek Ol",
"Website": "İnternet Sitesi",
"Ready": "Hazır",
"Needs preparing": "Hazırlanması gerekiyor",
"{version} is not installed": "{version} yüklü değil",
"No version installed": "Yüklü sürüm yok",
"Modpack": "Mod Paketi",
"From Horizon's registry": "Horizon'ın kendi doğrulanmış kayıt defterinden",
"Instance": "Örnek",
"Edit…": "Düzenle…",
"Copy…": "Kopyala…",
"Export…": "Dışa Aktar…",
"Close export": "Dışa aktarmayı kapat",
"Launch…": "Başlat…",
"Close launch": "Başlatmayı kapat",
"Open folder": "Klasörü aç",
"Join a friend": "Bir arkadaşa katıl",
"Paste an invite code": "Bir davet kodu yapıştır",
"Join": "Katıl",
"Shortcut": "Kısayol",
"Remove shortcut": "Kısayolu kaldır",
"today": "bugün",
"yesterday": "dün",
"{days} days ago": "{days} gün önce",
"{time} in total · last played {when}": "toplam {time} · son oynanma: {when}",
"Not played yet": "Henüz oynanmadı",
"under a minute": "bir dakikadan az",
"PARTNER": "İŞ ORTAĞI",
"Your Minecraft server, {online}.": "Minecraft sunucunuz, {online}.",
"24/7 online": "7/24 çevrimiçi",
"Ready in under a minute": "Bir dakikadan kısa sürede hazır",
"Get my server": "Sunucumu al",
"15% off": "%15 indirim",
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Hazırlanmadı. Horizon, kendi Minecraft {version} sürümünüzün dosyalarına ihtiyaç duyuyor.",
"Prepared for Minecraft {version}": "Minecraft {version} için hazırlandı",
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Minecraft {version} için sesler olmadan hazırlandı: oyun sessiz olacak. Minecraft kurulumunuzdan sesleri kopyalamak için tekrar hazırlayın.",
"This build of the launcher has no Microsoft sign-in (HORIZON_MSA_CLIENT_ID), so it can't start the game.": "Başlatıcının bu derlemesinde Microsoft oturum açma özelliği (HORIZON_MSA_CLIENT_ID) bulunmuyor, bu nedenle oyunu başlatamıyor.",
"Sign in with a Microsoft account that owns Minecraft: Java Edition to play": "Oynamak için Minecraft: Java Edition'a sahip bir Microsoft hesabıyla oturum açın",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers. It also plays online servers with your name and skin.": "Horizon, diğer başlatıcılar gibi Minecraft: Java Edition'a sahip bir Microsoft hesabına ihtiyaç duyar. Ayrıca çevrimiçi sunucularda adınız ve dış görünümünüzle oynamanızı sağlar.",
"Sign in with Microsoft": "Microsoft ile oturum aç",
"Not now": "Şimdi değil",
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers.": "Horizon, diğer başlatıcılar gibi Minecraft: Java Edition'a sahip bir Microsoft hesabına ihtiyaç duyar.",
"Your worlds, offline names and settings stay where they are.": "Dünyalarınız, çevrimdışı adlarınız ve ayarlarınız olduğu yerde kalır.",
"Open {link} in your browser and enter this code:": "Tarayıcınızda {link} adresini açın ve şu kodu girin:",
"From my install": "Kurulumumdan",
"Download and prepare": "İndir ve hazırla",
"Not prepared. Play downloads Minecraft {version} from Mojang for your account and prepares it.": "Hazırlanmadı. Oyna düğmesi, hesabınız için Minecraft {version} sürümünü Mojang'dan indirir ve hazırlar.",
"Not prepared. Horizon can download Minecraft {version} from Mojang for your account, or use your own install.": "Hazırlanmadı. Horizon, hesabınız için Minecraft {version} sürümünü Mojang'dan indirebilir veya kendi kurulumunuzu kullanabilir.",
"Not prepared. Sign in with the Microsoft account that owns Minecraft {version} first.": "Hazırlanmadı. Önce Minecraft {version} sürümüne sahip olan Microsoft hesabıyla oturum açın.",
"Cancel": "İptal",
"Worlds": "Dünyalar",
"Packs": "Paketler",
"Install Horizon first.": "Önce Horizon'ı yükleyin.",
"Starting the build": "Derleme başlatılıyor",
"These mods will run with full access to your computer": "Bu modlar bilgisayarınıza tam erişimle çalışacaktır",
"They can read, change or delete your files, and use your accounts and network, like any program you run. Build them only if you trust their authors: the Mods page names them.": "Çalıştırdığınız herhangi bir program gibi dosyalarınızı okuyabilir, değiştirebilir veya silebilir, hesaplarınızı ve ağınızı kullanabilirler. Yalnızca yazarlarına güveniyorsanız bunları derleyin: Modlar sayfasında adları belirtilmektedir.",
"Mods: {mods}": "Modlar: {mods}",
"Import a modpack…": "Bir mod paketi içe aktar…",
"The build compiles the game: 20 to 30 minutes and a few GB of disk the first time (it downloads Rust), a few minutes after that.": "Derleme işlemi oyunu derler: İlk seferde 20-30 dakika ve birkaç GB disk alanı (Rust indirilir), sonrakilerde ise birkaç dakika sürer.",
"Delete old versions automatically": "Eski sürümleri otomatik olarak sil",
"Keeps the {count} newest, the one in use and the ones your instances use. Deleted versions install again from the Versions page.": "En yeni {count} sürümü, kullanımda olanı ve örneklerinizin kullandığı sürümleri saklar. Silinen sürümler Sürümler sayfasından tekrar yüklenebilir.",
"Installed versions use {size}.": "Yüklü sürümler {size} alan kaplıyor.",
"{size} on disk": "disk üzerinde {size}",
"Minecraft {version} (Java Edition). Play downloads it from Mojang for your account the first time, with the Java it needs.": "Minecraft {version} (Java Edition). İlk kullanımda \"Oyna\", hesaplarınız için gereksinim duyduğu Java ile birlikte Mojang'dan indirir.",
"Move {name} to the Trash, with its worlds and settings? You can get it back from there.": "{name} öğesini dünyaları ve ayarlarıyla birlikte çöp kutusuna taşımak istiyor musunuz? Oradan geri alabilirsiniz.",
"Delete…": "Sil…",
"Keep": "Sakla",
"Move to Trash": "Çöp Kutusuna Taşı",
"These mods were made for different Horizon versions": "Bu modlar farklı Horizon sürümleri için yapılmış",
"They often still work together. If the build fails, install versions made for the same Nightly.": "Genellikle yine de birlikte çalışırlar. Derleme başarısız olursa, aynı Nightly sürümü için yapılmış sürümleri yükleyin.",
"Build anyway": "Yine de derle",
"Folder": "Klasör",
"Version": "Sürüm",
"Vanilla": "Saf (Vanilla)",
"Modded": "Modlu",
"Minecraft version": "Minecraft sürümü",
"Search versions": "Sürümleri ara",
"Snapshots": "Anlık Görüntüler (Snapshot)",
"Old": "Eski",
"Asking Mojang for its versions…": "Mojang'dan sürümleri isteniyor…",
"No version matches.": "Eşleşen sürüm yok.",
"Release": "Kararlı Sürüm",
"Snapshot": "Anlık Görüntü",
"Beta": "Beta",
"Alpha": "Alpha",
"Loader": "Yükleyici (Loader)",
"newest stable version": "en yeni kararlı sürüm",
"Play downloads the game from Mojang for your account the first time, with the Java it needs.": "Oyna düğmesi, ilk seferde gereksinim duyduğu Java ile birlikte oyunu hesabınız için Mojang'dan indirir.",
"Mojang's version list isn't here yet.": "Mojang'ın sürüm listesi henüz burada değil.",
"Sign in to play": "Oynamak için oturum aç",
"No Minecraft install needed: Horizon downloads it from Mojang for your account.": "Minecraft kurulumu gerekmez: Horizon, hesabınız için onu Mojang'dan indirir.",
"Waiting for Microsoft… the window goes on by itself.": "Microsoft bekleniyor… pencere kendiliğinden ilerleyecek.",
"Downloading Minecraft {version} from Mojang · {done} of {total} files": "Minecraft {version} Mojang'dan indiriliyor · {total} dosyanın {done} tanesi tamamlandı",
"Downloading from Mojang · {done} of {total} files": "Mojang'dan indiriliyor · {total} dosyanın {done} tanesi tamamlandı",
"Getting ready…": "Hazırlanıyor…",
"about a minute left": "yaklaşık bir dakika kaldı",
"about {n} min left": "yaklaşık {n} dk kaldı",
"Game files": "Oyun dosyaları",
"Sounds": "Sesler",
"Overview": "Genel Bakış",
"Multiplayer": "Çok Oyunculu",
"Screenshots": "Ekran Görüntüleri",
"Console": "Konsol",
"Update all ({count})": "Tümünü güncelle ({count})",
"Add mods": "Mod ekle",
"{loader} mods": "{loader} modları",
"Horizon mods": "Horizon modları",
"Filter installed": "Yüklenenleri filtrele",
"Reading the mods…": "Modlar okunuyor…",
"No mods yet: Add mods finds some for this instance.": "Henüz mod yok: Mod ekle seçeneği bu örnek için bazı modlar bulabilir.",
"No mod matches.": "Eşleşen mod yok.",
"{source} · needed by another mod": "{source} · başka bir mod tarafından gerekiyor",
"Local file · {file}": "Yerel dosya · {file}",
"Update {version}": "{version} sürümüne güncelle",
"Choose image…": "Resim seç…",
"Mods and shader packs made for Horizon from its own reviewed registry, and resource packs from Modrinth. Each file is checked against its source's checksum before it reaches your game folder.": "Horizon için kendi incelenmiş kayıt defterinden hazırlanan mod ve gölgelendirici (shader) paketleri ile Modrinth kaynak paketleri. Her dosya oyun klasörünüze ulaşmadan önce kaynağının sağlama toplamına (checksum) karşı kontrol edilir.",
"Modpacks": "Mod Paketleri",
"Resource packs": "Kaynak Paketleri",
"Shader packs": "Gölgelendirici Paketleri",
"Data packs": "Veri Paketleri",
"Show everything": "Hepsini göster",
"CurseForge (coming soon)": "CurseForge (yakında)",
"Best match": "En iyi eşleşme",
"Relevance": "Alaka düzeyi",
"Downloads": "İndirmeler",
"Follows": "Takipçiler",
"Newest": "En yeni",
"Updated": "Güncellendi",
"Filters": "Filtreler",
"Filters ({count})": "Filtreler ({count})",
"Loading filters": "Filtreler yükleniyor",
"Game version": "Oyun sürümü",
"Environment": "Ortam",
"Client": "İstemci (Client)",
"Server": "Sunucu",
"Categories": "Kategoriler",
"Clear filters": "Filtreleri temizle",
"Browse modpacks": "Mod paketlerine göz at",
"Installs into {instance}: pick another instance on the left.": "{instance} içine yükleniyor: Soldan başka bir örnek seçin.",
"A data pack goes into a world, and {instance} has none yet: create one in the game, then come back.": "Bir veri paketi bir dünyaya gider ve {instance} öğesinin henüz dünyası yok: Oyunda bir tane oluşturun, ardından geri dönün.",
"Installs into a world of {instance}; the game turns the pack on when the world opens.": "{instance} öğesinin bir dünyasına yüklenir; oyun, dünya açıldığında paketi etkinleştirir.",
"Not checked: {error}": "Kontrol edilmedi: {error}",
"Checking its jar…": "Jar dosyası kontrol ediliyor…",
"No ratings yet": "Henüz değerlendirme yok",
"Updating": "Güncelleniyor",
"Installing": "Yükleniyor",
"Installed": "Yüklendi",
"Checking": "Kontrol ediliyor",
"Works": "Çalışıyor",
"Partly": "Kısmen",
"Doesn't run": "Çalışmıyor",
"Update to {version}": "{version} sürümüne güncelle",
"Removed {name}.": "{name} kaldırıldı.",
"On": "Açık",
"Off": "Kapalı",
"from {source}": "{source} kaynağından",
"needed by another pack": "başka bir paket tarafından gerekiyor",
"Not ported: {item}": "Bağlanmadı / Uyarlanmadı: {item}",
"checked: {checksum}": "kontrol edildi: {checksum}",
"Searching…": "Aranıyor…",
"Nothing found for {target}.": "{target} için hiçbir şey bulunamadı.",
"Nothing in Horizon's registry matches yet.": "Horizon kayıt defterinde henüz eşleşen bir şey yok.",
"Nothing found.": "Hiçbir şey bulunamadı.",
"Nothing found that runs in Horizon. Show everything lists the rest.": "Horizon'da çalışan hiçbir şey bulunamadı. \"Hepsini göster\" geri kalanını listeler.",
"Page": "Sayfa",
"More": "Daha fazla",
"by {author}": "Yazar: {author}",
"{count} downloads": "{count} indirme",
"{count} followers": "{count} takipçi",
"updated {date}": "güncellenme tarihi: {date}",
"Back to results": "Sonuçlara dön",
"Sign in with GitHub to rate": "Değerlendirmek için GitHub ile oturum aç",
"Ports": "Uyarlamalar",
"Doesn't port": "Uyarlanmıyor",
"{source} page": "{source} sayfası",
"Description": "Açıklama",
"Gallery": "Galeri",
"Loading…": "Yükleniyor…",
"No gallery images.": "Galeri resmi yok.",
"Add from GitHub": "GitHub'dan ekle",
"Add a native mod from GitHub": "GitHub'dan bir yerel mod ekle",
"A link to a repository (its latest release), a release, a branch or tag, or a commit. The launcher installs one exact release file or commit, never a branch that moves.": "Bir depoya (en son sürümüne), bir sürüme, dala (branch), etiketli sürüme (tag) veya bir commite ait bağlantı. Başlatıcı, değişen bir dalı asla değil, kesin bir sürüm dosyasını veya commiti yükler.",
"{id} is up to date.": "{id} güncel.",
"Asking GitHub about {owner}/{repo}…": "GitHub'a {owner}/{repo} hakkında soruluyor…",
"Downloading {source}…": "{source} indiriliyor…",
"Installing…": "Yükleniyor…",
"The release file {file} ({source}).": "Sürüm dosyası {file} ({source}).",
"The source of {source}. Its release ships {file}, a content mod: installing those from GitHub is not supported yet.": "{source} kaynağı. Sürümü {file} (bir içerik modu) barındırıyor: Bunları GitHub'dan yüklemek henüz desteklenmiyor.",
"The source at commit {commit}.": "{commit} commitindeki kaynak.",
"Not reviewed by the Horizon registry": "Horizon kayıt defteri tarafından incelenmedi",
"Native mods run with full access to this computer. Read the source before you install it.": "Yerel modlar bu bilgisayara tam erişimle çalışır. Yüklemeden önce kaynağını okuyun.",
"Open the source": "Kaynağı aç",
"I read it and trust it: install": "Okudum ve güveniyorum: yükle",
"Install all": "Tümünü yükle",
"{name} holds {count} mods. Install which?": "{name}, {count} mod içeriyor. Hangisi yüklensin?",
"Check for updates": "Güncellemeleri kontrol et",
"Source": "Kaynak",
"from GitHub: {source}": "GitHub'dan: {source}",
"No AI": "Yapay Zeka Yok",
"On: every AI feature is hidden and off. The game opens no control socket, and horizon-ctl and the MCP servers refuse.": "Açık: Tüm yapay zeka özellikleri gizlenir ve kapatılır. Oyun hiçbir kontrol soketi açmaz; horizon-ctl ve MCP sunucuları reddeder.",
"Hides and turns off every AI feature: agent control of the launcher and the game, horizon-ctl and the MCP servers.": "Başlatıcının ve oyunun yapay zeka ajanı kontrolünü, horizon-ctl ve MCP sunucularını içeren her yapay zeka özelliğini gizler ve kapatır.",
"Connect an AI agent": "Bir yapay zeka ajanı bağla",
"Your own agent (Claude Code, Codex, Cursor or any MCP client) can drive the launcher and the game it starts, with the same commands as the terminal. It runs on your computer and talks to Horizon there; it asks before deleting anything.": "Kendi ajanınız (Claude Code, Codex, Cursor veya herhangi bir MCP istemcisi), terminalle aynı komutları kullanarak başlatıcıyı ve başlattığı oyunu yönetebilir. Bilgisayarınızda çalışır ve oradaki Horizon ile iletişim kurar; bir şeyi silmeden önce onay ister.",
"Any MCP client": "Herhangi bir MCP istemcisi",
"~/.claude.json (or .mcp.json in a project)": "~/.claude.json (veya bir projede .mcp.json)",
"its MCP settings (a stdio server)": "MCP ayarları (bir stdio sunucusu)",
"Add to {client}": "{client} öğesine ekle",
"Running…": "Çalışıyor…",
"Copy": "Kopyala",
"{client} adds it with this command (it runs only when you click):": "{client} bunu şu komutla ekler (yalnızca tıkladığınızda çalışır):",
"Or put this in {file}:": "Veya bunu {file} dosyasına yerleştirin:",
"Put this in {file}:": "Bunu {file} dosyasına yerleştirin:",
"Test the connection": "Bağlantıyı test et",
"The MCP server answered with {count} tools. Now ask your agent: \"Say hello from Horizon\".": "MCP sunucusu {count} araçla yanıt verdi. Şimdi ajanıza sorun: \"Say hello from Horizon\".",
"The MCP server did not answer: {error}": "MCP sunucusu yanıt vermedi: {error}",
"What an agent can do, and the commands": "Bir ajanın yapabilecekleri ve komutlar",
"Added. Restart {program} if it is open, then ask it: \"Say hello from Horizon\".": "Eklendi. Açıksa {program} programını yeniden başlatın, ardından ona sorun: \"Say hello from Horizon\".",
"{program} said: {output}": "{program} şunu söyledi: {output}",
"{program} is not installed here (or not on the PATH): copy the config instead.": "{program} buraya kurulu değil (veya PATH üzerinde değil): bunun yerine yapılandırmayı kopyalayın.",
"Could not run {program}: {error}": "{program} çalıştırılamadı: {error}",
"Always on": "Her zaman açık",
"Could not save your choice: {error}": "Seçiminiz kaydedilemedi: {error}",
"Data collection": "Veri toplama",
"Help make Horizon faster and steadier?": "Horizon'ın daha hızlı ve kararlı olmasına yardım etmek ister misiniz?",
"Horizon can send a few things to its developers. Each switch is off until you turn it on, and Settings can change it any time.": "Horizon, geliştiricilerine birkaç şey gönderebilir. Siz açana kadar her anahtar kapalıdır ve Ayarlar kısmından istendiği zaman değiştirilebilir.",
"Hide what would be sent": "Gönderilecekleri gizle",
"View what would be sent": "Gönderilecekleri görüntüle",
"Send nothing": "Hiçbir şey gönderme",
"Save choices": "Seçimleri kaydet",
"No install ID: nothing has been collected": "Yükleme kimliği yok: hiçbir şey toplanmadı",
"Install ID {prefix}…{suffix}": "Yükleme Kimliği {prefix}…{suffix}",
"{count} waiting to be sent": "{count} adet gönderilmeyi bekliyor",
"Sending is turned off here (HORIZON_TELEMETRY_URL=off)": "Burada veri gönderimi kapatılmış (HORIZON_TELEMETRY_URL=off)",
"Delete everything?": "Her şeyi sil?",
"Delete my data": "Verilerimi sil",
"Your data": "Verileriniz",
"Each switch is off unless you turn it on. What it sends is below it; the privacy page on playhorizon.dev says how long it is kept.": "Siz açmadığınız sürece her anahtar kapalıdır. Neler gönderildiği aşağıda belirtilmiştir; playhorizon.dev adresindeki gizlilik sayfasında ne kadar süre saklandığı yazmaktadır.",
"Performance statistics": "Performans istatistikleri",
"After each session: frame-time percentiles and a 14-bucket histogram, tick times, peak memory and startup times, with your machine (OS, CPU and its cores and clocks, memory size, GPU, its memory and driver, drive type, screen size and refresh rate, laptop or desktop, on battery or not) and the settings that change performance.": "Her oturumdan sonra: Kare süresi yüzdelikleri ve 14 bölmeli histogram, onay süreleri, pik bellek ve başlangıç süreleri; makinenizle (İşletim Sistemi, CPU çekirdekleri ve saat hızları, bellek boyutu, GPU, belleği ve sürücüsü, sürücü tipi, ekran boyutu ve yenileme hızı, dizüstü veya masaüstü, pilde olup olmadığı) ve performansı değiştiren ayarlar ile birlikte.",
"Crash reports": "Çökme raporları",
"When the game crashes: the error, where in Horizon's code it happened, your graphics adapter and settings. No log, no world or server, no user name in paths.": "Oyun çöktüğünde: Hata, Horizon'ın kodunun neresinde gerçekleştiği, grafik bağdaştırıcınız ve ayarlarınız. Günlük yok, dünya veya sunucu yok, yollarda kullanıcı adı yok.",
"Bug reports": "Hata raporları",
"Only the reports you write and send yourself, with the game's log if you add it after reading it.": "Yalnızca okuduktan sonra eklediğiniz oyun günlüğüyle birlikte bizzat yazıp gönderdiğiniz raporlar.",
"Benchmark uploads": "Performans testi (Benchmark) yüklemeleri",
"The results of benchmarks you run, for the community results on playhorizon.dev.": "playhorizon.dev adresindeki topluluk sonuçları için çalıştırdığınız testlerin sonuçları.",
"Never sent: your account name or UUID, world names, server addresses, chat, or your IP address. Each report is dated to the day, under a random install ID you can delete.": "Asla gönderilmez: Hesap adınız veya UUID'niz, dünya adlarınız, sunucu adresleriniz, sohbetleriniz veya IP adresiniz. Her rapor, silebilir olduğunuz rastgele bir yükleme kimliği altında güne göre tarihlenir.",
"\"I'm playing\" ping": "\"Oynuyorum\" sinyali",
"While you play, the game sends an anonymous \"I'm playing\" ping once a minute, so the launcher can show how many people play Horizon right now. It holds: a random number made when the game starts and forgotten when it closes, the Horizon version, your OS (Linux, macOS or Windows), and whether you have a world open to LAN or friends. It doesn't hold: your account, your install ID, world or server names, or anything you do in the game, and the server doesn't keep your IP address. It is sent whatever the switches say.": "Siz oynarken oyun, başlatıcının şu anda kaç kişinin Horizon oynadığını gösterebilmesi için dakikada bir anonim \"Oynuyorum\" sinyali gönderir. Şunları içerir: Oyun başladığında oluşturulan ve kapandığında unutulan rastgele bir sayı, Horizon sürümü, işletim sisteminiz (Linux, macOS veya Windows) ve LAN'a ya da arkadaşlara açık bir dünyanız olup olmadığı. Şunları içermez: Hesabınız, yükleme kimliğiniz, dünya veya sunucu adları ya da oyunda yaptığınız herhangi bir şey. Sunucu IP adresinizi saklamaz. Anahtarlar ne derse desin bu gönderilir.",
"All": "Tümü",
"Ported": "Uyarlanmış",
"Partial": "Kısmi",
"Not yet": "Henüz değil",
"Not needed": "Gerek yok",
"Import": "İçe Aktar",
"Use these jars": "Bu jar dosyalarını kullan",
"Import a pack file…": "Bir paket dosyası içe aktar…",
"Import an instance folder…": "Bir örnek klasörü içe aktar…",
".mrpack (Modrinth), .zip (CurseForge), or a Prism, MultiMC or CurseForge instance. Horizon ports what it can on this computer, from your own downloads, and plays the pack as its own profile. Nothing is uploaded or shared.": ".mrpack (Modrinth), .zip (CurseForge) veya bir Prism, MultiMC ya da CurseForge örneği. Horizon, bu bilgisayarda yapabildiklerini kendi indirdiklerinizden uyarlar ve paketi kendi profili olarak oynatır. Hiçbir şey yüklenmez veya paylaşılmaz.",
"Your modpacks": "Mod paketleriniz",
"No modpack imported yet: import one, or find one below.": "Henüz içe aktarılan mod paketi yok: Bir tane içe aktarın veya aşağıdan bulun.",
"Remove for good": "Kalıcı olarak kaldır",
"Remove": "Kaldır",
"{version} from Horizon's registry, its own profile: {mods}": "Horizon kayıt defterinden {version}, kendi profili: {mods}",
"Working": "Çalışıyor",
"Done": "Tamamlandı",
"Play with {count} mods": "{count} mod ile oyna",
"Port {count} mods": "{count} modu uyarla",
"Use jars from my instance…": "Kendi örneğimdeki jar dosyalarını kullan…",
"Port gets the jars of the {count} mods this computer can port today (content with data and assets only) from the pack's own links, Modrinth, or your folder, and carries them over. The ports stay here, in {folder}.": "Uyarlama işlemi, bu bilgisayarın bugün uyarlayabileceği {count} modun jar dosyalarını (yalnızca veri ve varlık içeren içerikler) paketin kendi bağlantılarından, Modrinth'ten veya klasörünüzden alır ve buraya taşır. Uyarlanan dosyalar burada, {folder} içinde kalır.",
"Left off: {line}.": "Kaldığı yer: {line}.",
"Playing needs a prepared Horizon version (Play page).": "Oynamak için hazırlanmış bir Horizon sürümü gereklidir (Oyna sayfası).",
"{count} files": "{count} dosya",
"downloaded": "indirildi",
"copied from your folder": "klasörünüzden kopyalandı",
"already here": "zaten burada",
"only on CurseForge (CurseForge downloads coming soon: choose your installed instance instead)": "yalnızca CurseForge üzerinde (CurseForge indirmeleri yakında geliyor: bunun yerine yüklü örneğinizi seçin)",
"not found": "bulunamadı",
"failed": "başarısız oldu",
"Jars: {summary}.": "Jar dosyaları: {summary}.",
"Host a world for your friends on this computer. Create one, press Start, and give them the address.": "Bu bilgisayarda arkadaşlarınız için bir dünya barındırın. Bir tane oluşturun, Başlat'a basın ve onlara adresi verin.",
"Create a server": "Sunucu oluştur",
"Add a server folder…": "Bir sunucu klasörü ekle…",
"No server yet. Create one: it takes a few seconds, and with the defaults (tick the EULA, press Create) you get a friends-only survival world.": "Henüz sunucu yok. Bir tane oluşturun: Birkaç saniye sürer ve varsayılanlarla (EULA'yı işaretleyin, Oluştur'a basın) yalnızca arkadaşlara özel bir hayatta kalma dünyası elde edersiniz.",
"{online} of {max} players": "{max} oyuncudan {online} tanesi çevrimiçi",
"← All servers": "← Tüm sunucular",
"Start": "Başlat",
"Stop": "Durdur",
"Open the web panel": "Web panelini aç",
"Open the folder": "Klasörü aç",
"Saved. Restart to apply: {settings}.": "Kaydedildi. Uygulamak için yeniden başlatın: {settings}.",
"Restart now": "Şimdi yeniden başlat",
"Players": "Oyuncular",
"Backups": "Yedekler",
"Players online": "Çevrimiçi oyuncular",
"TPS · 20 is full speed": "TPS · 20 tam hızdır",
"MSPT · tick time": "MSPT · onay (tick) süresi",
"Address on this network": "Bu ağdaki adres",
"Friends on the same Wi-Fi: Multiplayer, Add Server, then paste this address.": "Aynı Wi-Fi ağındaki arkadaşlar: Çok Oyunculu > Sunucu Ekle adımlarını izleyip bu adresi yapıştırın.",
"Friends elsewhere can't reach it as it is: use Tailscale (free, nothing to change on your router, only people you invite), or forward TCP port {port} on your router to this computer.": "Başka yerlerdeki arkadaşlar bu haliyle erişemez: Tailscale kullanın (ücretsiz, yönlendiricinizde (router) değişiklik yapmaya gerek yok, yalnızca davet ettiğiniz kişiler) veya yönlendiricinizdeki {port} numaralı TCP bağlantı noktasını (port) bu bilgisayara yönlendirin.",
"Online now": "Şimdi çevrimiçi",
"Waiting for the server's log…": "Sunucu günlüğü bekleniyor…",
"The server's log shows here while it runs.": "Sunucu çalışırken günlükleri burada görünür.",
"Start the server to type commands": "Komut yazmak için sunucuyu başlatın",
"Updating: {step}": "Güncelleniyor: {step}",
"Running · restarted after a crash": "Çalışıyor · çökmüş ve yeniden başlatılmış",
"Running · paused while nobody plays": "Çalışıyor · kimse oynamadığı için duraklatıldı",
"Starting": "Başlatılıyor",
"Restarting": "Yeniden başlatılıyor",
"Stopping": "Durduruluyor",
"the message in the server list": "sunucu listesindeki mesaj",
"the port": "bağlantı noktası (port)",
"most players": "maksimum oyuncu sayısı",
"view distance": "görüş mesafesi",
"simulation distance": "simülasyon mesafesi",
"account checks": "hesap kontrolleri",
"Friends over the internet · Tailscale": "İnternet üzerinden arkadaşlar · Tailscale",
"Check again": "Tekrar kontrol et",
"Looking for Tailscale…": "Tailscale aranıyor…",
"Tailscale lets friends anywhere join without opening your router to the internet. It's free: install it, sign in, then invite them here.": "Tailscale, yönlendiricinizi internete açmak zorunda kalmadan her yerden arkadaşlarınızın katılmasına izin verir. Ücretsizdir: yükleyin, oturum açın ve ardından onları buraya davet edin.",
"Install Tailscale": "Tailscale'i yükle",
"Open Tailscale": "Tailscale'i aç",
"Tailscale is installed but not running. Open it and switch it on.": "Tailscale yüklü ancak çalışmıyor. Açın ve etkinleştirin.",
"Sign in to Tailscale": "Tailscale'de oturum aç",
"Tailscale is installed. Sign in with your own account in Tailscale (Horizon never sees it), then come back here.": "Tailscale yüklü. Tailscale'de kendi hesabınızla oturum açın (Horizon bunu asla görmez), ardından buraya geri dönün.",
"On Linux, run `{cmd}` in a terminal and open the link it prints.": "Linux üzerinde, bir terminalde `{cmd}` komutunu çalıştırın ve çıktıda verdiği bağlantıyı açın.",
"Or {address}. Friends type it in Multiplayer, Add Server, once this computer is shared with them and Tailscale runs on theirs.": "Veya {address}. Bu bilgisayar kendileriyle paylaşıldığında ve Tailscale kendi bilgisayarlarında çalıştığında arkadaşlar bunu Çok Oyunculu > Sunucu Ekle kısmına yazarlar.",
"Friends type it in Multiplayer, Add Server, once this computer is shared with them and Tailscale runs on theirs.": "Bu bilgisayar kendileriyle paylaşıldığında ve Tailscale kendi bilgisayarlarında çalıştığında arkadaşlar bunu Çok Oyunculu > Sunucu Ekle kısmına yazarlar.",
"Invite a friend": "Bir arkadaşını davet et",
"Horizon never turns on Funnel: the server isn't put on the public internet, only shared with the people you invite.": "Horizon asla Funnel özelliğini açmaz: Sunucu genel internete açılmaz, yalnızca davet ettiğiniz kişilerle paylaşılır.",
"Share this computer with a friend: they get it in their own Tailscale, and nothing else of yours. Do it in Tailscale's admin console (Machines, this computer, Share), or here with an access token you make there.": "Bu bilgisayarı bir arkadaşınızla paylaşın: Kendi Tailscale hesaplarında yalnızca bunu görürler, başka hiçbir şeyinizi göremezler. Bunu Tailscale'in yönetim konsolundan (Makineler, bu bilgisayar, Paylaş) veya burada oluşturacağınız bir erişim belirteciyle (access token) yapın.",
"Open the admin console": "Yönetim konsolunu aç",
"Save": "Kaydet",
"Make a token": "Bir belirteç (token) oluştur",
"Settings, Keys, Generate access token. It's kept in this computer's keychain and only sent to Tailscale.": "Settings, Keys, Generate access token. Bu bilgisayarın anahtarlığında saklanır ve yalnızca Tailscale'e gönderilir.",
"Use an access token…": "Bir erişim belirteci kullan…",
"Make an invitation": "Bir davetiye oluştur",
"One invitation per friend. With an email Tailscale sends it; without, send them the link. They accept it in their own Tailscale account.": "Her arkadaş için bir davetiye. E-posta adresiyle Tailscale bunu gönderir; e-posta yoksa bağlantıyı onlara siz gönderin. Kendi Tailscale hesaplarında bunu kabul ederler.",
"Minecraft account needs attention": "Minecraft hesabı dikkat gerektiriyor"
}
+1
View File
@@ -24,6 +24,7 @@
"horizon-narrator": "engine",
"horizon-prepare": "engine",
"horizon-protocol": "engine",
"horizon-render-pipeline": "engine",
"horizon-server-files": "engine",
"horizon-system": "engine",
"horizon-threads": "engine",
+12 -12
View File
@@ -34,16 +34,16 @@ def stamp(check, only=None):
result = subprocess.run(
["git", "log", "--first-parent", "--reverse", "--diff-merges=first-parent",
"--format=", "--name-only", "--diff-filter=A", "--", str(FRAGMENTS)],
cwd=ROOT, check=True, text=True, capture_output=True,
cwd=ROOT, check=True, encoding="utf-8", capture_output=True,
)
pending = {p.relative_to(ROOT).as_posix(): p for p in paths}
ordered = [pending.pop(line) for line in result.stdout.splitlines() if line in pending]
# A developer can stamp uncommitted fragments for a local test.
ordered.extend(pending[name] for name in sorted(pending))
source = (ROOT / SOURCE).read_text()
docs = (ROOT / DOCS).read_text()
catalog = json.loads((ROOT / CATALOG).read_text())
source = (ROOT / SOURCE).read_text(encoding="utf-8")
docs = (ROOT / DOCS).read_text(encoding="utf-8")
catalog = json.loads((ROOT / CATALOG).read_text(encoding="utf-8"))
match = re.search(r"^pub\(crate\) const NATIVE_HOOKS_API: u32 = (\d+);$", source, re.M)
if not match:
raise ValueError("missing numeric NATIVE_HOOKS_API")
@@ -53,7 +53,7 @@ def stamp(check, only=None):
extra = {}
# Append the selected entries in filename order, as the build-time generator reads fragments.
for path in sorted(ordered):
fragment = json.loads(path.read_text())
fragment = json.loads(path.read_text(encoding="utf-8"))
for hook in fragment["hooks"]:
if hook["name"] in names:
raise ValueError(f"duplicate hook: {hook['name']}")
@@ -68,7 +68,7 @@ def stamp(check, only=None):
helper_names.add(helper["name"])
catalog.setdefault("helpers", []).append(helper)
for path in ordered:
fragment = json.loads(path.read_text())
fragment = json.loads(path.read_text(encoding="utf-8"))
version += 1
marker = f"@API:{path.stem}@"
rust_summary = fragment["rust_summary"].replace(marker, str(version))
@@ -87,7 +87,7 @@ def stamp(check, only=None):
file = Path(filename)
if file.is_absolute() or ".." in file.parts or file in (SOURCE, DOCS, CATALOG, MOD_API):
raise ValueError(f"invalid comment path: {filename}")
text = extra.get(file, (ROOT / file).read_text())
text = extra.get(file, (ROOT / file).read_text(encoding="utf-8"))
if marker not in text:
raise ValueError(f"missing {marker} in {filename}")
extra[file] = text.replace(marker, str(version))
@@ -95,17 +95,17 @@ def stamp(check, only=None):
source = source.replace(marker, str(version))
docs = docs.replace(marker, str(version))
if (ROOT / MOD_API).exists():
text = extra.get(MOD_API, (ROOT / MOD_API).read_text())
text = extra.get(MOD_API, (ROOT / MOD_API).read_text(encoding="utf-8"))
extra[MOD_API] = text.replace(marker, str(version))
source = re.sub(
r"^(pub\(crate\) const NATIVE_HOOKS_API: u32 = )\d+;$",
lambda m: f"{m.group(1)}{version};", source, count=1, flags=re.M)
(ROOT / SOURCE).write_text(source)
(ROOT / DOCS).write_text(docs)
(ROOT / CATALOG).write_text(json.dumps(catalog, indent=2, ensure_ascii=False) + "\n")
(ROOT / SOURCE).write_text(source, encoding="utf-8")
(ROOT / DOCS).write_text(docs, encoding="utf-8")
(ROOT / CATALOG).write_text(json.dumps(catalog, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
for file, content in extra.items():
(ROOT / file).write_text(content)
(ROOT / file).write_text(content, encoding="utf-8")
for path in ordered:
path.unlink()
print(f"Stamped {len(ordered)} fragment(s), through API {version}")