Compare commits
15
Commits
e5e369a693
...
f949c169a2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f949c169a2 | ||
|
|
cf7ea5617e | ||
|
|
217c346c02 | ||
|
|
4f450e9612 | ||
|
|
c5d25faa7c | ||
|
|
784136142a | ||
|
|
a9f33f3a52 | ||
|
|
60e10b35a3 | ||
|
|
d20b0fb11a | ||
|
|
ccc9695250 | ||
|
|
4fa9a6ab81 | ||
|
|
a15c912ad0 | ||
|
|
8f56e2f780 | ||
|
|
e18bd3ccbb | ||
|
|
a84319bb2f |
No files matched your search
@@ -7,3 +7,4 @@ immersive_paintings_cache/
|
||||
# Large or regenerable local artifacts
|
||||
*.tar.gz
|
||||
analysis/client-perf/proposed-configs/out/
|
||||
__pycache__/
|
||||
+13
-5
@@ -65,8 +65,16 @@ config/paxi/datapacks/*
|
||||
# Default Options copies them into place client-side, only when the target is absent.
|
||||
!config/defaultoptions
|
||||
!config/defaultoptions/**
|
||||
# Server spawn tuning (analysis/client-perf/SPAWN-TUNING.md); partial files, the mods
|
||||
# fill in defaults for every other key. Enforced: the server reads them from here.
|
||||
!config/alexsmobs-common.toml
|
||||
!config/fowlplay.json5
|
||||
!config/crittersandcompanions-common.json5
|
||||
# Spawn tuning reverted 2026-10-18: bench B14 showed no measurable load reduction.
|
||||
# ModernFix mixin overrides (build.py SHIP; enforced - ModernFix reads it before
|
||||
# Default Options runs). dynamic_resources=true per bench B6.
|
||||
!config/modernfix-mixins.properties
|
||||
# Player tier overrides (build.py --ship) plus the per-machine setup script that
|
||||
# applies a tier to an existing install (see PLAYER-TUNING.md §1). Must come after
|
||||
# the blanket config/* and *.json patterns above (last matching pattern wins), and
|
||||
# the directory itself must be re-included before anything under it.
|
||||
!config/onion-perf
|
||||
!config/onion-perf/**
|
||||
!onion-perf-setup.sh
|
||||
!onion-perf-setup.ps1
|
||||
!onion-perf-setup.bat
|
||||
+151
@@ -448,3 +448,154 @@ uncapped frametimes, then run B0→B14 per `BENCH-MATRIX.md`.
|
||||
### Pushed
|
||||
`8e8483d` (owner, CAC keys). This session's analysis/harness commit follows; it touches no
|
||||
shipped file.
|
||||
|
||||
## Session 2026-10-14 (d) — state capture for handoff (orchestrator)
|
||||
|
||||
No pack changes; HEAD stayed `e5e369a`. Three read-only scouts inventoried the
|
||||
interrupted session's artifacts; findings folded into the new **`analysis/state/`**
|
||||
directory (6 markdown files: README, pack+environment, timeline, current-state, plan,
|
||||
artifact-map) — that directory is now the canonical onboarding point for a new agent.
|
||||
|
||||
Facts established this session (all evidence-backed):
|
||||
- The 20:34 FML crash report is the **CAC spawn-key NPE** (fixed by `8e8483d`) — *not*
|
||||
the WATUT crash. No crash report exists for WATUT yet; it remains open and blocking.
|
||||
- The dedicated server from the boot check is **still running** (tmux `onion-mc`,
|
||||
PID 1699563, since 21:01) with an unidentified RCON keepalive polling every 30 s.
|
||||
Left alone per rules.
|
||||
- Working-tree diffs (`akashictome`, `alexsmobs` configs) are benign boot re-saves:
|
||||
whitelist intact; tuned spawn values retained. Per rule, restore before staging —
|
||||
not committed.
|
||||
- Bench instance `~/onion-bench/prism/instances/OnionBench` is fully provisioned
|
||||
(`.minecraft`, `saves/onionbench` 26 MB, Default Options seeded, bench pins,
|
||||
`BENCH_VARIANT.txt = C1`). One earlier scout misread it as unprovisioned — verified
|
||||
false by direct `ls`.
|
||||
- Jar cache `/tmp/onion/jars` is gone (ephemeral; `analysis/audit/fetch.py` rebuilds).
|
||||
|
||||
State of the effort at capture: **Phase 2 begun, zero matrix blocks run.** Next agent:
|
||||
follow `analysis/state/04-plan.md` §"Next actions" (WATUT → verify uncapped frametimes
|
||||
→ finish S4 → B0→B14 → ship → report). `analysis/state/` itself is uncommitted — fold
|
||||
it into the next commit.
|
||||
|
||||
## Session 2026-10-15 (b) — WATUT crash root-caused + fixed; harness v2
|
||||
|
||||
Commits: `a84319b` (fix(mods): MidnightLib 1.10.0 + `analysis/state/`), `e18bd3c`
|
||||
(feat(bench): harness v2). Not pushed.
|
||||
|
||||
- **WATUT crash = victim, not cause.** Fright's Delight 1.4.8 jar-in-jars MidnightLib
|
||||
1.9.2; its `MidnightConfig.init` races under NeoForge parallel mod construction →
|
||||
`ConcurrentModificationException` → broken mod state → `mod_resources` never loads →
|
||||
WATUT's unguarded `reloadShaders` mixin throws on `particle.json`. Fix: standalone
|
||||
`midnightlib` 1.10.0 (`side=both`; upstream fix 1fe69729, synchronizedMap). FML picks
|
||||
it over the JiJ copy (log: "Attempted to select a dependency jar ... Using Mod File").
|
||||
Verified 10/10 cold launches to world join, 0 CME, all on 1.10.0. Frightsdelight
|
||||
doesn't touch `entries` (binary-compat risk checked). **Dedicated server still runs
|
||||
1.9.2 until its next restart + jar install** — server-side exposure untested.
|
||||
- **Uncapped frametimes verified**: C1/S1 p50 2.9 ms, p99 9.5 ms, avg 297 fps
|
||||
(harness v2 smoke, `~/onion-bench/results/session.jsonl`, label `smoke`). The earlier
|
||||
60 fps row in `harness-results.csv` is void.
|
||||
- **Harness v1 was silently broken**: KDE Plasma is Wayland; xdotool into Xwayland `:1`
|
||||
drops input when a native window has focus → no `/function`, no markers, ever. Also:
|
||||
partial `options.txt` re-triggered the accessibility onboarding screen (blocks
|
||||
`--world`), and FTB Backups fired ~7 s after every join (inside capture windows).
|
||||
- **Harness v2** (`analysis/client-perf/harness/bench.py`, `analyze.py`, `tests/`,
|
||||
README): `boot --n N --join` reliability loop; `session` drives scenes via a
|
||||
per-session generated datapack (armed only during the session, disarmed in
|
||||
`finally`), log markers with ms timestamps, CSV picked by birth time, validity flags,
|
||||
full metadata JSONL; `analyze.py` applies the BENCH-MATRIX win rule. 18 offline tests.
|
||||
- Bench-instance-only edits: FTB Backups `auto: false`
|
||||
(`config/ftbbackups3-server.snbt`); harness pins in `options.txt` (re-applied per run).
|
||||
- Observed noise: the idle dedicated server uses 23–78 % of a core; amdgpu DPM is
|
||||
`auto`; the bench window shares the owner's live desktop.
|
||||
|
||||
Next: owner calls on noise controls (see harness README / session report), then S4
|
||||
build → B0 (`bench.py session --variant C0 --plan s1 --reps 3 --label B0`, ×3
|
||||
sessions) → matrix.
|
||||
|
||||
## Session 2026-10-16 — bench world finished, harness v3, matrix running
|
||||
|
||||
Owner: stop the dedicated server while benching (done via console `stop`; tmux
|
||||
`onion-mc` is gone — restart with `./start.sh` in tmux when benching ends; packwiz will
|
||||
pull MidnightLib 1.10.0, jar also dropped in `mods/`). Gamescope integration approved.
|
||||
|
||||
- **Harness**: `--display gamescope-headless` default @1920x1080 (~2 % cost vs desktop,
|
||||
isolates from the shared KDE Wayland desktop). Per-session `--shader/--shader-options/
|
||||
--dh/--disable-mods/--add-mods/--world-zip` with a crash-safe restore journal
|
||||
(`bench.py restore`). Plan forces **spectator** — elevated scene cameras previously fell
|
||||
(S4 measured the sea floor at y=34). `shader_not_loaded` flag. `matrix.py` +
|
||||
`matrix.toml` = resumable B0–B14 runner; B7 profiles generated from pack
|
||||
`profile.*` lines (`gen_shader_profiles.py`); B12b trial jars in `~/onion-bench/trials/`.
|
||||
Post-join settle 45 s (rep 1 still warming at 10 s). Commits `c6341d0`, `4fa9a6a`.
|
||||
- **Bench world** built headlessly on `~/onion-bench/server-bench` via RCON:
|
||||
S4 = 4 Sable sub-levels (5×7×15 planks/glass, levitite floor + 2 interior layers —
|
||||
the 1-layer design sank ~14 blocks per physics wake), via `/sable assemble area`;
|
||||
stable at y≈169.24 across restarts and a 60 s client session. Removed 370 stray
|
||||
force-loaded chunks (S2/S3/S4 ticked in every scene). DH LODs pre-generated with
|
||||
`/dh pregen start minecraft:overworld <x> 0 256` at x=0 and x=3680 (1.2 GB sqlite).
|
||||
S2 rotates (Speed 16 sampled), S3 = 30 villagers.
|
||||
Zips in `~/onion-bench/worlds/` (sha256):
|
||||
`bench-world-v1.zip` 144caf5fcdfea1a0eac0c787fdcded949e1a67df6ac112400f37be30ce7fb76e,
|
||||
`-s4-1.zip` 213b9b8871be527d0b5ac2e34615e8c2c1e3b206277fb37549a3014e2b895a38,
|
||||
`-s4-0.zip` 6b2a2bbd16f9f2e5aba34d53b09752d30fb7f9c3a97c24c46131b54d12bdc234.
|
||||
- **Matrix** run 1 (tmux `bench-matrix`, log `~/onion-bench/results/matrix-run1.log`):
|
||||
B0,B1,B2,B3,B4,B5,B6,B7(noshader),B8–B12a. B0 noise (settle 45): p99 spread 0.40 ms,
|
||||
1%-low spread 6.6 fps. **B2: C1 beats C0 on every scene** (S1 p99 8.23 vs 9.33;
|
||||
S3 1%-low 186 vs 110) → no bisect stop. Early B3: dh-qlow wins S1, dh-ssao loses,
|
||||
dh-thr4 loses S5 (threads stay 2). Analyze with `--since 2026-10-16T22:24`.
|
||||
- Remaining after run 1: B4h (`--param gc=`), B7 shader rows, B12b, B13, B14, ship,
|
||||
owner report. Analysis proxy-only; noise floor gpu_dpm_auto (no root).
|
||||
- **2026-10-17 06:00 pause**: matrix run 1 finished first (**56/56 ok, 0 failures**,
|
||||
B0–B12a incl. proxy B5). Timer `pause-bench-start-server.sh` started the server, but
|
||||
tmux was born inside the oneshot service's cgroup and systemd killed it (and the
|
||||
server, SIGKILL 1 s after `Done`, no players) when the unit exited. Restarted 06:05 in
|
||||
its own scope (`systemd-run --user --scope tmux …`, unit `onion-mc-tmux.scope`);
|
||||
script fixed the same way. **Server is up (25565).**
|
||||
- B4 GC verdict: J1 vs J0 S5 1%-low +1.3 fps < 6.6 spread → tie → **J0**.
|
||||
- Resume (stop the server first): `cd analysis/client-perf/harness &&
|
||||
python3 matrix.py run B4h,B7,B12b --param gc=J0` (20 sessions, ~3.5 h). Then B13
|
||||
(manual spark), B14 (manual spawn counts), ship, owner report.
|
||||
|
||||
## Session 2026-10-18 — run 2 + B14 done; server back
|
||||
|
||||
Overnight: 23:25 pause (owner) → 01:26 resume timer → matrix run 2 finished 03:18
|
||||
(B4h, B7, B12b; all sessions ok after retries) → B7.noshader re-run on default JVM →
|
||||
B14 defaults + shipped (33 rows each, biome_ok, config check ok) → server up 04:31.
|
||||
06:00 safety timer saw the server running and left it (its "not Done" warning was a
|
||||
scrolled-pane false alarm; readiness check now uses port 25565).
|
||||
|
||||
Results (full tables in `analysis/client-perf/RESULTS-proxy.md` §Run 2):
|
||||
- B4h: smallest heap within spread = **5 GB** (4 GB loses S5) → PLAYER-TUNING note.
|
||||
- B7: shaders' big cost is S2 — Flywheel backends drop (974 → ~120 fps avg) for every
|
||||
profile; MakeUp shadowless near-free elsewhere, Complementary ~2× on S4.
|
||||
- B12b: **Iris Flywheel Compat wins hugely under shaders** (S2 p99 13.3 → 4.0);
|
||||
**Flerovium wins S3** (entities; B12b's S3 baseline anomalous, so confirm once).
|
||||
Gnetum tie; BBE, Super Resolution, Ixeris (+1 crash), Cull Leaves rejected.
|
||||
- B14 (n=1): rule says back off, but driven by one goose/duck herd at the warm-ocean
|
||||
site; sea bunny never spawned in either config → inconclusive, needs ≥3 runs/config.
|
||||
- Super Resolution's first launch rewrote bench `fml.toml` (`earlyWindowControl=false`,
|
||||
unjournaled) → reset. Harness should snapshot/diff the whole config tree per session.
|
||||
|
||||
Remaining: B6 missing-texture check → ship modernfix-mixins; add Iris Flywheel Compat +
|
||||
Flerovium (visual check, then `packwiz modrinth add`, then re-run B0); DH low-quality
|
||||
visual check; B13 spark; B14 ×3 more; ship via build.py; owner report.
|
||||
|
||||
## Session 2026-10-18 (b) — shipped, run 3, owner report
|
||||
|
||||
- Shipped `7841361`: ModernFix dynamic_resources (enforced config; owner waived texture
|
||||
check), Flerovium 1.1.2, Iris Flywheel Compat 2.4.0. Deck already had DH LOW.
|
||||
- **Incident**: 7841361 refreshed the index while alexsmobs/akashictome held boot
|
||||
re-saves → server packwiz install "Hash invalid" → server down 14:06–14:18. Fixed
|
||||
`c5d25fa`. Rule: `git checkout -- config/` **before** `packwiz refresh`, not just
|
||||
before staging.
|
||||
- Run 3 (chain-0418): B0v2 noise p99 0.41 ms / 1%-low 1.6 fps. **Flerovium did not
|
||||
hold up** (S1 loss, S3 too noisy) → recommend removal (owner decision pending).
|
||||
B14 ×4: totals ~flat (jungle 130→128, river 208→212, warm ocean 145→136) → spawn
|
||||
tuning harmless but ineffective; keep/revert is the owner's call.
|
||||
- Docs: `analysis/client-perf/OWNER-REPORT.md` (new), RESULTS-proxy §Run 3,
|
||||
PLAYER-TUNING (5 GB heap; Iris Flywheel Compat under shaders).
|
||||
- Verified ModernFix honors the shipped override on the server boot log.
|
||||
- Open: owner decisions (Flerovium, spawn tuning); B13 spark (optional); fleet
|
||||
confirmation; push when the owner says so.
|
||||
- Owner approved: **Flerovium removed, spawn tuning reverted** (`217c346`). Server was
|
||||
stopped while the three spawn files were deleted so it couldn't keep re-saved tuned
|
||||
copies; restarted, Alex's Mobs regenerated defaults (gazelle weight 40). Bench
|
||||
instance jar removed too. b14.py's `shipped` config now has nothing to ship.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Client performance — owner report (2026-10-18)
|
||||
|
||||
Plain-language summary of the Phase 2 benchmarks. Full tables and method:
|
||||
[`RESULTS-proxy.md`](RESULTS-proxy.md). Player-facing advice:
|
||||
[`PLAYER-TUNING.md`](PLAYER-TUNING.md). All numbers come from **one stand-in machine**
|
||||
(Ryzen 9 9900X / RX 7800 XT, 1920×1080), not the fleet machines — see Caveats.
|
||||
|
||||
## TL;DR
|
||||
|
||||
- The tuned defaults we already ship beat upstream defaults in **every** test scene, and
|
||||
start the game about 6 s faster.
|
||||
- This session shipped faster startup (ModernFix dynamic resources, −7.6 s) and a fix that
|
||||
keeps Create's fast renderer working with shaders on (Iris Flywheel Compat).
|
||||
- Shaders stay **opt-in**. MakeUp shadowless is nearly free except on big Create builds;
|
||||
Complementary is expensive. Fresh Animations is the most expensive optional visual.
|
||||
- Players should give Minecraft **5 GB** of memory — 4 GB is measurably worse, more is not faster.
|
||||
- Two decisions for you: **remove Flerovium** (its win didn't hold up — I recommend yes), and
|
||||
**keep or revert the spawn tuning** (harmless, but it doesn't measurably cut mob load).
|
||||
|
||||
## What shipped
|
||||
|
||||
| Commit | Change |
|
||||
|---|---|
|
||||
| `7841361` | ModernFix `mixin.perf.dynamic_resources=true` (enforced `config/modernfix-mixins.properties`); added **Flerovium 1.1.2** and **Iris Flywheel Compat 2.4.0** (both client-only) |
|
||||
| `c5d25fa` | Fix: `7841361`'s pack index held hashes of server-re-saved spawn configs, so the server's install failed ("Hash invalid") and it didn't start at 14:06. Re-hashed; server up 14:18. Nothing was pushed. |
|
||||
|
||||
The Deck tier already uses Distant Horizons' LOW quality preset — the setting that won the
|
||||
DH test — so no DH change was needed. Nothing has been pushed yet.
|
||||
|
||||
## Measured gains
|
||||
|
||||
| What | Before | After |
|
||||
|---|---|---|
|
||||
| Tuned defaults vs upstream — vista p99 frametime | 9.33 ms | 8.23 ms |
|
||||
| Tuned defaults vs upstream — mob pen 1 % low | 110 fps | 186 fps |
|
||||
| Tuned defaults vs upstream — startup | 31.4 s | 25.4 s |
|
||||
| ModernFix dynamic resources — startup | 33.9 s | 26.3 s (plus better frametimes) |
|
||||
| Iris Flywheel Compat — Create floor with MakeUp shaders, p99 | 13.26 ms | 3.98 ms |
|
||||
| Iris Flywheel Compat — same scene, average fps | 117 | 644 |
|
||||
|
||||
(Lower p99 = fewer stutters; higher 1 % low = smoother worst moments.)
|
||||
|
||||
## Shaders
|
||||
|
||||
- Keep them **opt-in** (the pack downloads them; nothing turns them on).
|
||||
- **MakeUp – Ultra Fast, shadowless profile** is close to free on terrain and airships.
|
||||
- Without help, every shaderpack turns off Create's fast renderer: the Create floor dropped
|
||||
from ~974 to ~120 fps average in every profile. **Iris Flywheel Compat**, now shipped, fixes
|
||||
that (table above). It's an unofficial mod, so it may break when Create or Iris update.
|
||||
- **Complementary** is costly: about 2.7 ms worse p99 on the vista and about twice the frame
|
||||
time on the airship scene.
|
||||
- Shader **shadows corrupt airships** (Iris issue #3164) — recommend shadowless profiles.
|
||||
|
||||
## Fresh Animations
|
||||
|
||||
Mob-pen p99 goes from 4.32 ms to **8.65 ms** with Fresh Animations, and **10.20 ms** with
|
||||
the Extensions pack too. Keep it opt-in; Deck players shouldn't use it.
|
||||
|
||||
## Memory
|
||||
|
||||
**5 GB** is the smallest heap that performed as well as the best. 4 GB lost about 23 fps of
|
||||
1 % low on the teleport tour; 6 and 8 GB were no better. Players set this in their launcher.
|
||||
|
||||
## Spawn tuning
|
||||
|
||||
Measured 4 times per config at three biome sites. Total mob counts barely changed
|
||||
(jungle 129.5 → 128.0, river 208.0 → 211.5, warm ocean 145.0 → 136.0). The tuned species
|
||||
are a bit rarer, as intended, and nothing really disappeared (the automated "back off"
|
||||
flag came from species that only ever showed up once per run). **It's harmless, but it
|
||||
doesn't measurably reduce the load.** No config was changed.
|
||||
|
||||
## Decisions needed from you
|
||||
|
||||
> **Resolved 2026-10-18:** both removed in `217c346` (Flerovium dropped; spawn tuning
|
||||
> reverted, the mods regenerated their defaults on the server).
|
||||
|
||||
1. **Remove Flerovium?** Its earlier win was measured against an unusually bad baseline.
|
||||
On a fair re-test it was worse on the vista and a tie on the mob pen. **Recommended: yes.**
|
||||
2. **Spawn tuning: keep or revert?** Keeping it costs nothing; reverting returns the mods'
|
||||
own spawn rates. Either is fine performance-wise.
|
||||
|
||||
## Caveats
|
||||
|
||||
- **One stand-in machine.** The fleet machines (Steam Deck, 7700K + GTX 1060, 6700K +
|
||||
1080 Ti) were unreachable, so results are relative and unconfirmed on real player
|
||||
hardware. This GPU makes some costs (airships, DH distance) look cheaper than they are
|
||||
on older cards.
|
||||
- GPU clocks weren't pinned (no root access), which adds a little noise.
|
||||
- A harness bug ran part of the first run on the wrong Java settings. Comparisons inside
|
||||
each test are still fair; details in `RESULTS-proxy.md`.
|
||||
|
||||
## Not done
|
||||
|
||||
- Spark CPU profile of the worst scene (B13).
|
||||
- Confirmation on the fleet machines.
|
||||
- The airship decay test (B5), which needs the fleet machines.
|
||||
|
||||
## Please report texture problems
|
||||
|
||||
ModernFix dynamic resources loads textures and models on demand. We skipped the
|
||||
missing-texture check at your request, so if anyone sees purple/black "missing" blocks or
|
||||
items in play, report the block/item name.
|
||||
@@ -1,7 +1,7 @@
|
||||
# Onion MC: performance settings for players
|
||||
|
||||
These are things the pack can't set for you, plus the settings to change for your machine.
|
||||
Values marked *(provisional)* are waiting on Phase 2 benchmarks and may change.
|
||||
All values below are final (benchmarked in Phase 2; see `RESULTS-proxy.md` in this folder).
|
||||
|
||||
## 1. Which tier are you?
|
||||
|
||||
@@ -12,24 +12,34 @@ Values marked *(provisional)* are waiting on Phase 2 benchmarks and may change.
|
||||
| **High** | 8 GB+ VRAM card | GTX 1080 Ti, RTX 3060+ |
|
||||
|
||||
The pack's defaults are the **mid** tier. They're applied only when the config file doesn't
|
||||
exist yet. **If you played before the defaults shipped**, close the game and delete these
|
||||
files from the instance folder to pick them up on the next launch. This resets those mods'
|
||||
settings.
|
||||
exist yet. **If you played before the defaults shipped**, close Minecraft and run the
|
||||
tier setup script in the instance folder to apply a tier over your current settings:
|
||||
|
||||
- **Windows:** double-click `onion-perf-setup.bat` in the instance folder.
|
||||
- **Linux / Steam Deck / macOS:** run `sh onion-perf-setup.sh` in the instance folder.
|
||||
|
||||
Pick your tier when prompted. The script backs up everything it changes (to
|
||||
`onion-perf-backup/<date-time>/`), replaces these 7 config files (the **deck** tier also
|
||||
ships `config/sodium-options.json`; **high** has the same 7 as mid), and merges the tuned
|
||||
video keys into `options.txt` — your keybinds, audio and every other setting stay as
|
||||
they are. Nothing else is touched.
|
||||
|
||||
Manual fallback: copy the files below from `config/onion-perf/<tier>/` in the instance
|
||||
folder over your own configs, and set the `options.txt` values from the table below by
|
||||
hand (don't copy a tier's `options.txt` over yours — that resets all your video, audio
|
||||
and control settings, keybinds included):
|
||||
|
||||
```
|
||||
config/DistantHorizons.toml
|
||||
config/sodium-options.json
|
||||
config/entity_model_features.json
|
||||
config/flywheel-client.toml
|
||||
config/lambdynlights.toml
|
||||
config/ssrd-common.toml
|
||||
config/sound_physics_remastered/soundphysics.properties
|
||||
config/sound_physics_remastered/sound_rates.properties
|
||||
```
|
||||
|
||||
Don't delete `options.txt`: that resets all your video, audio and control settings,
|
||||
keybinds included. Set the video values in the table below by hand instead.
|
||||
|
||||
Then change these for your tier *(provisional)*:
|
||||
The whole tier table below is applied by the script:
|
||||
|
||||
| Setting | Where | Deck | Mid | High |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -49,30 +59,29 @@ extra in multiplayer. Distant Horizons draws everything beyond that.
|
||||
## 2. Java memory and garbage collector (Prism → Edit Instance → Settings → Java)
|
||||
|
||||
Use **Java 21** (Temurin 21 recommended). More memory is *not* faster: a bigger heap makes
|
||||
each GC pause longer.
|
||||
each GC pause longer. Bench (B4h): **5 GB is enough; 4 GB is measurably worse** (tour 1 % low
|
||||
−23 fps); 6 and 8 GB were no faster than 5. Don't go below 5 GB on any machine.
|
||||
|
||||
| Your machine | Max memory (`-Xmx`) | Min memory |
|
||||
| --- | --- | --- |
|
||||
| Steam Deck (16 GB shared with the GPU) | **5 GB**, never above 6 | leave low (512 MB) |
|
||||
| 16 GB PC | **6 GB** | leave low |
|
||||
| 16 GB PC | **5–6 GB** | leave low |
|
||||
| 32 GB PC | **6–8 GB** | may equal max |
|
||||
| PC with little free RAM (other apps/containers running) | min(6 GB, free RAM − 2 GB) | leave low |
|
||||
|
||||
**JVM arguments** *(provisional; Phase 2 block B4 picks the winner)*. Paste one line into
|
||||
*Java arguments* (Prism adds `-Xmx` itself from the memory box):
|
||||
**JVM arguments** — bench block B4 settled this: the client-tuned G1 line below and the
|
||||
phase-0 G1 set tied (S5 tour 1 % low 226.8 vs 228.1 fps), while Generational ZGC and
|
||||
Shenandoah were measurably worse on the tour scene (1 % low 216.6 and 173.4 vs 226.8 fps).
|
||||
So: **the G1 line below, for everyone.** Paste it into *Java arguments* (Prism adds
|
||||
`-Xmx` itself from the memory box):
|
||||
|
||||
- **Default (all machines):** client-tuned G1
|
||||
```
|
||||
-XX:+UseG1GC -XX:MaxGCPauseMillis=37 -XX:G1HeapRegionSize=16M -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=23 -XX:G1ReservePercent=20 -XX:G1MixedGCCountTarget=3 -XX:G1HeapWastePercent=20 -XX:InitiatingHeapOccupancyPercent=10 -XX:G1RSetUpdatingPauseTimePercent=0 -XX:MaxTenuringThreshold=1 -XX:GCTimeRatio=99 -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
|
||||
```
|
||||
- **32 GB machines, if you still get stutter:** Generational ZGC. Distant Horizons
|
||||
recommends it in its log warning about G1.
|
||||
```
|
||||
-XX:+UseZGC -XX:+ZGenerational -XX:+DisableExplicitGC -XX:+PerfDisableSharedMem
|
||||
```
|
||||
`-XX:+ZGenerational` is required: on Java 21, `-XX:+UseZGC` alone selects the older
|
||||
non-generational ZGC. Avoid ZGC on the Deck or on low-RAM PCs. It needs spare memory and
|
||||
CPU those machines don't have.
|
||||
- Don't switch to Generational ZGC — not even on 32 GB machines. Distant Horizons
|
||||
recommends it in a log warning about G1, but here it **tested worse** (tour 1 % low
|
||||
216.6 fps vs 226.8 for G1; Shenandoah was worse still at 173.4).
|
||||
|
||||
Don't add `-XX:+AlwaysPreTouch` on the Deck or on low-RAM PCs. Don't copy
|
||||
`-XX:ShenandoahGCMode=iu` from older guides: Java 21 refuses to start with it.
|
||||
@@ -82,8 +91,10 @@ Don't add `-XX:+AlwaysPreTouch` on the Deck or on low-RAM PCs. Don't copy
|
||||
*Pending the owner's shader policy. Interim advice:* **shaders off** is the pack default.
|
||||
They cost more in this pack than usual:
|
||||
|
||||
- With any shaderpack on, **Create's GPU renderer (Flywheel) switches off**, so every
|
||||
rotating shaft and machine is drawn the slow way, and drawn again for shadows.
|
||||
- On its own, Flywheel (Create's GPU renderer) **switches off when any shaderpack is on**.
|
||||
The pack now ships **Iris Flywheel Compat**, which keeps it working under shaders (bench:
|
||||
Create floor p99 13.3 → 4.0 ms with MakeUp). It's unofficial, so it may lag behind
|
||||
Create/Iris updates; if Create machines render wrong with shaders, report it.
|
||||
- Shader **shadows corrupt airships** (Sable sub-levels; Iris issue #3164).
|
||||
- Distant Horizons adds a shadow pass over all its terrain.
|
||||
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# Bench results — proxy machine, run 1 (B0–B12a)
|
||||
|
||||
**Proxy data only.** One machine (Ryzen 9 9900X / RX 7800 XT, Manjaro), not the fleet (P0 Deck,
|
||||
P1 7700K+1060, P2 6700K+1080 Ti). Run 1: 2026-10-16 22:25 → ~2026-10-17 05:00 EDT, 56 sessions
|
||||
(57 launches incl. one auto-retry), 306/309 capture windows valid. Source:
|
||||
`~/onion-bench/results/session.jsonl` (`ts >= 2026-10-16T22:24`), verdicts from
|
||||
`harness/analyze.py --baseline C1 --noise B0 --since 2026-10-16T22:24`.
|
||||
Run 2 (B4h, B7 shader rows, B12b, B14) and Run 3 (new mod set, Flerovium confirmation,
|
||||
B14 ×4) are below; B13 is not done.
|
||||
|
||||
## Method
|
||||
|
||||
- **Harness**: `harness/bench.py` + `matrix.py`. Every session is one cold launch at
|
||||
1920×1080 in headless gamescope, world restored from `bench-world-v1*.zip` (DH LODs
|
||||
pre-generated, `generatorPlan=DISABLED`), scenes driven by an in-world datapack, spectator
|
||||
camera, FTB Backups off, dedicated server stopped.
|
||||
- **Captures**: per scene 15 s warm-up + 30 s capture (S5 tour 120 s, no warm-up) × 3 reps,
|
||||
45 s settle after join. Frametimes from MangoHud; metrics = p50/p99 frametime, 1 % low
|
||||
(frametime-based), avg fps.
|
||||
- **Win rule (BENCH-MATRIX §4)**: win = median p99 improves by more than the B0 p99 spread
|
||||
AND 1 % low doesn't drop by more than the B0 1 %-low spread; loss = worse beyond either;
|
||||
else tie (ties keep the current/upstream value).
|
||||
- **Noise floor (B0, C0 × 3 sessions on S1)**: p99 spread **0.40 ms**, 1 %-low spread
|
||||
**6.6 fps** (p99 9.64 ms, 1 % low 88.3 fps).
|
||||
- **Baselines**: each variant is compared with the C1 session(s) of its own block
|
||||
(`C1@B3`, `C1@B11.baseline`, …), i.e. same JVM and same time window.
|
||||
|
||||
## Highlights
|
||||
|
||||
- **The shipped tuning (C1) beats upstream defaults (C0) on every scene.** C0 (B1) vs C1
|
||||
(B2), same JVM (Prism default, 8 GB):
|
||||
|
||||
| Scene | p99 C0 → C1 (ms) | 1 % low C0 → C1 (fps) | avg fps C0 → C1 |
|
||||
|---|---|---|---|
|
||||
| S1 vista | 9.33 → 8.23 | 93.0 → 104.9 | 218 → 272 |
|
||||
| S2 Create floor | 3.79 → 3.22 | 218.7 → 259.8 | 694 → 975 |
|
||||
| S3 mob pen | 7.43 → 4.46 | 110.1 → 186.0 | 255 → 395 |
|
||||
| S4 airships | 2.93 → 2.44 | 298.0 → 341.3 | 789 → 1073 |
|
||||
| S5 tour | 3.47 → 2.95 | 194.2 → 223.8 | 788 → 1017 |
|
||||
|
||||
Startup ("Game took"): C0 31.38 s → C1 25.36 s. Atlas: C0 4096×4096×4 → C1 4096×4096×2.
|
||||
- **Distant Horizons is the biggest single render cost.** Turning DH off (B3.dh-off) gives
|
||||
S1 p99 7.99 → 5.38 ms, 1 % low 105.5 → 144.0; S5 3.09 → 2.34 ms, 211.2 → 282.4.
|
||||
DH's LOW quality preset at C1's radius 128 (`dh-qlow`) is a **win** on S1 (−0.96 ms,
|
||||
+9.5 fps). SSAO costs 1.25 ms p99 on S1.
|
||||
- **Fresh Animations is expensive.** S3 p99 4.32 → 8.65 ms (FA) / 10.20 ms (FA +
|
||||
Extensions); 1 % low 184.6 → 104.9 / 89.0. It is opt-in (not enabled by default) — keep
|
||||
it that way and document the cost.
|
||||
- **The added performance mods earn their place.** Removing BadOptimizations, Particle Core
|
||||
or Sodium Extra each loses (Particle Core / Sodium Extra: S3 1 % low −24 / −25 fps).
|
||||
- **ModernFix `dynamic_resources` (B6)**: startup 7.6 s faster (26.32 vs 33.92 s median) and
|
||||
better frametimes with it on. Ship is gated only on the manual missing-texture check
|
||||
(see B6 row).
|
||||
|
||||
## Per-block verdicts
|
||||
|
||||
Numbers are p99 ms / 1 % low fps; Δ is vs the block's C1 baseline (+ = better p99).
|
||||
|
||||
| Block | Change tested | Result | Verdict | Ship action |
|
||||
|---|---|---|---|---|
|
||||
| B0 | noise floor, C0 × 3 on S1 | p99 9.64 (spread 0.40), 1 % low 88.3 (spread 6.6) | — | thresholds for all blocks |
|
||||
| B1 | C0 baseline, S1–S5 | see highlights table; startup 31.38 s | reference | — |
|
||||
| B2 | C1 vs C0 | C1 wins S1–S5 (table above) | **win** | C1 stays shipped; no bisect needed |
|
||||
| B2 | C1-deck vs C1 | S1 7.08/115.0 vs 8.23/104.9 (win); S2/S4/S5 tie; S3 4.84/167.5 vs 4.46/186.0 (loss) | mixed | keep Deck tier as is (S3 loss: check `entityShadows`/particles on fleet P0) |
|
||||
| B2 | C1-high vs C1 | S1 tie; S2–S5 losses (e.g. S4 2.66/311.6 vs 2.44/341.3) | expected cost | keep High as opt-in quality tier |
|
||||
| B3 | DH radius 64 | S1 7.46/111.1 vs 7.99/105.5 (win); S5 tie | win S1 | already the Deck value; no change to C1 |
|
||||
| B3 | DH radius 192 / 256 | S1 tie (7.69/105.5, 8.07/101.5); S5 tie | tie | **decision**: on this GPU 256 (upstream) ties 128; fleet floors decide — keep 128 until fleet data |
|
||||
| B3 | DH LOW quality preset @ r128 (`dh-qlow`) | S1 7.03/115.0 vs 7.99/105.5 (+0.96, +9.5) | **win** (S1 only) | candidate for DEFAULT after an S5 run + visual check |
|
||||
| B3 | DH SSAO on | S1 9.24/95.3 (−1.25, −10.2) | loss | confirms C1 SSAO off |
|
||||
| B3 | DH threads 4 | S1 tie; S5 3.32/197.4 vs 3.09/211.2 (loss) | loss | threads stay 2 |
|
||||
| B3 | DH off (reference) | S1 5.38/144.0; S5 2.34/282.4 | — | cost reference only |
|
||||
| B4 | GC J0/J1/J2/J3 @ 6 GB, S2+S5 | S5: J0 3.07/226.8, J1 3.11/228.1 (tie), J2 3.40/216.6 (loss), J3 3.75/173.4 (loss); S2 all tie | **J0** | keep J0; heap ladder = B4h (run 2) |
|
||||
| B5 | Sable ships 0/1/4 (proxy) | S4 3.00/280.6, 3.12/261.6, 2.97/273.2 | — | ships nearly free on this GPU |
|
||||
| B5 | SSRD 32/64/128, sable-noshade (4 ships) | 3.02/265.0, 3.03/264.7, 3.11/261.9, 3.04/263.0 vs pooled C1 (−8 to −11 fps 1 % low) | loss/noise | keep C1 (SSRD 16, shading on); decay run deferred (fleet P0/P1) |
|
||||
| B6 | ModernFix dynamic_resources off vs on (C1) | S1 9.11/83.8 vs 8.48/98.6; S2 3.29/236.1 vs 3.18/264.0; startup 33.92 vs 26.32 s | dynres on **wins** | ship `modernfix-mixins.properties` **after** the manual missing-texture check |
|
||||
| B8 | Fresh Animations (+Ext) on S3 | 8.65/104.9 (FA), 10.20/89.0 (FA+Ext) vs 4.32/184.6 | large cost | keep opt-in; document in PLAYER-TUNING |
|
||||
| B8 | EMF animation LOD 20 / 8 (with FA) | vs C1-fa (LOD 12) 8.65/104.9: LOD 20 8.91/101.8 (tie), LOD 8 8.16/108.0 (+0.49, +3.1 → win) | LOD 8 wins under FA | optional: LOD 8 for FA users (PLAYER-TUNING note) |
|
||||
| B8 | LambDynamicLights fancy / fastest | 4.41/181.8, 4.35/187.7 vs 4.32/184.6 | tie | keep C1 `fast` |
|
||||
| B9 | Flywheel light flat / smooth (S2) | 3.45/246.6 (tie), 3.46/238.9 (−8.6, loss) vs 3.44/247.5 | tie/loss | keep C1 TRI_LINEAR |
|
||||
| B10 | Sound Physics rays up / strict / no caps (S2s) | 3.69/216.2 (loss), 3.58/233.7 (tie), 3.64/216.9 (loss) vs 3.52/237.6 | loss/tie | keep C1 |
|
||||
| B11 | Sodium 3 builder threads, blend 0/2, mipmaps 4 | all losses except blend 2 (S5 tie); e.g. mip4 S1 8.82/97.6 vs 8.12/104.2 | loss/tie | keep C1 |
|
||||
| B12a | remove BadOptimizations / Particle Core / Sodium Extra | each loses ≥1 scene (e.g. S3 3.93/205.3, 4.63/188.4, 4.61/187.5 vs 3.87/212.5) | keep all | no change |
|
||||
| B4h, B7, B12b, B14 | see **Run 2** below | | | |
|
||||
| B13 | spark profile | — | **pending (manual)** | — |
|
||||
|
||||
## Run 2 (2026-10-17/18): B4h, B7, B12b, B14
|
||||
|
||||
Same machine and method; **default JVM** (run 1's B5–B12a ran on a leaked J3/6 GB JVM —
|
||||
harness bug fixed in `60e10b3`; within-block verdicts unaffected). Verdicts use the B0
|
||||
thresholds (p99 0.40 ms, 1 % low 6.6 fps); one session per row, so treat single-scene
|
||||
margins near the threshold as soft. Numbers: p99 ms / 1 % low fps.
|
||||
|
||||
### B4h — heap ladder (J0, S2 + S5)
|
||||
|
||||
| heap | S2 | S5 |
|
||||
|---|---|---|
|
||||
| 4 GB | 3.58 / 229.1 | 3.32 / 188.7 |
|
||||
| 5 GB | 3.58 / 238.6 | 3.13 / 209.1 |
|
||||
| 6 GB | 3.63 / 225.7 | 3.14 / 211.8 |
|
||||
| 8 GB | 3.58 / 234.5 | 3.18 / 209.2 |
|
||||
|
||||
**Smallest heap within the winner's spread: 5 GB** (S5 1 % low 209.1 vs best 211.8).
|
||||
4 GB loses on S5 (−23 fps). Heap is a launcher setting, not a shipped config →
|
||||
PLAYER-TUNING recommendation (≥ 5 GB), not a `build.py` change.
|
||||
|
||||
### B7 — shaders (data for owner; no auto-decision)
|
||||
|
||||
| profile | S1 vegetation | S2 Create floor | S4 airships |
|
||||
|---|---|---|---|
|
||||
| none (run-2 re-run) | 8.13 / 103.6 (274 fps avg) | 3.28 / 260.4 (974) | 2.50 / 332.5 (1072) |
|
||||
| MakeUp shadowless-medium | 8.69 / 97.3 (263) | 12.88 / 67.9 (120) | 2.58 / 314.0 (1050) |
|
||||
| MakeUp medium | 10.04 / 88.4 (213) | 12.32 / 71.9 (121) | 2.94 / 280.4 (779) |
|
||||
| Complementary LOW | 10.85 / 83.2 (182) | 13.23 / 64.0 (120) | 4.16 / 205.2 (444) |
|
||||
| Complementary MEDIUM | 10.92 / 78.5 (175) | 13.27 / 67.4 (118) | 4.33 / 192.8 (401) |
|
||||
|
||||
All shader rows loaded the pack (no `shader_not_loaded`). The dominant cost is **S2**:
|
||||
any shaderpack makes Flywheel drop its GPU backends, so the Create floor goes from
|
||||
974 to ~120 fps average (p99 3.3 → ~13 ms) regardless of profile. Elsewhere MakeUp
|
||||
shadowless is nearly free; Complementary costs ~2.7 ms p99 on S1 and ~2× frame time on S4.
|
||||
→ Supports **opt-in shaders**, MakeUp first, and **Iris Flywheel Compat** (B12b).
|
||||
|
||||
### B12b — trial mods (each vs its own baseline)
|
||||
|
||||
| trial | result | verdict |
|
||||
|---|---|---|
|
||||
| **Iris Flywheel Compat** 2.4.0 (MakeUp default, vs shader-baseline) | S2 **3.98 / 218.3 vs 13.26 / 65.8** (avg 644 vs 117 fps); S1 9.92 / 84.8 vs 10.23 / 83.8 (tie); S4 3.10 / 266.1 vs 3.01 / 277.6 (1 % low −11.5, loss) | **win under shaders** (restores Flywheel instancing) — add after a visual check of Create contraptions/airships with a shaderpack (unofficial mod; breaks on Create/Iris updates) |
|
||||
| **Flerovium** 1.1.2 (vs C1) | S3 **3.48 / 229.9 vs 5.71 / 147.4**; S1/S2/S5 tie | **win** on entities — but the B12b C1 S3 baseline (5.71) is worse than every run-1 C1 S3 (4.32–4.46), so the true gain is ~0.9 ms / +45 fps vs run 1. Add after one confirmation pair. **Superseded by Run 3** (did not hold up) |
|
||||
| Gnetum 3.3.6 | S3 4.41 / 189.3 (vs anomalous 5.71; ≈ run-1 C1) ; S1/S2/S5 tie | tie in practice → no add |
|
||||
| Better Block Entities 1.3.4 | S2 3.51 / 245.3 (loss), S3 loss; with FA 9.86 / 89.8 vs 9.52 / 87.4 (tie) | reject |
|
||||
| Super Resolution 0.9.2-alpha.2 | S1 8.74 / 99.7 (loss), S5 1 % low −16.6 (loss) | reject on this proxy (P0-only lever; its first launch rewrites `fml.toml` and demands a restart) |
|
||||
| Ixeris 4.6.8 | S1 p99 −0.49 (loss), S5 1 % low −12.2 (loss); **one crash** (NPE in `GameRules.getInt` during server tick, retry ok) | reject |
|
||||
| Cull Leaves 4.1.1 (vs Better Leaves baseline) | S1 tie; S5 1 % low 208.8 vs 217.1 (loss) | reject |
|
||||
|
||||
New mod adds (Iris Flywheel Compat, Flerovium) ⇒ **re-run B0** before any later matrix work.
|
||||
|
||||
### B14 — spawn tuning (defaults vs shipped, 10 min per site, **1 run each**)
|
||||
|
||||
*Superseded by Run 3 (4 runs per config).*
|
||||
|
||||
| site | defaults total / tuned | shipped total / tuned |
|
||||
|---|---|---|
|
||||
| jungle (−8 107 −201) | 123 / 5 | 118 / 3 |
|
||||
| river (904 63 88) | 208 / 29 | 208 / 25 |
|
||||
| warm ocean (−200 63 56) | 155 / 22 | 130 / 0 |
|
||||
|
||||
Automated rule says **back off**, but it is driven by one Fowl Play herd: at the warm-ocean
|
||||
site defaults spawned 14 geese + 8 ducks (on nearby land/water inside the 128-block
|
||||
sphere), shipped none; the other "vanished" species (gazelle, dragonfly) had max count 1.
|
||||
Sea bunny — the warm-ocean target — appeared under neither config, so that site doesn't
|
||||
exercise it. **Inconclusive at n = 1**: needs ≥ 3 interleaved runs per config (≈ 3 h)
|
||||
before any back-off; if it persists, the candidate is Fowl Play duck/goose weights.
|
||||
All runs: 33 count rows, `biome_ok=1`, config check ok.
|
||||
|
||||
## Run 3 (2026-10-18): new mod set, Flerovium confirmation, B14 ×4
|
||||
|
||||
Same machine and method, default JVM, **new mod set** (shipped in `7841361`: + Flerovium
|
||||
1.1.2, + Iris Flywheel Compat 2.4.0, ModernFix `dynamic_resources=true`). Numbers are
|
||||
p99 ms / 1 % low fps, medians of session medians.
|
||||
|
||||
### B0v2 — noise floor for the new mod set (C0, S1, 3 sessions)
|
||||
|
||||
| p99 | p99 spread | 1 % low | 1 % low spread | avg fps |
|
||||
|---|---|---|---|---|
|
||||
| 9.74 | **0.41 ms** | 87.2 | **1.6 fps** | 219 |
|
||||
|
||||
These are the thresholds for any later matrix work on this mod set.
|
||||
|
||||
### Flerovium confirmation (C1, 2 sessions each, interleaved on/off)
|
||||
|
||||
| scene | Flerovium on | Flerovium off | Δ (on vs off) | verdict |
|
||||
|---|---|---|---|---|
|
||||
| S1 vista | 8.42 / 99.8 | 8.02 / 105.0 | p99 −0.40, 1 % low −5.2 | **loss** (1 % low drop > 1.6) |
|
||||
| S3 mob pen | 5.55 / 151.9 | 5.94 / 141.1 | p99 +0.39 (just under 0.41) | **tie / inconclusive** — S3 session spread is huge (p99 1.42–2.34 ms, 1 % low 32–61 fps) |
|
||||
|
||||
**The run-2 win did not hold up** (it was measured against an anomalous S3 baseline).
|
||||
**Recommendation: remove Flerovium** — awaiting owner OK; it is currently shipped in `7841361`.
|
||||
|
||||
### B14 — spawn tuning, 4 runs per config
|
||||
|
||||
Median final count at 10 min within 128 blocks (total / tuned species):
|
||||
|
||||
| site | defaults | shipped |
|
||||
|---|---|---|
|
||||
| jungle | 129.5 / 4.0 | 128.0 / 2.5 |
|
||||
| river | 208.0 / 34.5 | 211.5 / 28.5 |
|
||||
| warm ocean | 145.0 / 9.5 | 136.0 / 0.5 |
|
||||
|
||||
The automated rule says **back off** (snail vanished at the jungle site, gazelle at the warm
|
||||
ocean), but both peaked at 1 per run — noise, not a disappearance. Sea bunny never appeared
|
||||
under either config, so this site doesn't exercise it. The warm-ocean tuned drop is mostly
|
||||
a Fowl Play goose/duck herd. **Verdict: the tuning is harmless but does not measurably
|
||||
reduce total entity load.** Owner decides keep vs revert; no config change made.
|
||||
|
||||
### Shipping incident
|
||||
|
||||
`7841361`'s `index.toml` captured hashes of runtime re-saved spawn configs, so the server's
|
||||
packwiz install failed (`Hash invalid`) at 14:06 and the server didn't start. Fixed by
|
||||
re-hashing in `c5d25fa` (server up 14:18). Nothing was pushed.
|
||||
|
||||
## Caveats
|
||||
|
||||
- Run 2: Super Resolution's first launch flipped `earlyWindowControl` in the bench `fml.toml` (not journaled by the harness); later run-2 sessions started with it off. Loading-screen only — frametimes unaffected; startup times after 01:54 are not comparable. Reset 2026-10-18.
|
||||
|
||||
- **Single proxy machine.** Fleet machines (P0–P2) were unreachable; the fleet fps floors
|
||||
(P0 1 % low ≥ 30, P1/P2 ≥ 60) don't map to a 7800 XT — every verdict here is relative.
|
||||
B2's "C1 wins on ≥ 2 machines" gate is satisfied on one machine only.
|
||||
- **This GPU hides render costs.** Airships (B5), Flywheel lighting and DH radius are cheap
|
||||
here; the fleet's older GPUs may rank them differently. B5 decay is deferred to P0/P1.
|
||||
- **GPU clocks unpinned** (amdgpu DPM `auto`, no root); every row carries `gpu_dpm_auto`.
|
||||
- **JVM differed between block groups (harness bug).** B0–B3 ran Prism's default args at
|
||||
8 GB; B4 set each GC at 6 GB; `bench.py` never restored `instance.cfg`, so **B5–B12a all
|
||||
ran on J3 Shenandoah, 6 GB**. Every verdict compares sessions inside one block (same JVM),
|
||||
so verdicts stand, but absolute numbers aren't comparable across those groups and the B0
|
||||
noise floor was measured under the other JVM.
|
||||
- **One crash**: B10 `spr-nocap`, first attempt — `OutOfMemoryError: Ran out of memory
|
||||
reading level data` after an unusually slow (~1.5 min) resource load, on the leaked
|
||||
Shenandoah/6 GB JVM. The automatic retry was clean.
|
||||
- Single sessions per variant (except B0/B6, 3 each); 3 reps per scene per session.
|
||||
@@ -0,0 +1,142 @@
|
||||
# Bench harness v2
|
||||
|
||||
`bench.py` drives the OnionBench Prism instance (`~/onion-bench/prism/instances/OnionBench`)
|
||||
with **no input injection**: a generated world datapack runs the scenes and prints
|
||||
markers, the harness only reads logs and the MangoHud CSV. `run_scene.py` is the legacy
|
||||
xdotool harness, kept for reference. Stdlib only, Python 3.
|
||||
|
||||
## Loop
|
||||
|
||||
```sh
|
||||
cd analysis/client-perf/harness
|
||||
python3 bench.py boot --n 3 --variant C1 # cold launches -> results/boot.jsonl
|
||||
python3 bench.py boot --n 3 --join # ... into the world (join time)
|
||||
python3 bench.py session --variant C0 --plan s1 --reps 3 --label B0 # x3 sessions for B0
|
||||
python3 bench.py session --variant C1 --plan s1,s2,s3,s4,s5 --label B2
|
||||
python3 bench.py session --variant C1 --plan s5 --jvm J2 --heap 6144 --label B4
|
||||
python3 analyze.py --baseline C0 --noise B0 [--label B2] # markdown table + verdicts
|
||||
python3 -m unittest discover -s tests # offline tests (from this dir)
|
||||
```
|
||||
|
||||
`session --dry-run` prints the datapack it would write and changes nothing. `bench.py meta`
|
||||
prints the environment snapshot; `bench.py disarm` removes a leftover plan pack.
|
||||
|
||||
## Per-session instance changes (undone after every session)
|
||||
|
||||
```sh
|
||||
python3 bench.py session --variant C1 --plan s1,s2,s4 --shader ComplementaryReimagined_r5.9.3.zip \
|
||||
[--shader-options profile.txt] --label B7
|
||||
python3 bench.py session --variant C1 --plan s1 --dh off
|
||||
python3 bench.py session --variant C1 --plan s1 --disable-mods 'badoptimizations*,particle_core*'
|
||||
python3 bench.py session --variant C1 --plan s2 --add-mods ~/trials/gnetum.jar
|
||||
python3 bench.py session --variant C1 --plan s4 --world-zip ~/onion-bench/worlds/bench-world-v1-s4-1.zip
|
||||
python3 bench.py restore # after a hard kill: replay .harness-pending.json
|
||||
```
|
||||
|
||||
- `--shader NAME|off` (default `off`): `config/iris.properties` `enableShaders` /
|
||||
`shaderPack`; NAME must exist in `shaderpacks/`. `--shader-options FILE` is copied to
|
||||
`shaderpacks/<NAME>.txt` (Iris' per-pack options file; journaled whenever a shader is set).
|
||||
- `--dh on|off` (default `on`): `[client.advanced.debugging] rendererMode` =
|
||||
`DEFAULT` / `DISABLED` in `config/DistantHorizons.toml`, set after the variant (DH's
|
||||
`quickEnableRendering` is GUI-only and maps to this key).
|
||||
- `--disable-mods GLOB,..`: case-insensitive globs over `mods/*.jar`, renamed to
|
||||
`.jar.disabled`; a glob that matches nothing is an error. `--add-mods PATH,..` copies jars
|
||||
in (name collision = error). `mods_hash` is taken after these changes.
|
||||
- `--world-zip PATH`: `saves/onionbench` -> `saves/onionbench.harness-bak`, zip extracted
|
||||
(world at the zip root or under one top directory, needs `level.dat`); sha256 recorded.
|
||||
- Every session first resets `options.txt` `resourcePacks` / `incompatibleResourcePacks`
|
||||
to `["vanilla","mod_resources"]` / `[]` (variants may override); not undone.
|
||||
- Crash safety: before the first change `session` writes `.minecraft/.harness-pending.json`
|
||||
(write-ahead: each entry precedes its change; file backups in `.minecraft/.harness-backup/`).
|
||||
The normal `finally` and `bench.py restore` run the same restore, newest entry first;
|
||||
failed entries stay in the journal. `session` and `boot` refuse to start while it exists.
|
||||
SIGTERM unwinds through the restore; SIGKILL leaves the journal for `bench.py restore`.
|
||||
- Rows record `shader`, `shader_options(_sha256)`, `dh`, `disabled_mods`, `added_mods`,
|
||||
`world_zip(_sha256)`, `resource_packs`, `restore_errors`; warn flags `gpu_dpm_auto` (RX
|
||||
7800 XT `power_dpm_force_performance_level` != `high`), `server_running`,
|
||||
`restore_failed`.
|
||||
|
||||
- **boot**: N launches; waits for `Game took N seconds` (or `joined the game` with
|
||||
`--join`); outcome `ok` / `crash` / `timeout`. Crash = new file in `crash-reports/` or
|
||||
`hs_err_pid*.log`, client java exited, or a log line with `Failed to create mod
|
||||
instance`, a `/FATAL]` level tag, `Cowardly refusing`, or a crash-report header.
|
||||
- **session**: applies the variant (`proposed-configs/apply_variant.py`), then pins
|
||||
`options.txt` (no onboarding/narrator, no pause on focus loss, `maxFps:260`, vsync
|
||||
off), hashes `config/` (minus `.bak`/`.tmp`/spark buffers) and `options.txt`, writes the
|
||||
plan datapack, launches once into `onionbench`, tails `latest.log` until `BENCH_DONE`,
|
||||
crash, exit or timeout, kills the client, removes the plan pack, then slices the CSV.
|
||||
One JSON row per scene x rep window goes to `~/onion-bench/results/session.jsonl`;
|
||||
every session's `latest.log` is archived under `~/onion-bench/results/logs/`.
|
||||
- Only java processes whose command line contains `instances/OnionBench` are ever
|
||||
signalled. The dedicated server (`unix_args.txt`) is only observed (running + %CPU).
|
||||
- `--jvm J0..J3` / `--heap MB` rewrite `JvmArgs` / `MaxMemAlloc` in the bench
|
||||
`instance.cfg` (first original saved as `instance.cfg.harness-orig`). Without them the
|
||||
instance is left as is; the effective `-X*` flags are read from the running client.
|
||||
|
||||
## Datapack mechanism
|
||||
|
||||
The scene functions live in the permanent pack `saves/onionbench/datapacks/onion-bench`
|
||||
(`bench:s1` ... `bench:tour_start`, source in `../bench/onion-bench`). Per session the
|
||||
harness writes `datapacks/onion-bench-autoplan/` (pack_format 48, 1.21.1 `function/`
|
||||
dirs) with a session id `pYYYYMMDDHHMMSS`:
|
||||
|
||||
- `#minecraft:load` -> `bench:autoplan/<sid>/load`: unless score `#<sid> bench_plan` is
|
||||
set, poll every second (`schedule`) until `execute if entity @a`;
|
||||
- `begin`: set the score (a `/reload` won't restart the plan), clear stale
|
||||
`storm_tick` / tour schedules, `say BENCH_PLAN <sid> begin`, wait `--settle` s;
|
||||
- chained `stepN` functions: `execute as @a[limit=1] at @s run function bench:<scene>`,
|
||||
`say BENCH_MARK warm|start|end <scene> <rep>`, `schedule` the warm-up / capture ticks;
|
||||
`say BENCH_DONE <sid>` at the end.
|
||||
|
||||
S5 is fixed by BENCH-MATRIX: `bench:s1`, 10 s, `bench:tour_start`, all 120 s captured
|
||||
(warm-up ignored). S2s = S2 + `bench:storm_on` during warm-up and capture. Plans must
|
||||
follow S1 -> S5 order (`--any-order` overrides). Function names contain the session id, so
|
||||
schedules saved into `level.dat` by an autosave can never fire in a later session. The pack
|
||||
is deleted in a `finally`, so normal play of the world is unaffected; after a hard kill
|
||||
of the harness itself run `bench.py disarm`.
|
||||
|
||||
Markers are taken from the server log line (`[Server] BENCH_MARK ...`, ms timestamps).
|
||||
The MangoHud CSV used is one created at/after launch (never a stale one); wall time =
|
||||
file birth time (`stat -c %.9W`, else the name's timestamp) + `elapsed`, and the method is
|
||||
recorded as `align`. Per window: n, p50/p95/p99/peak ms, avg fps (1000/mean frametime),
|
||||
1 % low = 1000/mean of the worst 1 % frametimes, and the legacy fps-column 1 % low.
|
||||
|
||||
Flags: `vsync_suspect` (p50 within 3 % of 60 Hz or an xrandr refresh), `low_n`
|
||||
(n < 500 or < half of duration x avg fps), `crashed`/`timeout` (window never ended),
|
||||
`session_crash` (window ended, session crashed later), `backup_in_window` (backup or
|
||||
autosave line from 5 s before start to end; warn only). `analyze.py` drops rows with
|
||||
any non-warn flag unless `--include-invalid`.
|
||||
|
||||
Metadata per row: label, machine (`proxy-9900X-7800XT`), git sha + dirty, harness sha,
|
||||
variant + config/options hashes, mods hash/count, Java version, cfg JVM args + effective
|
||||
flags, MaxMemAlloc, kernel, CPU governor, per-GPU DPM level + sclk, MangoHud's GPU
|
||||
string, MemAvailable before/after, load average, server running + %CPU, startup and
|
||||
join seconds, block-atlas size, `level.dat` hash.
|
||||
|
||||
## Known limits
|
||||
|
||||
- The desktop is **KDE Plasma on Wayland** (kwin_wayland + Xwayland at `:1`). xdotool
|
||||
only reaches Xwayland windows, `windowactivate --sync` hangs under kwin, focus-steal
|
||||
prevention can drop keystrokes, and typed chat lands in whatever has focus. Hence no
|
||||
input injection at all in v2.
|
||||
- B7 shader profiles are automated: `gen_shader_profiles.py` resolves each pack's
|
||||
`profile.NAME` (shaders.properties, Iris ProfileSet semantics) into
|
||||
`shader-profiles/<pack-stem>.<profile>.txt`; matrix.toml passes it as `shader_options`
|
||||
-> `--shader-options`. Re-run the generator (or `--check`) after a shaderpack update.
|
||||
- Still manual: **S6** (JEI/GUI overlay needs a key press), checks needing F3
|
||||
reads, `/sparkc` heap/GC reads (B1/B4).
|
||||
- MangoHud's `log_duration=1800` in `~/.local/bin/mangohud-local` caps one CSV at 30 min;
|
||||
`session` warns when a plan is longer (B5's 45-min decay run needs a split or a larger
|
||||
value).
|
||||
- The integrated server ticks the plan, so scene timing stretches if it lags; windows use
|
||||
the marker wall times, not tick counts. The marker chat line is visible for ~10 s at
|
||||
the start of each capture (identical across variants).
|
||||
- **FTB Backups 3** backs up the world right after join (its index
|
||||
`ftbbackups3/backups.json` doesn't parse, so it treats the world as never backed up)
|
||||
and every `backup_timer` = 120 min. **Done (bench instance only):** `auto: false` in
|
||||
the instance's `config/ftbbackups3-server.snbt`. Never edit the repo's `config/` for this.
|
||||
- Stop the dedicated server (tmux `onion-mc`) while benching (owner-approved); it used up
|
||||
to ~78 % of a core idle. Rows are flagged `server_running` if it is up.
|
||||
- amdgpu DPM is `auto` (no root); rows are flagged `gpu_dpm_auto`. For tighter noise:
|
||||
`echo high | sudo tee /sys/class/drm/card1/device/power_dpm_force_performance_level`.
|
||||
- Scenes run in **spectator** mode (set by the plan) so elevated cameras don't fall.
|
||||
@@ -0,0 +1,201 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarise bench.py session rows and apply the BENCH-MATRIX win rule.
|
||||
|
||||
analyze.py [--file ~/onion-bench/results/session.jsonl] [--label B2]
|
||||
[--baseline C0 --noise B0] [--include-invalid] [--exclude-backup]
|
||||
|
||||
Groups windows by (label, variant, scene). Each session contributes its median p99 and
|
||||
median 1 % low (frametime-based); a group's value is the median of its session medians
|
||||
and its spread is max - min of those. With --baseline/--noise, every non-baseline group
|
||||
is compared with the baseline variant on the same scene: same label if present, else the
|
||||
same block (a tagged label 'B3.dh-off' looks in 'B3', 'B3.baseline', then all 'B3.*'
|
||||
pooled), else all labels pooled. A tagged session of the baseline variant itself is
|
||||
compared with 'B3' / 'B3.baseline' when one exists: win = median p99 improves by more than the noise label's p99 spread
|
||||
AND 1 % low does not worsen by more than its 1 % low spread; loss = p99 worsens beyond
|
||||
the spread or 1 % low drops beyond it; otherwise tie (ties keep upstream defaults).
|
||||
Rows flagged crashed / timeout / no_data / no_markers / low_n / vsync_suspect are
|
||||
excluded unless --include-invalid. Stdlib only.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import statistics
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
INVALID = {"crashed", "timeout", "no_data", "no_markers", "low_n", "vsync_suspect",
|
||||
"shader_not_loaded"}
|
||||
DEFAULT_FILE = Path.home() / "onion-bench" / "results" / "session.jsonl"
|
||||
|
||||
|
||||
def load_rows(path):
|
||||
rows = []
|
||||
for line in Path(path).read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
rows.append(json.loads(line))
|
||||
return [r for r in rows if r.get("kind", "window") == "window"]
|
||||
|
||||
|
||||
def is_valid(row, exclude_backup=False):
|
||||
flags = set(row.get("flags") or [])
|
||||
if not row.get("metrics") or flags & INVALID:
|
||||
return False
|
||||
return not (exclude_backup and "backup_in_window" in flags)
|
||||
|
||||
|
||||
def summarise(rows):
|
||||
"""{(label, variant, scene): stats} from valid window rows."""
|
||||
by_session = {}
|
||||
for r in rows:
|
||||
key = (r.get("label"), r["variant"], r["scene"])
|
||||
by_session.setdefault(key, {}).setdefault(r["session"], []).append(r["metrics"])
|
||||
out = {}
|
||||
for key, sessions in by_session.items():
|
||||
p99s = [statistics.median(m["p99_ms"] for m in ms) for ms in sessions.values()]
|
||||
lows = [statistics.median(m["one_low_fps"] for m in ms) for ms in sessions.values()]
|
||||
fps = [statistics.median(m["avg_fps"] for m in ms) for ms in sessions.values()]
|
||||
out[key] = {
|
||||
"sessions": len(sessions),
|
||||
"windows": sum(len(ms) for ms in sessions.values()),
|
||||
"p99": statistics.median(p99s), "p99_spread": max(p99s) - min(p99s),
|
||||
"low": statistics.median(lows), "low_spread": max(lows) - min(lows),
|
||||
"fps": statistics.median(fps),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def noise_spread(summary, noise_label):
|
||||
"""(p99 spread, 1 % low spread) of the noise block, worst case across its groups."""
|
||||
groups = [v for (lab, _, _), v in summary.items() if lab == noise_label]
|
||||
if not groups:
|
||||
return None
|
||||
return max(g["p99_spread"] for g in groups), max(g["low_spread"] for g in groups)
|
||||
|
||||
|
||||
def verdict(base, cand, spread_p99, spread_low):
|
||||
gain = base["p99"] - cand["p99"] # > 0: candidate's p99 is lower (better)
|
||||
low_drop = base["low"] - cand["low"] # > 0: candidate's 1 % low is worse
|
||||
if gain > spread_p99 and low_drop <= spread_low:
|
||||
return "win"
|
||||
if gain < -spread_p99 or low_drop > spread_low:
|
||||
return "loss"
|
||||
return "tie"
|
||||
|
||||
|
||||
def block_of(label):
|
||||
"""'B3.dh-off' -> 'B3'; untagged labels (and None) are their own block."""
|
||||
return label.split(".", 1)[0] if label else label
|
||||
|
||||
|
||||
def _pooled(rows, labels, baseline, scene, name):
|
||||
pooled = [r for r in rows if r["variant"] == baseline and r["scene"] == scene
|
||||
and (labels is None or r.get("label") in labels)]
|
||||
if not pooled:
|
||||
return None
|
||||
return summarise([dict(r, label=name) for r in pooled]).get((name, baseline, scene))
|
||||
|
||||
|
||||
def baseline_for(summary, rows, label, scene, baseline):
|
||||
"""(baseline stats, where-label) for one candidate group, or (None, None).
|
||||
|
||||
Order: the candidate's own label; then its block - the untagged block label
|
||||
('B3', for tagged candidates), '<block>.baseline', then every '<block>.*' label pooled; then all labels
|
||||
pooled. A tagged session of the baseline variant itself (e.g. 'B3.dh-off' running
|
||||
C1 against baseline C1) is compared only with 'B3' or 'B3.baseline'."""
|
||||
block = block_of(label)
|
||||
tagged = bool(label) and block != label
|
||||
order = [label] + ([block] if tagged else []) + ([f"{block}.baseline"] if block else [])
|
||||
for lab in order:
|
||||
if (lab, baseline, scene) in summary:
|
||||
return summary[(lab, baseline, scene)], lab
|
||||
if block:
|
||||
labels = {r.get("label") for r in rows
|
||||
if r.get("label") == block or str(r.get("label")).startswith(block + ".")}
|
||||
b = _pooled(rows, labels, baseline, scene, f"{block}.*")
|
||||
if b is not None:
|
||||
return b, f"{block}.*"
|
||||
b = _pooled(rows, None, baseline, scene, "*")
|
||||
return (b, "*") if b is not None else (None, None)
|
||||
|
||||
|
||||
def self_baseline(summary, label, scene, baseline):
|
||||
"""Baseline for a tagged group that runs the baseline variant itself, or None
|
||||
(then the group *is* the baseline)."""
|
||||
block = block_of(label)
|
||||
if not label or block == label:
|
||||
return None, None
|
||||
for lab in (block, f"{block}.baseline"):
|
||||
if lab != label and (lab, baseline, scene) in summary:
|
||||
return summary[(lab, baseline, scene)], lab
|
||||
return None, None
|
||||
|
||||
|
||||
def table(summary, rows, baseline=None, noise=None):
|
||||
spreads = noise_spread(summary, noise) if noise else None
|
||||
full = summarise(rows) # baselines may live in labels filtered out of the table
|
||||
hdr = ["label", "variant", "scene", "sess", "win#", "p99 ms", "p99 spr", "1%low",
|
||||
"1%low spr", "avg fps"]
|
||||
if baseline:
|
||||
hdr += ["vs", "Δp99 ms", "Δ1%low", "verdict"]
|
||||
out = ["| " + " | ".join(hdr) + " |", "|" + "---|" * len(hdr)]
|
||||
for key in sorted(summary, key=lambda k: (str(k[0]), k[2], k[1])):
|
||||
lab, var, scene = key
|
||||
s = summary[key]
|
||||
cells = [str(lab), var, scene, str(s["sessions"]), str(s["windows"]),
|
||||
f"{s['p99']:.2f}", f"{s['p99_spread']:.2f}", f"{s['low']:.1f}",
|
||||
f"{s['low_spread']:.1f}", f"{s['fps']:.0f}"]
|
||||
if baseline:
|
||||
b, blab = (self_baseline(full, lab, scene, baseline) if var == baseline
|
||||
else baseline_for(full, rows, lab, scene, baseline))
|
||||
if var == baseline and b is None:
|
||||
cells += ["—", "", "", "baseline"]
|
||||
else:
|
||||
if b is None:
|
||||
cells += ["—", "", "", "no baseline"]
|
||||
elif spreads is None:
|
||||
cells += [f"{baseline}@{blab}", f"{b['p99'] - s['p99']:+.2f}",
|
||||
f"{s['low'] - b['low']:+.1f}", "no noise data"]
|
||||
else:
|
||||
cells += [f"{baseline}@{blab}", f"{b['p99'] - s['p99']:+.2f}",
|
||||
f"{s['low'] - b['low']:+.1f}", verdict(b, s, *spreads)]
|
||||
out.append("| " + " | ".join(cells) + " |")
|
||||
if baseline and noise:
|
||||
out.append("")
|
||||
out.append(f"noise ({noise}): " + (
|
||||
f"p99 spread {spreads[0]:.2f} ms, 1%low spread {spreads[1]:.1f} fps"
|
||||
if spreads else "no rows - verdicts unavailable"))
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
ap.add_argument("--file", default=str(DEFAULT_FILE))
|
||||
ap.add_argument("--label", action="append", help="only these labels (repeatable); "
|
||||
"the noise label is always kept")
|
||||
ap.add_argument("--baseline", help="baseline variant, e.g. C0")
|
||||
ap.add_argument("--noise", help="label whose spread is the win threshold, e.g. B0")
|
||||
ap.add_argument("--include-invalid", action="store_true")
|
||||
ap.add_argument("--exclude-backup", action="store_true")
|
||||
ap.add_argument("--since", help="only rows with ts >= this ISO time, e.g. 2026-10-16T22:24")
|
||||
a = ap.parse_args(argv)
|
||||
rows = load_rows(a.file)
|
||||
if a.since:
|
||||
rows = [r for r in rows if r.get("ts", "") >= a.since]
|
||||
total = len(rows)
|
||||
if not a.include_invalid:
|
||||
rows = [r for r in rows if is_valid(r, a.exclude_backup)]
|
||||
warn = sum(1 for r in rows if "backup_in_window" in (r.get("flags") or []))
|
||||
shown = rows
|
||||
if a.label:
|
||||
keep = set(a.label) | ({a.noise} if a.noise else set())
|
||||
shown = [r for r in rows if r.get("label") in keep]
|
||||
if not shown:
|
||||
print("no valid rows", file=sys.stderr)
|
||||
return 1
|
||||
print(table(summarise(shown), rows, a.baseline, a.noise))
|
||||
print(f"\n{len(rows)}/{total} windows valid; {warn} with backup_in_window (warn only)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,169 @@
|
||||
# B14: spawn-tuning measurement
|
||||
|
||||
`b14.py` automates SPAWN-TUNING.md §B14. It compares the upstream spawn defaults with the
|
||||
shipped partial files (`d80f85b`, `8e8483d`: `alexsmobs-common.toml`, `fowlplay.json5`,
|
||||
`crittersandcompanions-common.json5`). Each comparison stands still for 10 minutes at a
|
||||
river, a jungle and a warm ocean, and counts entities within 128 blocks.
|
||||
|
||||
It reuses `../bench.py` as a module: `launch`, `kill_client`, `LogTail`, `monitor`, the
|
||||
`Journal` / `restore_pending` crash-safe restore, `apply_pins`, `apply_dh`, and
|
||||
`zip_world_root`. It needs nothing beyond the Python 3 standard library.
|
||||
|
||||
## Loop
|
||||
|
||||
```sh
|
||||
cd analysis/client-perf/harness
|
||||
python3 b14/b14.py run --config defaults --dry-run # world, config plan, datapack
|
||||
python3 b14/b14.py run --config defaults # ~32 min, all three biomes
|
||||
python3 b14/b14.py run --config shipped
|
||||
python3 b14/b14.py summary # SPAWN-TUNING §B14 verdict
|
||||
```
|
||||
|
||||
- `--biome river,jungle,warm_ocean` (default `all`) runs the sites in that order in one
|
||||
launch.
|
||||
- `--minutes 10` (default) sets the stay at each site.
|
||||
- Repeating runs (for example two of each config, interleaved
|
||||
defaults → shipped → defaults → shipped) gives medians. The summary takes the median
|
||||
across runs.
|
||||
- Results go to `~/onion-bench/results/b14.jsonl`. Each count line is a `b14_count` row,
|
||||
and each launch is a `b14_session` row (outcome, config check, git/mods hash, sites,
|
||||
archived log at `results/logs/session-<sid>.log`).
|
||||
- `b14.py restore` (or `bench.py restore`, which uses the same journal) undoes a
|
||||
hard-killed run.
|
||||
- `b14.py parse LOG` prints the count rows found in any archived log.
|
||||
|
||||
The script refuses to start in these cases:
|
||||
- a restore journal is pending;
|
||||
- a bench client is running;
|
||||
- any `matrix.py run`, `bench.py session` or other `b14.py run` process exists. It shares
|
||||
the OnionBench instance with the matrix, so **never run it while the matrix runs.**
|
||||
|
||||
## Design
|
||||
|
||||
### Throwaway world `saves/onionspawn`, one per run
|
||||
- It contains only the bench world's `level.dat` (seed `onionbench`, gamerules, datapack
|
||||
list), the `onion-bench` datapack (it has no load or tick tags) and `serverconfig/`.
|
||||
- It has no `region/`, `entities/` or `data/` (no forceloads, no DH database). Every chunk
|
||||
around a site is therefore generated during the run, under the config being measured.
|
||||
That covers both kinds of spawning: the initial creatures placed at world generation
|
||||
and natural spawning.
|
||||
- The seed is the same, so the terrain is identical between runs. The world is journaled
|
||||
as a `tree` entry and deleted on restore. The bench world `saves/onionbench` is never
|
||||
touched.
|
||||
|
||||
### Sites are pre-located offline (`b14.py locate`)
|
||||
`/locate biome` can't be used from a datapack. Functions run with suppressed output, and
|
||||
`execute store` only captures the distance, not the coordinates.
|
||||
|
||||
So `locate` parses the overworld region files in `bench-world-v1.zip` with a small NBT
|
||||
reader (`nbt.py`):
|
||||
- It reads each chunk's biome palette at the surface, using the `MOTION_BLOCKING_NO_LEAVES`
|
||||
heightmap in full chunks and `WORLD_SURFACE_WG` in proto chunks.
|
||||
- For each target biome it picks the chunk whose 5×5 neighbourhood has the largest share
|
||||
of that biome.
|
||||
- Sites must be at least 192 blocks from the world spawn and the saved player position
|
||||
(spawn chunks and the first-load area stay outside the 128-block count sphere), and at
|
||||
least 320 blocks from each other (the count spheres don't overlap).
|
||||
|
||||
Biomes depend only on the seed, so the coordinates are valid for any world with this seed
|
||||
and mod set. They are committed in `sites.json` (from zip sha256
|
||||
`144caf5f…`, seed −306676217):
|
||||
|
||||
| biome | x y z | biome at site | share in 5×5 chunks |
|
||||
|---|---|---|---|
|
||||
| river | 904 63 88 | minecraft:river | 0.37 |
|
||||
| jungle | −8 107 −201 | minecraft:jungle | 0.52 |
|
||||
| warm_ocean | −200 63 56 | minecraft:warm_ocean | 0.75 |
|
||||
|
||||
Every count line carries `biome_ok`, an in-game `execute if biome` check at the player's
|
||||
feet (`minecraft:river`, `#minecraft:is_jungle`, `minecraft:warm_ocean`). If the
|
||||
regenerated terrain doesn't match the offline lookup, the summary marks that biome
|
||||
invalid instead of silently measuring the wrong place.
|
||||
|
||||
### Config variants (journaled, bench instance only)
|
||||
- `defaults` deletes the three files. Each mod regenerates its upstream defaults on load
|
||||
(the same as "these three config files removed" in SPAWN-TUNING). They aren't in
|
||||
`config/defaultoptions/extra/`, so Default Options can't put them back.
|
||||
- `shipped` writes `git show <rev>:config/<file>` (default `HEAD`), meaning the committed
|
||||
partials, not the working tree's boot re-saves.
|
||||
- After the run, and before the restore, the files the mods re-saved are read back and
|
||||
checked against the expected values. A mismatch fails the run (`config_check.ok=false`),
|
||||
and the summary ignores that run. The expected upstream defaults were verified from the
|
||||
jars:
|
||||
- Alex's Mobs: the `# Default:` comments in its re-saved TOML: gazelle 40, capuchin 28,
|
||||
elephant 30, straddler 70, rocky roller 60, underminer 50, blobfish 30,
|
||||
mudskipper 28, caiman 29.
|
||||
- Fowl Play: `FPConfig.<init>` bytecode: duck 4/max 6, goose 4/4, gull 5/8,
|
||||
sparrow 20/6.
|
||||
- Critters and Companions: `CACCommonConfig$SpawningConfig.<init>`: sea bunny warm
|
||||
ocean 32 (max 4), other oceans 16; leaf insect jungle 14, forest 6; dragonfly lush 10.
|
||||
|
||||
### Datapack (`saves/onionspawn/datapacks/b14-autoplan`, pack_format 48)
|
||||
- **Start-up:** a load tag polls once a second until a player exists. Then it pins the
|
||||
rules that matter for spawning:
|
||||
- `gamemode creative` (it counts for spawning and despawning, where spectators don't,
|
||||
and the player can't die);
|
||||
- `doMobSpawning true`;
|
||||
- the daylight, weather, patrol, trader, insomnia and warden rules;
|
||||
- time 6000 and clear weather.
|
||||
- **Per site:**
|
||||
1. Teleport above the site.
|
||||
2. 5 s later: clear a 3×3×3 pocket, put a barrier at y−1 (a platform over water), and
|
||||
teleport onto it.
|
||||
3. Count at t=0 (+10 s), then every 60 s up to t=10. Each count re-asserts the barrier
|
||||
and the teleport, so the player never drifts.
|
||||
- **Each count** runs `execute store result score #<alias> b14 if entity
|
||||
@e[type=<id>,distance=..128]` as the player, for the 22 tuned species and 10 vanilla
|
||||
spot-check species. It also stores:
|
||||
- `total` = `@e[type=!player,distance=..128]`, as SPAWN-TUNING defines it;
|
||||
- `nonitem`, which also excludes items and XP orbs;
|
||||
- `tuned`, the sum of the tuned species.
|
||||
- **Output:** a `tellraw` prints one line, which the client logs as `[System] [CHAT]`:
|
||||
`B14 <sid> <biome> t=<min> biome_ok=1 total=N nonitem=N tuned=N alexsmobs:gazelle=N ...`.
|
||||
- **Errors:** `Failed to load function b14:` in the log (an unknown entity id makes the
|
||||
whole function unloadable) aborts the run as `datapack_error`.
|
||||
|
||||
### Entity ids
|
||||
All are checked against the jars in the bench instance. Each has an `entity.<mod>.<name>`
|
||||
lang key, and is either in the mod's registry class strings (`CACEntities`) or has a
|
||||
`loot_table/entities/<name>.json`.
|
||||
- **Alex's Mobs** (`alexsmobs-1.22.17`): `gazelle`, `capuchin_monkey`, `elephant`,
|
||||
`straddler`, `rocky_roller`, `underminer`, `blobfish`, `mudskipper`, `caiman`.
|
||||
- **Fowl Play** (`fowlplay-1.2.3`): `duck`, `goose`, `gull`, `sparrow`.
|
||||
- **Critters and Companions** (`crittersandcompanions 2.7.0`): `leaf_insect`, `sea_bunny`,
|
||||
`dragonfly`, `ladybug`, `stag_beetle`, `roly_poly`, `snail`, `stick_bug`, `weevil`.
|
||||
|
||||
### Verdict (`summary`, SPAWN-TUNING §B14)
|
||||
- **Per biome**, using the final count (t = minutes):
|
||||
- **keep** if the median `total` drops under shipped *and* no tuned species that
|
||||
appeared at any minute under defaults is absent under shipped;
|
||||
- **back off** (listing the species) if one disappears;
|
||||
- **no drop** otherwise.
|
||||
- **Warnings:** `rare_warn` flags species whose shipped maximum is under 25 % of the
|
||||
defaults maximum (when defaults saw at least 4). `vanilla_gone` flags cows or sheep
|
||||
that vanished.
|
||||
- **Overall:** `keep` only if all three biomes keep.
|
||||
- **Excluded runs:** runs with a failed config check or a non-ok outcome are ignored. A
|
||||
biome with `biome_ok != 1` is `invalid`.
|
||||
|
||||
## Manual prep and limits
|
||||
- **Prep:** nothing beyond what's committed. `sites.json` is already generated. Re-run
|
||||
`b14.py locate --out b14/sites.json` only if the seed, the source zip or the
|
||||
biome-affecting mods (Terralith, Tectonic) change.
|
||||
- **Thin biome coverage:** the source world has only about 2300 chunks with final biomes,
|
||||
so the river and jungle patches are small (37 % and 52 % of the 5×5 neighbourhood). The
|
||||
128-block count sphere also covers neighbouring biomes. This is the same for both
|
||||
configs, but it lowers the absolute counts.
|
||||
- **Better sites (optional):** on `~/onion-bench/server-bench` (seed `onionbench`), run
|
||||
`/locate biome minecraft:river` (and jungle, warm_ocean), check the share around each
|
||||
result, and put the coordinates into `sites.json` by hand. The `y` value is the first
|
||||
free block above the surface or the water.
|
||||
- **Jungle site from a proto chunk:** its `y` is the height before trees are placed. The
|
||||
3×3×3 air pocket keeps a tree from burying the player, and `biome_ok` still guards the
|
||||
biome.
|
||||
- **Hostile mobs in `total`:** difficulty stays as in `level.dat` (normal), so hostiles
|
||||
from caves within 128 blocks count toward `total`. `tuned` and `nonitem` are recorded
|
||||
separately for reading the result.
|
||||
- **Machine:** this is proxy-machine data (singleplayer, simulation distance 8 pinned).
|
||||
Spawning is server logic, so the GPU doesn't matter, but CPU load does affect chunk
|
||||
generation speed.
|
||||
@@ -0,0 +1,855 @@
|
||||
#!/usr/bin/env python3
|
||||
"""B14 spawn-tuning measurement (SPAWN-TUNING.md §B14), automated.
|
||||
|
||||
b14.py locate [--zip bench-world-v1.zip] [--out sites.json] # offline, once
|
||||
b14.py run --config {defaults,shipped} [--biome river,jungle,warm_ocean]
|
||||
[--minutes 10] [--dry-run]
|
||||
b14.py summary [--file ~/onion-bench/results/b14.jsonl]
|
||||
b14.py parse LOG # debug: count lines
|
||||
b14.py restore # = bench.py restore
|
||||
|
||||
One `run` = one client launch into a throwaway world `saves/onionspawn` that holds only
|
||||
the bench world's level.dat (seed `onionbench`, gamerules, datapack list), so every
|
||||
chunk around the sites is generated fresh under the config being measured. A generated
|
||||
datapack sets doMobSpawning true + Creative, parks the player on a barrier block at each
|
||||
pre-located site (sites.json), and every 60 s prints a parsable chat line
|
||||
`B14 <sid> <biome> t=<min> biome_ok=1 total=N <entity id>=N ...` counted within 128
|
||||
blocks. The spawn config variant is journaled through bench.Journal, so `bench.py
|
||||
restore` undoes a hard-killed run. Stdlib only.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE))
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
import bench # noqa: E402 (harness module: launch, Journal, LogTail, monitor, ...)
|
||||
import nbt # noqa: E402
|
||||
|
||||
WORLD = "onionspawn"
|
||||
PACK = "b14-autoplan"
|
||||
SITES_FILE = HERE / "sites.json"
|
||||
SOURCE_ZIP = bench.BENCH / "worlds" / "bench-world-v1.zip"
|
||||
RESULTS_FILE = bench.RESULTS / "b14.jsonl"
|
||||
|
||||
# The three shipped partial files (d80f85b, 8e8483d). `defaults` deletes them so each mod
|
||||
# regenerates its upstream defaults; `shipped` writes the committed partials.
|
||||
SPAWN_FILES = ["alexsmobs-common.toml", "fowlplay.json5", "crittersandcompanions-common.json5"]
|
||||
|
||||
# Tuned species (verified against the jars' lang keys + registries, see README).
|
||||
TUNED = [
|
||||
("am_gazelle", "alexsmobs:gazelle"),
|
||||
("am_capuchin", "alexsmobs:capuchin_monkey"),
|
||||
("am_elephant", "alexsmobs:elephant"),
|
||||
("am_straddler", "alexsmobs:straddler"),
|
||||
("am_rocky_roller", "alexsmobs:rocky_roller"),
|
||||
("am_underminer", "alexsmobs:underminer"),
|
||||
("am_blobfish", "alexsmobs:blobfish"),
|
||||
("am_mudskipper", "alexsmobs:mudskipper"),
|
||||
("am_caiman", "alexsmobs:caiman"),
|
||||
("fp_duck", "fowlplay:duck"),
|
||||
("fp_goose", "fowlplay:goose"),
|
||||
("fp_gull", "fowlplay:gull"),
|
||||
("fp_sparrow", "fowlplay:sparrow"),
|
||||
("cac_leaf_insect", "crittersandcompanions:leaf_insect"),
|
||||
("cac_sea_bunny", "crittersandcompanions:sea_bunny"),
|
||||
("cac_dragonfly", "crittersandcompanions:dragonfly"),
|
||||
("cac_ladybug", "crittersandcompanions:ladybug"),
|
||||
("cac_stag_beetle", "crittersandcompanions:stag_beetle"),
|
||||
("cac_roly_poly", "crittersandcompanions:roly_poly"),
|
||||
("cac_snail", "crittersandcompanions:snail"),
|
||||
("cac_stick_bug", "crittersandcompanions:stick_bug"),
|
||||
("cac_weevil", "crittersandcompanions:weevil"),
|
||||
]
|
||||
# Vanilla spot-check (SPAWN-TUNING: "cows, sheep still spawn normally") + biome natives.
|
||||
VANILLA = [
|
||||
("mc_cow", "minecraft:cow"), ("mc_sheep", "minecraft:sheep"), ("mc_pig", "minecraft:pig"),
|
||||
("mc_chicken", "minecraft:chicken"), ("mc_parrot", "minecraft:parrot"),
|
||||
("mc_ocelot", "minecraft:ocelot"), ("mc_tropical_fish", "minecraft:tropical_fish"),
|
||||
("mc_cod", "minecraft:cod"), ("mc_squid", "minecraft:squid"),
|
||||
("mc_dolphin", "minecraft:dolphin"),
|
||||
]
|
||||
# Species SPAWN-TUNING §B14 names as the point of each biome (reported first).
|
||||
FOCUS = {
|
||||
"river": ["fowlplay:duck", "fowlplay:gull"],
|
||||
"jungle": ["alexsmobs:capuchin_monkey", "crittersandcompanions:leaf_insect"],
|
||||
"warm_ocean": ["crittersandcompanions:sea_bunny"],
|
||||
}
|
||||
BIOMES = {
|
||||
"river": {"match": {"minecraft:river"}, "check": "minecraft:river"},
|
||||
"jungle": {"match": {"minecraft:jungle", "minecraft:sparse_jungle",
|
||||
"minecraft:bamboo_jungle"}, "check": "#minecraft:is_jungle"},
|
||||
"warm_ocean": {"match": {"minecraft:warm_ocean"}, "check": "minecraft:warm_ocean"},
|
||||
}
|
||||
|
||||
# Upstream defaults of the keys we check after load (SPAWN-TUNING "default -> new").
|
||||
EXPECT_DEFAULTS = {
|
||||
"alexsmobs-common.toml": {
|
||||
"gazelleSpawnWeight": 40, "capuchinMonkeySpawnWeight": 28, "elephantSpawnWeight": 30,
|
||||
"straddlerSpawnWeight": 70, "rockyRollerSpawnWeight": 60, "underminerSpawnWeight": 50,
|
||||
"blobfishSpawnWeight": 30, "mudskipperSpawnWeight": 28, "caimanSpawnWeight": 29},
|
||||
"fowlplay.json5": {
|
||||
"duckSpawnWeight": 4, "duckMaxGroupSize": 6, "gooseSpawnWeight": 4,
|
||||
"gooseMaxGroupSize": 4, "gullSpawnWeight": 5, "gullMaxGroupSize": 8,
|
||||
"sparrowSpawnWeight": 20, "sparrowMaxGroupSize": 6},
|
||||
"crittersandcompanions-common.json5": {
|
||||
"sea_bunny@minecraft:warm_ocean": 32, "leaf_insect@#c:is_jungle": 14,
|
||||
"leaf_insect@#c:is_forest": 6, "dragonfly@#c:is_lush": 10},
|
||||
}
|
||||
|
||||
COUNT_RE = re.compile(r"\bB14 (?P<sid>b14-\d+) (?P<biome>[a-z_]+) t=(?P<t>\d+) (?P<rest>.*)$")
|
||||
DONE_RE = re.compile(r"B14_DONE (b14-\d+)")
|
||||
FUNC_FAIL_RE = re.compile(r"Failed to load function b14:|Unknown function b14:")
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"[b14] {msg}", flush=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- config variants
|
||||
|
||||
def shipped_texts(rev="HEAD", repo=bench.REPO):
|
||||
out = {}
|
||||
for name in SPAWN_FILES:
|
||||
r = subprocess.run(["git", "-C", str(repo), "show", f"{rev}:config/{name}"],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git show {rev}:config/{name} failed: {r.stderr.strip()}")
|
||||
out[name] = r.stdout
|
||||
return out
|
||||
|
||||
|
||||
def render_variant(config, shipped):
|
||||
"""{file name: text to write, or None = delete (mod regenerates defaults)}."""
|
||||
if config == "defaults":
|
||||
return {name: None for name in SPAWN_FILES}
|
||||
if config == "shipped":
|
||||
return {name: shipped[name] for name in SPAWN_FILES}
|
||||
raise ValueError(f"unknown config {config!r}")
|
||||
|
||||
|
||||
def _cac_weight(text, mob, spec):
|
||||
m = re.search(rf'"?{re.escape(mob)}"?\s*:\s*\[(.*?)\]', text, re.S)
|
||||
if not m:
|
||||
return None
|
||||
for entry in m.group(1).split("}"):
|
||||
if f'"{spec}"' in entry:
|
||||
w = re.search(r'"?weight"?\s*:\s*(-?\d+)', entry)
|
||||
return int(w.group(1)) if w else None
|
||||
return None
|
||||
|
||||
|
||||
def extract_values(name, text, keys):
|
||||
"""Values of `keys` from a partial or mod-re-saved spawn file (None = absent)."""
|
||||
out = {}
|
||||
for k in keys:
|
||||
if name.endswith(".toml"):
|
||||
m = re.search(rf"^\s*{re.escape(k)}\s*=\s*(-?\d+)", text, re.M)
|
||||
out[k] = int(m.group(1)) if m else None
|
||||
elif "@" in k:
|
||||
mob, spec = k.split("@", 1)
|
||||
out[k] = _cac_weight(text, mob, spec)
|
||||
else:
|
||||
m = re.search(rf'"?{re.escape(k)}"?\s*:\s*(-?\d+)', text)
|
||||
out[k] = int(m.group(1)) if m else None
|
||||
return out
|
||||
|
||||
|
||||
def expected_values(config, shipped):
|
||||
if config == "defaults":
|
||||
return {n: dict(v) for n, v in EXPECT_DEFAULTS.items()}
|
||||
return {n: extract_values(n, shipped[n], EXPECT_DEFAULTS[n]) for n in SPAWN_FILES}
|
||||
|
||||
|
||||
def check_config(game, config, shipped):
|
||||
"""Compare what the mods re-saved after load with what the variant should produce."""
|
||||
want = expected_values(config, shipped)
|
||||
got, mismatch = {}, []
|
||||
for name in SPAWN_FILES:
|
||||
p = Path(game) / "config" / name
|
||||
if not p.is_file() and config == "defaults":
|
||||
got[name] = dict(want[name]) # never written: the mod ran on its defaults
|
||||
continue
|
||||
text = p.read_text(errors="replace") if p.is_file() else ""
|
||||
got[name] = extract_values(name, text, want[name])
|
||||
for k, v in want[name].items():
|
||||
if got[name][k] != v:
|
||||
mismatch.append(f"{name}:{k} want {v} got {got[name][k]}")
|
||||
return {"ok": not mismatch, "mismatch": mismatch, "values": got}
|
||||
|
||||
|
||||
def apply_spawn_config(game, journal, files):
|
||||
for name, text in files.items():
|
||||
rel = f"config/{name}"
|
||||
journal.backup_file(rel)
|
||||
p = Path(game) / rel
|
||||
if text is None:
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
else:
|
||||
p.write_text(text)
|
||||
|
||||
|
||||
def config_diff(game, files, shipped):
|
||||
"""Key-level plan of the config change (for --dry-run): checked keys now -> after."""
|
||||
out = []
|
||||
for name, text in files.items():
|
||||
p = Path(game) / "config" / name
|
||||
cur = p.read_text(errors="replace") if p.is_file() else None
|
||||
keys = EXPECT_DEFAULTS[name]
|
||||
now = extract_values(name, cur, keys) if cur else {}
|
||||
if text is None:
|
||||
out.append(f"--- config/{name}: DELETE ({len(cur)} bytes now)" if cur
|
||||
else f"--- config/{name}: DELETE (absent now)")
|
||||
out.append(" mod regenerates upstream defaults on load")
|
||||
after = keys
|
||||
else:
|
||||
out.append(f"--- config/{name}: WRITE shipped partial ({len(text)} bytes)")
|
||||
after = extract_values(name, shipped[name], keys)
|
||||
for k in keys:
|
||||
mark = "" if now.get(k) == after[k] else " <- changes"
|
||||
out.append(f" {k}: {now.get(k)} -> {after[k]}{mark}")
|
||||
if text is not None:
|
||||
out.extend(" | " + ln for ln in text.splitlines())
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- datapack
|
||||
|
||||
def _score(name):
|
||||
return {"score": {"name": f"#{name}", "objective": "b14"}}
|
||||
|
||||
|
||||
def count_function():
|
||||
lines = ["# B14 entity counts around the executing position (the player)",
|
||||
"scoreboard players set #tuned b14 0"]
|
||||
for alias, eid in TUNED + VANILLA:
|
||||
lines.append(f"execute store result score #{alias} b14 "
|
||||
f"if entity @e[type={eid},distance=..128]")
|
||||
for alias, _ in TUNED:
|
||||
lines.append(f"scoreboard players operation #tuned b14 += #{alias} b14")
|
||||
lines.append("execute store result score #total b14 "
|
||||
"if entity @e[type=!minecraft:player,distance=..128]")
|
||||
lines.append("execute store result score #nonitem b14 if entity "
|
||||
"@e[type=!minecraft:player,type=!minecraft:item,"
|
||||
"type=!minecraft:experience_orb,distance=..128]")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def tellraw(sid, biome, t):
|
||||
parts = ["", {"text": f"B14 {sid} {biome} t={t} biome_ok="}, _score("biome_ok")]
|
||||
for key, alias in [("total", "total"), ("nonitem", "nonitem"), ("tuned", "tuned")]:
|
||||
parts += [{"text": f" {key}="}, _score(alias)]
|
||||
for alias, eid in TUNED + VANILLA:
|
||||
parts += [{"text": f" {eid}="}, _score(alias)]
|
||||
return "tellraw @a " + json.dumps(parts, separators=(",", ":"))
|
||||
|
||||
|
||||
def build_timeline(sid, sites, minutes, settle_s, arrive_s=10, gen_s=5):
|
||||
"""[(commands, delay_ticks_to_next)] for the unrolled plan."""
|
||||
ns = f"b14:{sid}"
|
||||
tl = []
|
||||
for biome, s in sites:
|
||||
x, y, z = s["x"], s["y"], s["z"]
|
||||
pos = f"{x + 0.5} {y} {z + 0.5}"
|
||||
# proto-chunk sites come from the pre-feature heightmap: clear a 3x3x3 pocket in
|
||||
# case a tree landed on the column, then stand on a barrier (also over water).
|
||||
hold = [f"fill {x - 1} {y} {z - 1} {x + 1} {y + 2} {z + 1} minecraft:air",
|
||||
f"setblock {x} {y - 1} {z} minecraft:barrier",
|
||||
f"execute in minecraft:overworld run tp @a {pos} 0 0"]
|
||||
tl.append(([f"execute in minecraft:overworld run tp @a {x + 0.5} {y + 2} {z + 0.5} 0 0"],
|
||||
gen_s * 20))
|
||||
tl.append((hold + [f"say B14_ARRIVE {sid} {biome} {x} {y} {z}"], arrive_s * 20))
|
||||
for t in range(minutes + 1):
|
||||
cmds = hold + [
|
||||
f"execute as @a[limit=1] at @s run function {ns}/count",
|
||||
f"execute as @a[limit=1] at @s store success score #biome_ok b14 "
|
||||
f"if biome ~ ~ ~ {BIOMES[biome]['check']}",
|
||||
tellraw(sid, biome, t)]
|
||||
tl.append((cmds, 1200 if t < minutes else 20))
|
||||
tl.append(([f"say B14_DONE {sid}"], 0))
|
||||
return tl
|
||||
|
||||
|
||||
def plan_seconds(n_sites, minutes, settle_s, arrive_s=10, gen_s=5):
|
||||
return settle_s + n_sites * (gen_s + arrive_s + minutes * 60 + 1)
|
||||
|
||||
|
||||
def generate_datapack(sid, sites, minutes, settle_s):
|
||||
"""{relative path: text}; namespace b14, functions under b14:<sid>/."""
|
||||
ns, holder = f"b14:{sid}", "#armed"
|
||||
base = f"data/b14/function/{sid}"
|
||||
files = {
|
||||
"pack.mcmeta": json.dumps({"pack": {
|
||||
"pack_format": 48,
|
||||
"description": f"Onion B14 spawn plan {sid} (generated by b14.py; "
|
||||
f"throwaway world)"}}) + "\n",
|
||||
"data/minecraft/tags/function/load.json": json.dumps({"values": [f"{ns}/load"]}) + "\n",
|
||||
f"{base}/load.mcfunction":
|
||||
"scoreboard objectives add b14 dummy\n"
|
||||
f"execute unless score {holder} b14 matches 1.. run schedule function {ns}/poll 20t\n",
|
||||
f"{base}/poll.mcfunction":
|
||||
f"execute if entity @a run function {ns}/begin\n"
|
||||
f"execute unless entity @a run schedule function {ns}/poll 20t\n",
|
||||
f"{base}/begin.mcfunction":
|
||||
f"scoreboard players set {holder} b14 1\n"
|
||||
# Creative: counts as a spawning/despawning player (spectators don't), and
|
||||
# can't die. Every rule that changes spawning is pinned, not inherited.
|
||||
"gamemode creative @a\n"
|
||||
"gamerule doMobSpawning true\n"
|
||||
"gamerule doDaylightCycle false\n"
|
||||
"gamerule doWeatherCycle false\n"
|
||||
"gamerule doPatrolSpawning false\n"
|
||||
"gamerule doTraderSpawning false\n"
|
||||
"gamerule doInsomnia false\n"
|
||||
"gamerule doWardenSpawning false\n"
|
||||
"time set 6000\n"
|
||||
"weather clear\n"
|
||||
f"say B14_PLAN {sid} begin\n"
|
||||
f"schedule function {ns}/step0 {max(1, settle_s * 20)}t\n",
|
||||
f"{base}/count.mcfunction": count_function(),
|
||||
}
|
||||
tl = build_timeline(sid, sites, minutes, settle_s)
|
||||
for i, (cmds, delay) in enumerate(tl):
|
||||
body = list(cmds)
|
||||
if delay > 0 and i + 1 < len(tl):
|
||||
body.append(f"schedule function {ns}/step{i + 1} {delay}t")
|
||||
files[f"{base}/step{i}.mcfunction"] = "\n".join(body) + "\n"
|
||||
return files
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- world
|
||||
|
||||
def world_members(zip_path):
|
||||
"""Members copied into the throwaway world: level.dat, the bench datapack (it is in
|
||||
level.dat's enabled list; no load/tick tags) and serverconfig/. No chunks/entities/
|
||||
data/ (forceloads, DH db) so every chunk is generated under the measured config."""
|
||||
prefix = bench.zip_world_root(zip_path)
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
names = z.namelist()
|
||||
keep = []
|
||||
for n in names:
|
||||
rel = n[len(prefix):]
|
||||
if n.endswith("/") or not n.startswith(prefix):
|
||||
continue
|
||||
if rel == "level.dat" or rel.startswith(("datapacks/onion-bench/", "serverconfig/")):
|
||||
keep.append((n, rel))
|
||||
if not any(rel == "level.dat" for _, rel in keep):
|
||||
raise ValueError(f"{zip_path}: no level.dat")
|
||||
return keep
|
||||
|
||||
|
||||
def build_world(game, journal, zip_path, files):
|
||||
root = Path(game) / "saves" / WORLD
|
||||
if root.exists():
|
||||
raise ValueError(f"{root} exists (stale B14 world?) - run `b14.py restore` or remove it")
|
||||
members = world_members(zip_path)
|
||||
journal.record({"op": "tree", "path": f"saves/{WORLD}"})
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
for n, rel in members:
|
||||
dst = root / rel
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
dst.write_bytes(z.read(n))
|
||||
write_pack(root, files)
|
||||
return root
|
||||
|
||||
|
||||
def write_pack(world_root, files):
|
||||
d = Path(world_root) / "datapacks" / PACK
|
||||
for rel, text in files.items():
|
||||
p = d / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(text)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- locate (offline)
|
||||
|
||||
# Chunk statuses whose biome palette is final (before "biomes" it is a placeholder).
|
||||
BIOME_STATUSES = {"biomes", "noise", "surface", "carvers", "features", "initialize_light",
|
||||
"light", "spawn", "full"}
|
||||
|
||||
|
||||
def _chunk_info(chunk):
|
||||
"""Surface samples of one chunk, or None. Full chunks use MOTION_BLOCKING_NO_LEAVES
|
||||
(ground under trees, water surface); proto chunks the worldgen WORLD_SURFACE_WG."""
|
||||
status = str(chunk.get("Status", "")).split(":")[-1]
|
||||
if status not in BIOME_STATUSES:
|
||||
return None
|
||||
hm = nbt.heightmap(chunk) or nbt.heightmap(chunk, "WORLD_SURFACE_WG")
|
||||
if hm is None:
|
||||
return None
|
||||
ys = {}
|
||||
for y in hm:
|
||||
ys[y] = ys.get(y, 0) + 1
|
||||
mode_y = max(ys, key=lambda k: (ys[k], -k))
|
||||
samples = []
|
||||
for lx, lz in ((2, 2), (2, 10), (10, 2), (10, 10), (8, 8)):
|
||||
y = hm[lz * 16 + lx]
|
||||
samples.append(nbt.biome_at(chunk, lx, y - 1, lz))
|
||||
col = None # a column at the modal height (ground, not a trunk or hole), near centre
|
||||
best = None
|
||||
for lz in range(16):
|
||||
for lx in range(16):
|
||||
if hm[lz * 16 + lx] == mode_y:
|
||||
d = (lx - 8) ** 2 + (lz - 8) ** 2
|
||||
if best is None or d < best:
|
||||
best, col = d, (lx, lz)
|
||||
centre = nbt.biome_at(chunk, col[0], mode_y - 1, col[1])
|
||||
return {"samples": samples, "y": mode_y, "col": col, "centre": centre,
|
||||
"full": status == "full"}
|
||||
|
||||
|
||||
def scan_world(zip_path, stride=1):
|
||||
"""{(cx, cz): info} for every fully generated overworld chunk in the zip."""
|
||||
prefix = bench.zip_world_root(zip_path)
|
||||
out = {}
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
regions = [i for i in z.infolist()
|
||||
if i.filename.startswith(prefix + "region/") and i.filename.endswith(".mca")
|
||||
and i.file_size > 8192]
|
||||
for info in regions:
|
||||
rx, rz = (int(v) for v in info.filename.rsplit("/", 1)[1].split(".")[1:3])
|
||||
for lx, lz, chunk in nbt.region_chunks(z.read(info.filename)):
|
||||
if stride > 1 and (lx % stride or lz % stride):
|
||||
continue
|
||||
ci = _chunk_info(chunk)
|
||||
if ci:
|
||||
out[(rx * 32 + lx, rz * 32 + lz)] = ci
|
||||
return out
|
||||
|
||||
|
||||
def pick_sites(chunks, avoid, min_gap=320, avoid_gap=192, radius=2):
|
||||
"""Best chunk per target biome: centre column in the biome, maximal share of the
|
||||
biome in the (2r+1)^2 neighbourhood; far from `avoid` points and from each other."""
|
||||
chosen = {}
|
||||
for biome, spec in BIOMES.items():
|
||||
match = spec["match"]
|
||||
cands = []
|
||||
for (cx, cz), ci in chunks.items():
|
||||
if ci["centre"] not in match:
|
||||
continue
|
||||
hit = tot = forest = 0
|
||||
for dx in range(-radius, radius + 1):
|
||||
for dz in range(-radius, radius + 1):
|
||||
n = chunks.get((cx + dx, cz + dz))
|
||||
if not n:
|
||||
continue
|
||||
for b in n["samples"]:
|
||||
tot += 1
|
||||
hit += b in match
|
||||
forest += bool(b) and "forest" in b
|
||||
if tot < 5 * 9:
|
||||
continue # fewer than 9 known chunks around it
|
||||
score = hit / tot + (0.5 * forest / tot if biome == "river" else 0.0) \
|
||||
+ (0.05 if ci["full"] else 0.0)
|
||||
x, z = cx * 16 + ci["col"][0], cz * 16 + ci["col"][1]
|
||||
cands.append((score, x, ci["y"], z, hit / tot, ci["full"], ci["centre"]))
|
||||
for score, x, y, z, frac, full, centre in sorted(cands, reverse=True):
|
||||
pts = list(avoid) + [(s["x"], s["z"]) for s in chosen.values()]
|
||||
gaps = [avoid_gap] * len(avoid) + [min_gap] * len(chosen)
|
||||
if all((x - px) ** 2 + (z - pz) ** 2 >= g * g for (px, pz), g in zip(pts, gaps)):
|
||||
chosen[biome] = {"x": x, "y": y, "z": z, "biome": centre,
|
||||
"frac": round(frac, 3), "score": round(score, 3),
|
||||
"full_chunk": full, "candidates": len(cands)}
|
||||
break
|
||||
return chosen
|
||||
|
||||
|
||||
def avoid_points(zip_path):
|
||||
prefix = bench.zip_world_root(zip_path)
|
||||
with zipfile.ZipFile(zip_path) as z:
|
||||
_, ld = nbt.load_gzip(z.read(prefix + "level.dat"))
|
||||
d = ld.get("Data", {})
|
||||
pts = [(d.get("SpawnX", 0), d.get("SpawnZ", 0))]
|
||||
pos = (d.get("Player") or {}).get("Pos")
|
||||
if pos:
|
||||
pts.append((int(pos[0]), int(pos[2])))
|
||||
seed = (d.get("WorldGenSettings") or {}).get("seed")
|
||||
return pts, seed
|
||||
|
||||
|
||||
def cmd_locate(a):
|
||||
t0 = time.time()
|
||||
chunks = scan_world(a.zip, a.stride)
|
||||
avoid, seed = avoid_points(a.zip)
|
||||
sites = pick_sites(chunks, avoid)
|
||||
hist = {}
|
||||
for ci in chunks.values():
|
||||
hist[ci["centre"]] = hist.get(ci["centre"], 0) + 1
|
||||
log(f"scanned {len(chunks)} chunks with final biomes in {time.time() - t0:.0f}s; avoid {avoid}")
|
||||
for b, n in sorted(hist.items(), key=lambda kv: -kv[1])[:25]:
|
||||
print(f" {n:6d} {b}")
|
||||
missing = sorted(set(BIOMES) - set(sites))
|
||||
doc = {"source_zip": str(a.zip), "source_sha256": bench.sha256_file(a.zip),
|
||||
"seed": seed, "avoid": avoid, "stride": a.stride,
|
||||
"note": "y = first free block (full chunks: MOTION_BLOCKING_NO_LEAVES, proto "
|
||||
"chunks: WORLD_SURFACE_WG); the player stands on a barrier at y-1. "
|
||||
"Valid only for this seed + mod set (biomes are seed-deterministic).",
|
||||
"sites": sites}
|
||||
print(json.dumps(doc, indent=1))
|
||||
if a.out:
|
||||
Path(a.out).write_text(json.dumps(doc, indent=1) + "\n")
|
||||
log(f"wrote {a.out}")
|
||||
if missing:
|
||||
log(f"NO SITE for {missing}: not in the generated chunks; see README (manual locate)")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- parse / summary
|
||||
|
||||
def parse_count_line(line):
|
||||
m = COUNT_RE.search(line)
|
||||
if not m:
|
||||
return None
|
||||
vals = {}
|
||||
for tok in m.group("rest").split():
|
||||
k, sep, v = tok.partition("=")
|
||||
if sep and re.fullmatch(r"-?\d+", v):
|
||||
vals[k] = int(v)
|
||||
return {"sid": m.group("sid"), "biome": m.group("biome"), "t": int(m.group("t")),
|
||||
"biome_ok": vals.pop("biome_ok", None), "total": vals.pop("total", None),
|
||||
"nonitem": vals.pop("nonitem", None), "tuned": vals.pop("tuned", None),
|
||||
"counts": vals}
|
||||
|
||||
|
||||
def parse_lines(lines, sid=None):
|
||||
"""Count rows from log lines; the client logs each tellraw once as [CHAT]."""
|
||||
rows, seen = [], set()
|
||||
for ln in lines:
|
||||
r = parse_count_line(ln)
|
||||
if not r or (sid and r["sid"] != sid):
|
||||
continue
|
||||
key = (r["sid"], r["biome"], r["t"])
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
rows.append(r)
|
||||
return rows
|
||||
|
||||
|
||||
def load_rows(path):
|
||||
p = Path(path)
|
||||
if not p.exists():
|
||||
return [], []
|
||||
rows = [json.loads(ln) for ln in p.read_text().splitlines() if ln.strip()]
|
||||
return ([r for r in rows if r.get("kind") == "b14_count"],
|
||||
[r for r in rows if r.get("kind") == "b14_session"])
|
||||
|
||||
|
||||
def summarize(counts, sessions):
|
||||
"""{biome: {config: stats}} plus verdict per SPAWN-TUNING §B14."""
|
||||
bad = {s["sid"] for s in sessions
|
||||
if s.get("outcome") != "ok" or not (s.get("config_check") or {}).get("ok")}
|
||||
runs = {}
|
||||
for r in counts:
|
||||
if r["sid"] in bad:
|
||||
continue
|
||||
runs.setdefault((r["biome"], r["config"], r["sid"]), []).append(r)
|
||||
out = {}
|
||||
for (biome, config, sid), rs in runs.items():
|
||||
rs.sort(key=lambda r: r["t"])
|
||||
final = rs[-1]
|
||||
st = out.setdefault(biome, {}).setdefault(config, {
|
||||
"runs": 0, "final_total": [], "final_nonitem": [], "final_tuned": [],
|
||||
"seen": {}, "final": {}, "biome_ok": True, "minutes": []})
|
||||
st["runs"] += 1
|
||||
st["minutes"].append(final["t"])
|
||||
st["final_total"].append(final["total"])
|
||||
st["final_nonitem"].append(final["nonitem"])
|
||||
st["final_tuned"].append(final["tuned"])
|
||||
st["biome_ok"] &= all(r.get("biome_ok") == 1 for r in rs)
|
||||
for eid in set().union(*(r["counts"] for r in rs)):
|
||||
mx = max(r["counts"].get(eid, 0) for r in rs)
|
||||
st["seen"][eid] = max(st["seen"].get(eid, 0), mx)
|
||||
st["final"].setdefault(eid, []).append(final["counts"].get(eid, 0))
|
||||
tuned_ids = {eid for _, eid in TUNED}
|
||||
verdicts = {}
|
||||
for biome, by in out.items():
|
||||
d, s = by.get("defaults"), by.get("shipped")
|
||||
if not (d and s):
|
||||
verdicts[biome] = {"verdict": "incomplete",
|
||||
"have": sorted(by)}
|
||||
continue
|
||||
dt, stt = statistics.median(d["final_total"]), statistics.median(s["final_total"])
|
||||
gone = sorted(e for e in tuned_ids if d["seen"].get(e, 0) > 0 and s["seen"].get(e, 0) == 0)
|
||||
rare = sorted(e for e in tuned_ids
|
||||
if d["seen"].get(e, 0) >= 4 and 0 < s["seen"].get(e, 0) < 0.25 * d["seen"][e])
|
||||
vanilla_gone = sorted(e for _, e in VANILLA if e in ("minecraft:cow", "minecraft:sheep")
|
||||
and d["seen"].get(e, 0) > 0 and s["seen"].get(e, 0) == 0)
|
||||
ok_biome = d["biome_ok"] and s["biome_ok"]
|
||||
v = "keep" if stt < dt and not gone else ("back off" if gone else "no drop")
|
||||
verdicts[biome] = {"verdict": v if ok_biome else "invalid (biome check failed)",
|
||||
"total_defaults": dt, "total_shipped": stt, "disappeared": gone,
|
||||
"rare_warn": rare, "vanilla_gone": vanilla_gone}
|
||||
vs = [v["verdict"] for v in verdicts.values()]
|
||||
overall = ("incomplete" if not vs or len(verdicts) < len(BIOMES)
|
||||
or any(v not in ("keep", "back off", "no drop") for v in vs)
|
||||
else "back off" if "back off" in vs else "keep" if all(v == "keep" for v in vs)
|
||||
else "review")
|
||||
return out, verdicts, overall
|
||||
|
||||
|
||||
def render_summary(out, verdicts, overall):
|
||||
lines = ["| biome | config | runs | final total | nonitem | tuned | focus (max seen) |",
|
||||
"|---|---|---|---|---|---|---|"]
|
||||
for biome in sorted(out):
|
||||
for config in ("defaults", "shipped"):
|
||||
st = out[biome].get(config)
|
||||
if not st:
|
||||
continue
|
||||
focus = ", ".join(f"{e.split(':')[1]}={st['seen'].get(e, 0)}"
|
||||
for e in FOCUS.get(biome, []))
|
||||
lines.append(f"| {biome} | {config} | {st['runs']} | "
|
||||
f"{statistics.median(st['final_total'])} | "
|
||||
f"{statistics.median(st['final_nonitem'])} | "
|
||||
f"{statistics.median(st['final_tuned'])} | {focus} |")
|
||||
lines.append("")
|
||||
for biome, v in sorted(verdicts.items()):
|
||||
lines.append(f"- **{biome}**: {v['verdict']}"
|
||||
+ (f" (total {v['total_defaults']} -> {v['total_shipped']}"
|
||||
f"{'; disappeared ' + ', '.join(v['disappeared']) if v['disappeared'] else ''}"
|
||||
f"{'; rare ' + ', '.join(v['rare_warn']) if v['rare_warn'] else ''}"
|
||||
f"{'; vanilla gone ' + ', '.join(v['vanilla_gone']) if v['vanilla_gone'] else ''})"
|
||||
if "total_defaults" in v else f" (have {v.get('have')})"))
|
||||
lines.append(f"\n**Overall: {overall}** (keep = totals drop in every biome and no tuned "
|
||||
f"species that appears under defaults vanishes; SPAWN-TUNING §B14)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def cmd_summary(a):
|
||||
counts, sessions = load_rows(a.file)
|
||||
if not counts:
|
||||
log(f"no B14 rows in {a.file}")
|
||||
return 1
|
||||
out, verdicts, overall = summarize(counts, sessions)
|
||||
print(render_summary(out, verdicts, overall))
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_parse(a):
|
||||
rows = parse_lines(Path(a.log).read_text(errors="replace").splitlines())
|
||||
for r in rows:
|
||||
print(json.dumps(r, sort_keys=True))
|
||||
return 0 if rows else 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- run
|
||||
|
||||
def _ancestors(pid):
|
||||
out = set()
|
||||
while pid > 1 and pid not in out:
|
||||
out.add(pid)
|
||||
try:
|
||||
stat = Path(f"/proc/{pid}/stat").read_text()
|
||||
pid = int(stat.rsplit(")", 1)[1].split()[1])
|
||||
except (OSError, ValueError, IndexError):
|
||||
break
|
||||
return out
|
||||
|
||||
|
||||
def bench_busy():
|
||||
"""Other harness processes (matrix runner / bench.py session / another B14 run) that
|
||||
share the instance; our own process and its parent shells are excluded."""
|
||||
mine = _ancestors(os.getpid())
|
||||
busy = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit() or int(d.name) in mine:
|
||||
continue
|
||||
try:
|
||||
args = (d / "cmdline").read_bytes().split(b"\0")
|
||||
except OSError:
|
||||
continue
|
||||
s = " ".join(x.decode(errors="replace") for x in args)
|
||||
if re.search(r"\bmatrix\.py run\b|\bbench\.py session\b|\bb14\.py run\b", s) \
|
||||
and "--dry-run" not in s:
|
||||
busy.append(int(d.name))
|
||||
return busy
|
||||
|
||||
|
||||
def load_sites(path, biomes):
|
||||
doc = json.loads(Path(path).read_text())
|
||||
sites = doc.get("sites", {})
|
||||
missing = [b for b in biomes if b not in sites]
|
||||
if missing:
|
||||
raise ValueError(f"{path}: no site for {missing} (run `b14.py locate` or add by hand)")
|
||||
return [(b, sites[b]) for b in biomes], doc
|
||||
|
||||
|
||||
def cmd_run(a):
|
||||
biomes = list(BIOMES) if a.biome == "all" else bench.split_list(a.biome)
|
||||
for b in biomes:
|
||||
if b not in BIOMES:
|
||||
sys.exit(f"unknown biome {b!r}; choose from {sorted(BIOMES)}")
|
||||
try:
|
||||
sites, sites_doc = load_sites(a.sites, biomes)
|
||||
shipped = shipped_texts(a.rev)
|
||||
files = render_variant(a.config, shipped)
|
||||
world_members(a.source_zip)
|
||||
except (ValueError, OSError, RuntimeError, zipfile.BadZipFile) as e:
|
||||
sys.exit(str(e))
|
||||
sid = "b14-" + time.strftime("%Y%m%d%H%M%S")
|
||||
pack = generate_datapack(sid, sites, a.minutes, a.settle)
|
||||
budget = plan_seconds(len(sites), a.minutes, a.settle)
|
||||
timeout = a.timeout or int(a.boot_timeout + budget + 180)
|
||||
game = bench.GAME
|
||||
if a.dry_run:
|
||||
log(f"dry run {sid}: config={a.config} rev={a.rev} biomes={biomes} "
|
||||
f"minutes={a.minutes} plan {budget}s timeout {timeout}s dh={a.dh}")
|
||||
for b, s in sites:
|
||||
log(f" site {b}: {s}")
|
||||
log(f"world saves/{WORLD} <- {a.source_zip}: "
|
||||
f"{[rel for _, rel in world_members(a.source_zip)][:6]}...")
|
||||
print(config_diff(game, files, shipped))
|
||||
if bench.pending_exists():
|
||||
log(f"WARNING: {game / bench.PENDING} exists; a real run would refuse")
|
||||
busy = bench_busy()
|
||||
if busy:
|
||||
log(f"WARNING: harness processes running {busy}; a real run would refuse")
|
||||
for rel, text in pack.items():
|
||||
print(f"--- {PACK}/{rel}")
|
||||
print(text, end="")
|
||||
return 0
|
||||
bench.refuse_pending()
|
||||
busy = bench_busy()
|
||||
if busy:
|
||||
sys.exit(f"harness processes running (pids {busy}); B14 shares the bench instance")
|
||||
if bench.server_procs() and not a.allow_server:
|
||||
log("WARNING: a dedicated server is running (noise only; spawning is client-side)")
|
||||
env = bench.bench_env()
|
||||
bench.preflight(a.kill_stale)
|
||||
signal.signal(signal.SIGTERM, lambda *_: sys.exit(143))
|
||||
journal = bench.Journal.begin(game, sid)
|
||||
prism, state, tail = None, {"done": False, "fail": None}, None
|
||||
outcome, detail, check, meta, failed = "error", None, None, None, []
|
||||
t_launch = time.time()
|
||||
try:
|
||||
bench.reset_resource_packs(game)
|
||||
bench.apply_pins()
|
||||
bench.apply_dh(game, journal, a.dh == "on")
|
||||
apply_spawn_config(game, journal, files)
|
||||
build_world(game, journal, a.source_zip, pack)
|
||||
meta = {"git": bench.git_state(), "mods": bench.mods_hash(game / "mods"),
|
||||
"variant": bench.read_text(game / "BENCH_VARIANT.txt")}
|
||||
before = bench.crash_files()
|
||||
log(f"{sid}: config={a.config} biomes={biomes} {a.minutes} min, timeout {timeout}s")
|
||||
t_launch = time.time()
|
||||
prism = bench.launch(env, WORLD)
|
||||
tail = bench.LogTail(game / "logs" / "latest.log")
|
||||
|
||||
def on_line(line):
|
||||
r = parse_count_line(line)
|
||||
if r and r["sid"] == sid and "[CHAT]" in line:
|
||||
log(f" {r['biome']} t={r['t']} total={r['total']} tuned={r['tuned']} "
|
||||
f"biome_ok={r['biome_ok']}")
|
||||
if DONE_RE.search(line):
|
||||
state["done"] = True
|
||||
if FUNC_FAIL_RE.search(line):
|
||||
state["fail"] = line.strip()[:300]
|
||||
|
||||
outcome, detail, _ = bench.monitor(
|
||||
tail, prism, before, lambda: state["done"] or state["fail"], timeout, on_line)
|
||||
if state["fail"]:
|
||||
outcome, detail = "datapack_error", state["fail"]
|
||||
finally:
|
||||
if prism is not None:
|
||||
bench.kill_client(prism)
|
||||
if tail is not None:
|
||||
tail.poll()
|
||||
try:
|
||||
check = check_config(game, a.config, shipped) # mods' re-saved files, pre-restore
|
||||
except OSError as e:
|
||||
check = {"ok": False, "mismatch": [f"read failed: {e}"], "values": {}}
|
||||
failed = bench.restore_pending(game)
|
||||
for e, err in failed:
|
||||
log(f"RESTORE FAILED {e}: {err}")
|
||||
if failed:
|
||||
log(f"journal kept at {game / bench.PENDING}; fix and run `b14.py restore`")
|
||||
log_path = bench.archive_log(f"session-{sid}")
|
||||
rows = parse_lines(tail.lines if tail else [], sid)
|
||||
base = {"sid": sid, "config": a.config, "rev": a.rev, "minutes": a.minutes,
|
||||
"ts": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(t_launch)),
|
||||
"machine": bench.MACHINE}
|
||||
for r in rows:
|
||||
bench.append_jsonl(RESULTS_FILE, {**base, **r, "kind": "b14_count",
|
||||
"site": dict(sites)[r["biome"]]})
|
||||
bench.append_jsonl(RESULTS_FILE, {
|
||||
**base, "kind": "b14_session", "outcome": outcome, "detail": detail,
|
||||
"biomes": biomes, "sites": dict(sites), "sites_sha256": sites_doc.get("source_sha256"),
|
||||
"config_check": check, "rows": len(rows), "log": log_path,
|
||||
"git_sha": meta["git"][0] if meta else None,
|
||||
"git_dirty": meta["git"][1] if meta else None,
|
||||
"mods_hash": meta["mods"][0] if meta else None,
|
||||
"mods_count": meta["mods"][1] if meta else None,
|
||||
"bench_variant": meta["variant"] if meta else None,
|
||||
"restore_failed": [e for e, _ in failed]})
|
||||
log(f"{sid} {outcome}{': ' + detail if detail else ''}; {len(rows)} count rows; "
|
||||
f"config check {'ok' if check and check['ok'] else check and check['mismatch']}")
|
||||
want = len(sites) * (a.minutes + 1)
|
||||
return 0 if outcome == "ok" and len(rows) == want and check and check["ok"] else 1
|
||||
|
||||
|
||||
def cmd_restore(a):
|
||||
if bench.client_pids():
|
||||
sys.exit("bench client is running; close it before restoring")
|
||||
if not bench.pending_exists():
|
||||
log("nothing pending")
|
||||
return 0
|
||||
failed = bench.restore_pending(bench.GAME)
|
||||
for e, err in failed:
|
||||
log(f"FAILED {e}: {err}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
def build_parser():
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
r = sub.add_parser("run", help="one client launch: config variant x biome sites")
|
||||
r.add_argument("--config", required=True, choices=["defaults", "shipped"])
|
||||
r.add_argument("--biome", default="all",
|
||||
help="comma list of river,jungle,warm_ocean (in this order), or all")
|
||||
r.add_argument("--minutes", type=int, default=10)
|
||||
r.add_argument("--settle", type=int, default=20, help="seconds after join before site 1")
|
||||
r.add_argument("--sites", default=str(SITES_FILE))
|
||||
r.add_argument("--source-zip", default=str(SOURCE_ZIP),
|
||||
help="world whose level.dat (seed, gamerules) seeds the throwaway world")
|
||||
r.add_argument("--rev", default="HEAD", help="git rev of the shipped partial files")
|
||||
r.add_argument("--dh", choices=["on", "off"], default="off")
|
||||
r.add_argument("--boot-timeout", type=int, default=300)
|
||||
r.add_argument("--timeout", type=int, default=None)
|
||||
r.add_argument("--kill-stale", action="store_true")
|
||||
r.add_argument("--allow-server", action="store_true", help="silence the server warning")
|
||||
r.add_argument("--dry-run", action="store_true")
|
||||
r.set_defaults(fn=cmd_run)
|
||||
lo = sub.add_parser("locate", help="find biome sites offline from a world zip")
|
||||
lo.add_argument("--zip", default=str(SOURCE_ZIP))
|
||||
lo.add_argument("--out", default=None, help=f"write sites json (e.g. {SITES_FILE})")
|
||||
lo.add_argument("--stride", type=int, default=1, help="sample every Nth chunk")
|
||||
lo.set_defaults(fn=cmd_locate)
|
||||
s = sub.add_parser("summary", help="defaults vs shipped, SPAWN-TUNING §B14 rule")
|
||||
s.add_argument("--file", default=str(RESULTS_FILE))
|
||||
s.set_defaults(fn=cmd_summary)
|
||||
p = sub.add_parser("parse", help="print count rows found in a log file")
|
||||
p.add_argument("log")
|
||||
p.set_defaults(fn=cmd_parse)
|
||||
rs = sub.add_parser("restore", help="undo a hard-killed run (same journal as bench.py)")
|
||||
rs.set_defaults(fn=cmd_restore)
|
||||
return ap
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
a = build_parser().parse_args(argv)
|
||||
return a.fn(a)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Minimal read-only NBT / Anvil region reader (stdlib only) for B14 site finding.
|
||||
|
||||
Enough of the format to read level.dat and 1.18+ chunk biomes + heightmaps:
|
||||
https://minecraft.wiki/w/NBT_format, /Region_file_format, /Chunk_format.
|
||||
"""
|
||||
import gzip
|
||||
import io
|
||||
import math
|
||||
import struct
|
||||
import zlib
|
||||
|
||||
END, BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, BYTE_ARRAY, STRING, LIST, COMPOUND, \
|
||||
INT_ARRAY, LONG_ARRAY = range(13)
|
||||
|
||||
|
||||
class _Reader:
|
||||
def __init__(self, data):
|
||||
self.b, self.i = data, 0
|
||||
|
||||
def take(self, n):
|
||||
s = self.b[self.i:self.i + n]
|
||||
if len(s) != n:
|
||||
raise ValueError("truncated NBT")
|
||||
self.i += n
|
||||
return s
|
||||
|
||||
def unpack(self, fmt):
|
||||
size = struct.calcsize(fmt)
|
||||
return struct.unpack(fmt, self.take(size))
|
||||
|
||||
def string(self):
|
||||
(n,) = self.unpack(">H")
|
||||
return self.take(n).decode("utf-8", "replace")
|
||||
|
||||
def payload(self, t):
|
||||
if t == BYTE:
|
||||
return self.unpack(">b")[0]
|
||||
if t == SHORT:
|
||||
return self.unpack(">h")[0]
|
||||
if t == INT:
|
||||
return self.unpack(">i")[0]
|
||||
if t == LONG:
|
||||
return self.unpack(">q")[0]
|
||||
if t == FLOAT:
|
||||
return self.unpack(">f")[0]
|
||||
if t == DOUBLE:
|
||||
return self.unpack(">d")[0]
|
||||
if t == BYTE_ARRAY:
|
||||
(n,) = self.unpack(">i")
|
||||
return self.take(n)
|
||||
if t == STRING:
|
||||
return self.string()
|
||||
if t == LIST:
|
||||
et, n = self.unpack(">bi")
|
||||
return [self.payload(et) for _ in range(max(0, n))]
|
||||
if t == COMPOUND:
|
||||
out = {}
|
||||
while True:
|
||||
(ct,) = self.unpack(">b")
|
||||
if ct == END:
|
||||
return out
|
||||
name = self.string()
|
||||
out[name] = self.payload(ct)
|
||||
if t == INT_ARRAY:
|
||||
(n,) = self.unpack(">i")
|
||||
return list(struct.unpack(f">{n}i", self.take(4 * n)))
|
||||
if t == LONG_ARRAY:
|
||||
(n,) = self.unpack(">i")
|
||||
return list(struct.unpack(f">{n}q", self.take(8 * n)))
|
||||
raise ValueError(f"bad NBT tag type {t}")
|
||||
|
||||
|
||||
def loads(data):
|
||||
"""Parse an uncompressed NBT blob -> (root name, root compound)."""
|
||||
r = _Reader(data)
|
||||
(t,) = r.unpack(">b")
|
||||
if t != COMPOUND:
|
||||
raise ValueError("NBT root is not a compound")
|
||||
name = r.string()
|
||||
return name, r.payload(COMPOUND)
|
||||
|
||||
|
||||
def load_gzip(data):
|
||||
"""level.dat style (gzip) NBT."""
|
||||
return loads(gzip.decompress(data))
|
||||
|
||||
|
||||
def region_chunks(data):
|
||||
"""Yield (local_x, local_z, chunk_compound) for every chunk in an .mca blob."""
|
||||
if len(data) < 8192:
|
||||
return
|
||||
for idx in range(1024):
|
||||
off = int.from_bytes(data[idx * 4:idx * 4 + 3], "big")
|
||||
if off == 0:
|
||||
continue
|
||||
start = off * 4096
|
||||
if start + 5 > len(data):
|
||||
continue
|
||||
length = int.from_bytes(data[start:start + 4], "big")
|
||||
comp = data[start + 4]
|
||||
blob = data[start + 5:start + 4 + length]
|
||||
try:
|
||||
if comp == 2:
|
||||
raw = zlib.decompress(blob)
|
||||
elif comp == 1:
|
||||
raw = gzip.GzipFile(fileobj=io.BytesIO(blob)).read()
|
||||
elif comp == 3:
|
||||
raw = blob
|
||||
else: # 4 = lz4 (not in vanilla defaults) / 128+ = external .mcc
|
||||
continue
|
||||
_, chunk = loads(raw)
|
||||
except (ValueError, zlib.error, OSError, struct.error):
|
||||
continue
|
||||
yield idx % 32, idx // 32, chunk
|
||||
|
||||
|
||||
def _unsigned(v):
|
||||
return v & 0xFFFFFFFFFFFFFFFF
|
||||
|
||||
|
||||
def unpack_packed(longs, bits, count):
|
||||
"""1.16+ packed array: entries never span longs."""
|
||||
per = 64 // bits
|
||||
mask = (1 << bits) - 1
|
||||
out = []
|
||||
for i in range(count):
|
||||
word = _unsigned(longs[i // per])
|
||||
out.append((word >> ((i % per) * bits)) & mask)
|
||||
return out
|
||||
|
||||
|
||||
def section_biome_grid(section):
|
||||
"""64 biome ids (index = (y4*4 + z4)*4 + x4) for one chunk section, or None."""
|
||||
b = section.get("biomes")
|
||||
if not b or not b.get("palette"):
|
||||
return None
|
||||
pal = b["palette"]
|
||||
if len(pal) == 1 or "data" not in b:
|
||||
return [pal[0]] * 64
|
||||
bits = max(1, math.ceil(math.log2(len(pal))))
|
||||
return [pal[i] if i < len(pal) else pal[0] for i in unpack_packed(b["data"], bits, 64)]
|
||||
|
||||
|
||||
def heightmap(chunk, kind="MOTION_BLOCKING_NO_LEAVES", min_y=-64, height=384):
|
||||
"""256 world-y values (first free block above the top matching block), idx = z*16+x."""
|
||||
hm = (chunk.get("Heightmaps") or {}).get(kind)
|
||||
if not hm:
|
||||
return None
|
||||
bits = math.ceil(math.log2(height + 1))
|
||||
return [v + min_y for v in unpack_packed(hm, bits, 256)]
|
||||
|
||||
|
||||
def biome_at(chunk, x, y, z):
|
||||
"""Biome id at block (x, y, z) in this chunk (x, z may be world coords)."""
|
||||
sy = y >> 4
|
||||
for s in chunk.get("sections", []):
|
||||
if s.get("Y") == sy:
|
||||
grid = section_biome_grid(s)
|
||||
if grid is None:
|
||||
return None
|
||||
return grid[(((y & 15) >> 2) * 4 + ((z & 15) >> 2)) * 4 + ((x & 15) >> 2)]
|
||||
return None
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"source_zip": "/home/logan/onion-bench/worlds/bench-world-v1.zip",
|
||||
"source_sha256": "144caf5fcdfea1a0eac0c787fdcded949e1a67df6ac112400f37be30ce7fb76e",
|
||||
"seed": -306676217,
|
||||
"avoid": [
|
||||
[
|
||||
0,
|
||||
0
|
||||
],
|
||||
[
|
||||
3000,
|
||||
-9
|
||||
]
|
||||
],
|
||||
"stride": 1,
|
||||
"note": "y = first free block (full chunks: MOTION_BLOCKING_NO_LEAVES, proto chunks: WORLD_SURFACE_WG); the player stands on a barrier at y-1. Valid only for this seed + mod set (biomes are seed-deterministic).",
|
||||
"sites": {
|
||||
"river": {
|
||||
"x": 904,
|
||||
"y": 63,
|
||||
"z": 88,
|
||||
"biome": "minecraft:river",
|
||||
"frac": 0.373,
|
||||
"score": 0.373,
|
||||
"full_chunk": false,
|
||||
"candidates": 17
|
||||
},
|
||||
"jungle": {
|
||||
"x": -8,
|
||||
"y": 107,
|
||||
"z": -201,
|
||||
"biome": "minecraft:jungle",
|
||||
"frac": 0.52,
|
||||
"score": 0.52,
|
||||
"full_chunk": false,
|
||||
"candidates": 19
|
||||
},
|
||||
"warm_ocean": {
|
||||
"x": -200,
|
||||
"y": 63,
|
||||
"z": 56,
|
||||
"biome": "minecraft:warm_ocean",
|
||||
"frac": 0.75,
|
||||
"score": 0.75,
|
||||
"full_chunk": false,
|
||||
"candidates": 31
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
"""Offline tests for b14.py (temp dirs only; no game, no git writes)."""
|
||||
import gzip
|
||||
import json
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
sys.path.insert(0, str(HERE.parent.parent))
|
||||
import b14 # noqa: E402
|
||||
import bench # noqa: E402
|
||||
import nbt # noqa: E402
|
||||
|
||||
AM_PARTIAL = """# partial
|
||||
[spawning]
|
||||
\tgazelleSpawnWeight = 20
|
||||
\tcapuchinMonkeySpawnWeight = 14
|
||||
\telephantSpawnWeight = 15
|
||||
\tstraddlerSpawnWeight = 35
|
||||
\trockyRollerSpawnWeight = 30
|
||||
\tunderminerSpawnWeight = 25
|
||||
\tblobfishSpawnWeight = 15
|
||||
\tmudskipperSpawnWeight = 14
|
||||
\tcaimanSpawnWeight = 15
|
||||
"""
|
||||
FP_PARTIAL = """// partial
|
||||
{
|
||||
"duckSpawnWeight": 2, "duckMaxGroupSize": 4, "gooseSpawnWeight": 2,
|
||||
"gooseMaxGroupSize": 3, "gullSpawnWeight": 3, "gullMaxGroupSize": 5,
|
||||
"sparrowSpawnWeight": 12, "sparrowMaxGroupSize": 4
|
||||
}
|
||||
"""
|
||||
CAC_PARTIAL = """{
|
||||
"configVersion": 2,
|
||||
"spawning": {
|
||||
"leaf_insect": [
|
||||
{"biome_spec": "#c:is_jungle", "weight": 8, "min": 1, "max": 1},
|
||||
{"biome_spec": "#c:is_forest", "weight": 4, "min": 1, "max": 1}
|
||||
],
|
||||
"sea_bunny": [
|
||||
{"biome_spec": "minecraft:ocean", "weight": 10, "min": 1, "max": 2},
|
||||
{"biome_spec": "minecraft:warm_ocean", "weight": 20, "min": 1, "max": 3}
|
||||
],
|
||||
"dragonfly": [
|
||||
{"biome_spec": "#c:is_river", "weight": 4, "min": 1, "max": 1},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 1}
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
SHIPPED = {"alexsmobs-common.toml": AM_PARTIAL, "fowlplay.json5": FP_PARTIAL,
|
||||
"crittersandcompanions-common.json5": CAC_PARTIAL}
|
||||
|
||||
# What the mods write back (re-saved, json5 unquoted keys, tabs) with default values.
|
||||
AM_RESAVED_DEFAULT = "[spawning]\n" + "".join(
|
||||
f"\t#Spawn Weight\n\t# Default: {v}\n\t{k} = {v}\n"
|
||||
for k, v in b14.EXPECT_DEFAULTS["alexsmobs-common.toml"].items())
|
||||
FP_RESAVED_DEFAULT = "{\n" + ",\n".join(
|
||||
f"\t{k}: {v}" for k, v in b14.EXPECT_DEFAULTS["fowlplay.json5"].items()) + "\n}"
|
||||
CAC_RESAVED_DEFAULT = """{
|
||||
\tconfigVersion: 2,
|
||||
\tspawning: {
|
||||
\t\tleaf_insect: [
|
||||
\t\t\t{
|
||||
\t\t\t\tbiome_spec: "#c:is_jungle",
|
||||
\t\t\t\tweight: 14,
|
||||
\t\t\t\tmin: 1,
|
||||
\t\t\t\tmax: 1
|
||||
\t\t\t},
|
||||
\t\t\t{ biome_spec: "#c:is_forest", weight: 6, min: 1, max: 1 }
|
||||
\t\t],
|
||||
\t\tred_panda: [ { biome_spec: "#c:is_jungle", weight: 8, min: 1, max: 2 } ],
|
||||
\t\tsea_bunny: [
|
||||
\t\t\t{ biome_spec: "minecraft:ocean", weight: 16, min: 1, max: 2 },
|
||||
\t\t\t{ biome_spec: "minecraft:warm_ocean", weight: 32, min: 1, max: 4 }
|
||||
\t\t],
|
||||
\t\tdragonfly: [
|
||||
\t\t\t{ biome_spec: "#c:is_river", weight: 4, min: 1, max: 1 },
|
||||
\t\t\t{ biome_spec: "#c:is_lush", weight: 10, min: 1, max: 1 }
|
||||
\t\t]
|
||||
\t}
|
||||
}"""
|
||||
|
||||
|
||||
class ConfigVariants(unittest.TestCase):
|
||||
def test_render(self):
|
||||
d = b14.render_variant("defaults", SHIPPED)
|
||||
self.assertEqual(set(d), set(b14.SPAWN_FILES))
|
||||
self.assertTrue(all(v is None for v in d.values()))
|
||||
s = b14.render_variant("shipped", SHIPPED)
|
||||
self.assertEqual(s["fowlplay.json5"], FP_PARTIAL)
|
||||
with self.assertRaises(ValueError):
|
||||
b14.render_variant("nope", SHIPPED)
|
||||
|
||||
def test_extract_partial_and_resaved(self):
|
||||
keys = b14.EXPECT_DEFAULTS
|
||||
got = b14.extract_values("crittersandcompanions-common.json5", CAC_PARTIAL,
|
||||
keys["crittersandcompanions-common.json5"])
|
||||
self.assertEqual(got["sea_bunny@minecraft:warm_ocean"], 20)
|
||||
self.assertEqual(got["leaf_insect@#c:is_jungle"], 8)
|
||||
self.assertEqual(got["dragonfly@#c:is_lush"], 6)
|
||||
got = b14.extract_values("crittersandcompanions-common.json5", CAC_RESAVED_DEFAULT,
|
||||
keys["crittersandcompanions-common.json5"])
|
||||
self.assertEqual(got, keys["crittersandcompanions-common.json5"])
|
||||
self.assertEqual(b14.extract_values("fowlplay.json5", FP_RESAVED_DEFAULT,
|
||||
keys["fowlplay.json5"]), keys["fowlplay.json5"])
|
||||
self.assertEqual(b14.extract_values("alexsmobs-common.toml", AM_RESAVED_DEFAULT,
|
||||
keys["alexsmobs-common.toml"]),
|
||||
keys["alexsmobs-common.toml"])
|
||||
# "Default: 40" comment lines must not be mistaken for the value
|
||||
self.assertEqual(b14.extract_values("alexsmobs-common.toml", AM_PARTIAL,
|
||||
["gazelleSpawnWeight"]), {"gazelleSpawnWeight": 20})
|
||||
|
||||
def test_check_config_and_journaled_apply(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
game = Path(td)
|
||||
(game / "config").mkdir()
|
||||
originals = {"alexsmobs-common.toml": AM_RESAVED_DEFAULT + "# orig\n",
|
||||
"fowlplay.json5": FP_RESAVED_DEFAULT} # CAC absent on purpose
|
||||
for n, t in originals.items():
|
||||
(game / "config" / n).write_text(t)
|
||||
j = bench.Journal.begin(game, "b14-1")
|
||||
b14.apply_spawn_config(game, j, b14.render_variant("shipped", SHIPPED))
|
||||
for n in b14.SPAWN_FILES:
|
||||
self.assertEqual((game / "config" / n).read_text(), SHIPPED[n])
|
||||
self.assertTrue(b14.check_config(game, "shipped", SHIPPED)["ok"])
|
||||
chk = b14.check_config(game, "defaults", SHIPPED)
|
||||
self.assertFalse(chk["ok"])
|
||||
self.assertIn("fowlplay.json5:duckSpawnWeight want 4 got 2", chk["mismatch"])
|
||||
self.assertEqual(bench.restore_pending(game), [])
|
||||
for n, t in originals.items():
|
||||
self.assertEqual((game / "config" / n).read_text(), t)
|
||||
self.assertFalse((game / "config" / "crittersandcompanions-common.json5").exists())
|
||||
self.assertFalse((game / bench.PENDING).exists())
|
||||
|
||||
def test_defaults_delete_and_restore(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
game = Path(td)
|
||||
(game / "config").mkdir()
|
||||
for n in b14.SPAWN_FILES:
|
||||
(game / "config" / n).write_text("x" + n)
|
||||
j = bench.Journal.begin(game, "b14-2")
|
||||
b14.apply_spawn_config(game, j, b14.render_variant("defaults", SHIPPED))
|
||||
self.assertFalse(any((game / "config" / n).exists() for n in b14.SPAWN_FILES))
|
||||
self.assertTrue(b14.check_config(game, "defaults", SHIPPED)["ok"]) # absent = defaults
|
||||
self.assertFalse(b14.check_config(game, "shipped", SHIPPED)["ok"])
|
||||
# what the mods write after load
|
||||
(game / "config" / "alexsmobs-common.toml").write_text(AM_RESAVED_DEFAULT)
|
||||
(game / "config" / "fowlplay.json5").write_text(FP_RESAVED_DEFAULT)
|
||||
(game / "config" / "crittersandcompanions-common.json5").write_text(CAC_RESAVED_DEFAULT)
|
||||
self.assertTrue(b14.check_config(game, "defaults", SHIPPED)["ok"])
|
||||
bench.restore_pending(game)
|
||||
for n in b14.SPAWN_FILES:
|
||||
self.assertEqual((game / "config" / n).read_text(), "x" + n)
|
||||
|
||||
def test_config_diff(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
game = Path(td)
|
||||
(game / "config").mkdir()
|
||||
(game / "config" / "fowlplay.json5").write_text(FP_RESAVED_DEFAULT)
|
||||
txt = b14.config_diff(game, b14.render_variant("shipped", SHIPPED), SHIPPED)
|
||||
self.assertIn("duckSpawnWeight: 4 -> 2 <- changes", txt)
|
||||
self.assertIn("config/alexsmobs-common.toml: WRITE", txt)
|
||||
txt = b14.config_diff(game, b14.render_variant("defaults", SHIPPED), SHIPPED)
|
||||
self.assertIn("config/fowlplay.json5: DELETE", txt)
|
||||
|
||||
|
||||
SITES = [("river", {"x": 904, "y": 63, "z": 88}), ("warm_ocean", {"x": -200, "y": 63, "z": 56})]
|
||||
|
||||
|
||||
class Datapack(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.files = b14.generate_datapack("b14-20261018000000", SITES, 2, 20)
|
||||
self.base = "data/b14/function/b14-20261018000000"
|
||||
|
||||
def test_structure(self):
|
||||
f = self.files
|
||||
self.assertEqual(json.loads(f["pack.mcmeta"])["pack"]["pack_format"], 48)
|
||||
self.assertEqual(json.loads(f["data/minecraft/tags/function/load.json"]),
|
||||
{"values": ["b14:b14-20261018000000/load"]})
|
||||
begin = f[f"{self.base}/begin.mcfunction"]
|
||||
self.assertIn("gamerule doMobSpawning true", begin)
|
||||
self.assertIn("gamemode creative @a", begin)
|
||||
self.assertNotIn("spectator", "".join(f.values()))
|
||||
self.assertIn("step0 400t", begin)
|
||||
# 2 sites x (tp, arrive, t=0..2) + done
|
||||
steps = [k for k in f if "/step" in k]
|
||||
self.assertEqual(len(steps), 2 * (2 + 3) + 1)
|
||||
self.assertIn("B14_DONE", f[f"{self.base}/step{len(steps) - 1}.mcfunction"])
|
||||
|
||||
def test_counts_and_chain(self):
|
||||
count = self.files[f"{self.base}/count.mcfunction"]
|
||||
for _, eid in b14.TUNED + b14.VANILLA:
|
||||
self.assertIn(f"@e[type={eid},distance=..128]", count)
|
||||
self.assertIn("@e[type=!minecraft:player,distance=..128]", count)
|
||||
self.assertEqual(count.count("+= #"), len(b14.TUNED))
|
||||
tl = b14.build_timeline("b14-20261018000000", SITES, 2, 20)
|
||||
delays = [d for _, d in tl]
|
||||
self.assertEqual(delays[:5], [100, 200, 1200, 1200, 20])
|
||||
self.assertEqual(delays[-1], 0)
|
||||
for i in range(len(tl) - 1):
|
||||
body = self.files[f"{self.base}/step{i}.mcfunction"]
|
||||
self.assertIn(f"schedule function b14:b14-20261018000000/step{i + 1} ", body)
|
||||
s2 = self.files[f"{self.base}/step2.mcfunction"]
|
||||
self.assertIn("setblock 904 62 88 minecraft:barrier", s2)
|
||||
self.assertIn("tp @a 904.5 63 88.5", s2)
|
||||
self.assertIn("if biome ~ ~ ~ minecraft:river", s2)
|
||||
|
||||
def test_tellraw_roundtrip(self):
|
||||
"""The rendered tellraw text, with scores substituted, parses back."""
|
||||
cmd = b14.tellraw("b14-20261018000000", "river", 3)
|
||||
parts = json.loads(cmd[len("tellraw @a "):])
|
||||
text = "".join(p if isinstance(p, str) else p.get("text", "7") for p in parts)
|
||||
line = ("[18Oct2026 00:10:00.000] [Render thread/INFO] [net.minecraft.client.gui."
|
||||
f"components.ChatComponent/]: [System] [CHAT] {text}")
|
||||
r = b14.parse_count_line(line)
|
||||
self.assertEqual((r["sid"], r["biome"], r["t"], r["total"], r["biome_ok"]),
|
||||
("b14-20261018000000", "river", 3, 7, 7))
|
||||
self.assertEqual(set(r["counts"]), {eid for _, eid in b14.TUNED + b14.VANILLA})
|
||||
|
||||
def test_world_build(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
td = Path(td)
|
||||
zp = td / "w.zip"
|
||||
with zipfile.ZipFile(zp, "w") as z:
|
||||
z.writestr("onionbench/level.dat", b"lvl")
|
||||
z.writestr("onionbench/region/r.0.0.mca", b"x")
|
||||
z.writestr("onionbench/data/chunks.dat", b"x")
|
||||
z.writestr("onionbench/datapacks/onion-bench/pack.mcmeta", "{}")
|
||||
z.writestr("onionbench/serverconfig/a.toml", "a")
|
||||
game = td / "game"
|
||||
(game / "saves").mkdir(parents=True)
|
||||
j = bench.Journal.begin(game, "b14-3")
|
||||
root = b14.build_world(game, j, zp, self.files)
|
||||
self.assertEqual((root / "level.dat").read_bytes(), b"lvl")
|
||||
self.assertFalse((root / "region").exists())
|
||||
self.assertFalse((root / "data").exists())
|
||||
self.assertTrue((root / "serverconfig/a.toml").exists())
|
||||
self.assertTrue((root / "datapacks" / b14.PACK / "pack.mcmeta").exists())
|
||||
with self.assertRaises(ValueError):
|
||||
b14.build_world(game, bench.Journal(game, "x"), zp, self.files)
|
||||
bench.restore_pending(game)
|
||||
self.assertFalse(root.exists())
|
||||
|
||||
|
||||
def _line(sid, biome, t, total, counts, ok=1):
|
||||
body = " ".join(f"{k}={v}" for k, v in counts.items())
|
||||
return (f"[x] [Render thread/INFO] [ChatComponent/]: [System] [CHAT] B14 {sid} {biome} "
|
||||
f"t={t} biome_ok={ok} total={total} nonitem={total} tuned=0 {body}")
|
||||
|
||||
|
||||
class ParseAndSummary(unittest.TestCase):
|
||||
def test_parse_dedupes_and_filters(self):
|
||||
lines = [_line("b14-1", "river", 0, 5, {"fowlplay:duck": 1}),
|
||||
_line("b14-1", "river", 0, 5, {"fowlplay:duck": 1}),
|
||||
"[Server thread/INFO]: [Server] B14_ARRIVE b14-1 river 1 2 3",
|
||||
_line("b14-2", "river", 0, 9, {"fowlplay:duck": 2}),
|
||||
"noise B14 garbage"]
|
||||
rows = b14.parse_lines(lines)
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(len(b14.parse_lines(lines, "b14-2")), 1)
|
||||
self.assertEqual(rows[0]["counts"], {"fowlplay:duck": 1})
|
||||
|
||||
def _rows(self, config, sid, biome, totals, duck):
|
||||
out = []
|
||||
for t, (tot, d) in enumerate(zip(totals, duck)):
|
||||
r = b14.parse_count_line(_line(sid, biome, t, tot, {"fowlplay:duck": d,
|
||||
"minecraft:cow": 1}))
|
||||
out.append({**r, "config": config, "kind": "b14_count"})
|
||||
return out
|
||||
|
||||
def _sess(self, sid, ok=True):
|
||||
return {"kind": "b14_session", "sid": sid, "outcome": "ok",
|
||||
"config_check": {"ok": ok}}
|
||||
|
||||
def test_keep_and_back_off(self):
|
||||
counts = (self._rows("defaults", "b14-1", "river", [10, 40], [2, 6])
|
||||
+ self._rows("shipped", "b14-2", "river", [8, 25], [0, 3]))
|
||||
sess = [self._sess("b14-1"), self._sess("b14-2")]
|
||||
out, verdicts, overall = b14.summarize(counts, sess)
|
||||
self.assertEqual(verdicts["river"]["verdict"], "keep")
|
||||
self.assertEqual(out["river"]["shipped"]["seen"]["fowlplay:duck"], 3)
|
||||
self.assertEqual(overall, "incomplete") # jungle / warm_ocean missing
|
||||
counts2 = (self._rows("defaults", "b14-1", "river", [10, 40], [2, 6])
|
||||
+ self._rows("shipped", "b14-2", "river", [8, 25], [0, 0]))
|
||||
_, v2, o2 = b14.summarize(counts2, sess)
|
||||
self.assertEqual(v2["river"]["verdict"], "back off")
|
||||
self.assertEqual(v2["river"]["disappeared"], ["fowlplay:duck"])
|
||||
self.assertIn("back off", b14.render_summary(*b14.summarize(counts2, sess)))
|
||||
|
||||
def test_bad_sessions_and_biome_check_excluded(self):
|
||||
counts = (self._rows("defaults", "b14-1", "river", [10, 40], [2, 6])
|
||||
+ self._rows("shipped", "b14-2", "river", [8, 25], [1, 3]))
|
||||
_, v, _ = b14.summarize(counts, [self._sess("b14-1"), self._sess("b14-2", ok=False)])
|
||||
self.assertEqual(v["river"]["verdict"], "incomplete")
|
||||
for r in counts:
|
||||
r["biome_ok"] = 0
|
||||
_, v, _ = b14.summarize(counts, [self._sess("b14-1"), self._sess("b14-2")])
|
||||
self.assertTrue(v["river"]["verdict"].startswith("invalid"))
|
||||
|
||||
def test_all_biomes_keep(self):
|
||||
counts, sess = [], []
|
||||
for i, biome in enumerate(b14.BIOMES):
|
||||
counts += self._rows("defaults", f"b14-{i}0", biome, [5, 30], [1, 2])
|
||||
counts += self._rows("shipped", f"b14-{i}1", biome, [5, 20], [1, 1])
|
||||
sess += [self._sess(f"b14-{i}0"), self._sess(f"b14-{i}1")]
|
||||
self.assertEqual(b14.summarize(counts, sess)[2], "keep")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- nbt / locate
|
||||
|
||||
def _tag(t, name, payload):
|
||||
n = name.encode()
|
||||
return bytes([t]) + struct.pack(">H", len(n)) + n + payload
|
||||
|
||||
|
||||
def _str(s):
|
||||
b = s.encode()
|
||||
return struct.pack(">H", len(b)) + b
|
||||
|
||||
|
||||
def _compound(*items):
|
||||
return b"".join(items) + b"\x00"
|
||||
|
||||
|
||||
def _chunk_nbt(biome, height=70, status="minecraft:full"):
|
||||
hm_longs = []
|
||||
vals = [height + 64] * 256 # stored relative to min_y -64, 9 bits, 7 per long
|
||||
for i in range(0, 256, 7):
|
||||
w = 0
|
||||
for k, v in enumerate(vals[i:i + 7]):
|
||||
w |= v << (9 * k)
|
||||
hm_longs.append(w if w < 2 ** 63 else w - 2 ** 64)
|
||||
hm = struct.pack(">i", len(hm_longs)) + struct.pack(f">{len(hm_longs)}q", *hm_longs)
|
||||
section = _compound(
|
||||
_tag(nbt.BYTE, "Y", struct.pack(">b", (height - 1) >> 4)),
|
||||
_tag(nbt.COMPOUND, "biomes", _compound(
|
||||
_tag(nbt.LIST, "palette", bytes([nbt.STRING]) + struct.pack(">i", 1) + _str(biome)))))
|
||||
root = _compound(
|
||||
_tag(nbt.STRING, "Status", _str(status)),
|
||||
_tag(nbt.LIST, "sections", bytes([nbt.COMPOUND]) + struct.pack(">i", 1) + section),
|
||||
_tag(nbt.COMPOUND, "Heightmaps", _compound(
|
||||
_tag(nbt.LONG_ARRAY, "MOTION_BLOCKING_NO_LEAVES", hm))))
|
||||
return bytes([nbt.COMPOUND]) + _str("") + root
|
||||
|
||||
|
||||
def _region(chunks):
|
||||
"""chunks: {(lx, lz): nbt bytes} -> .mca bytes."""
|
||||
header, body, sector = bytearray(8192), bytearray(), 2
|
||||
for (lx, lz), raw in chunks.items():
|
||||
data = zlib.compress(raw)
|
||||
blob = struct.pack(">I", len(data) + 1) + b"\x02" + data
|
||||
blob += b"\x00" * (-len(blob) % 4096)
|
||||
n = len(blob) // 4096
|
||||
idx = lz * 32 + lx
|
||||
header[idx * 4:idx * 4 + 4] = sector.to_bytes(3, "big") + bytes([n])
|
||||
body += blob
|
||||
sector += n
|
||||
return bytes(header) + bytes(body)
|
||||
|
||||
|
||||
class NbtAndLocate(unittest.TestCase):
|
||||
def test_packed(self):
|
||||
self.assertEqual(nbt.unpack_packed([0b10_01_00 | (3 << 62)], 2, 3), [0, 1, 2])
|
||||
self.assertEqual(nbt.unpack_packed([-1], 4, 2), [15, 15])
|
||||
|
||||
def test_chunk_roundtrip(self):
|
||||
_, c = nbt.loads(_chunk_nbt("minecraft:river", 63))
|
||||
self.assertEqual(nbt.heightmap(c)[0], 63)
|
||||
self.assertEqual(nbt.biome_at(c, 5, 62, 9), "minecraft:river")
|
||||
ld = gzip.compress(bytes([nbt.COMPOUND]) + _str("") + _compound(
|
||||
_tag(nbt.COMPOUND, "Data", _compound(_tag(nbt.INT, "SpawnX", struct.pack(">i", 7))))))
|
||||
self.assertEqual(nbt.load_gzip(ld)[1]["Data"]["SpawnX"], 7)
|
||||
|
||||
def test_scan_and_pick(self):
|
||||
reg = {}
|
||||
for lx in range(5):
|
||||
for lz in range(5):
|
||||
reg[(lx, lz)] = _chunk_nbt("minecraft:river" if lx < 3 else "minecraft:forest", 63)
|
||||
reg[(lx + 20, lz)] = _chunk_nbt("minecraft:warm_ocean", 63)
|
||||
reg[(lx + 10, lz + 20)] = _chunk_nbt("minecraft:jungle", 80,
|
||||
"minecraft:structure_starts")
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
zp = Path(td) / "w.zip"
|
||||
with zipfile.ZipFile(zp, "w") as z:
|
||||
z.writestr("w/level.dat", b"")
|
||||
z.writestr("w/region/r.0.0.mca", _region(reg))
|
||||
chunks = b14.scan_world(zp)
|
||||
self.assertEqual(len(chunks), 50) # structure_starts placeholders skipped
|
||||
sites = b14.pick_sites(chunks, avoid=[(-1000, -1000)])
|
||||
self.assertEqual(sites["river"]["biome"], "minecraft:river")
|
||||
self.assertEqual(sites["river"]["y"], 63)
|
||||
self.assertIn("warm_ocean", sites)
|
||||
self.assertNotIn("jungle", sites)
|
||||
self.assertGreaterEqual(abs(sites["warm_ocean"]["x"] - sites["river"]["x"]), 320)
|
||||
# avoid radius excludes everything near the avoid point
|
||||
self.assertEqual(b14.pick_sites(chunks, avoid=[(40, 40), (360, 40)]), {})
|
||||
|
||||
|
||||
class Busy(unittest.TestCase):
|
||||
def test_ancestors_include_self(self):
|
||||
import os
|
||||
self.assertIn(os.getpid(), b14._ancestors(os.getpid()))
|
||||
self.assertNotIn(os.getpid(), b14.bench_busy())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,204 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Iris per-pack option files that reproduce a shaderpack's built-in profile.
|
||||
|
||||
Iris 1.8.x reads `shaderpacks/<pack name>.txt` (java.util.Properties) when it loads a
|
||||
pack and applies the key=value pairs as option overrides; there is no "selected
|
||||
profile" key - the GUI's profile button just writes the profile's option values, and
|
||||
the profile shown is re-derived by matching values (ProfileSet.scan). So a profile is
|
||||
reproduced by writing its resolved option list. bench.py --shader-options copies the
|
||||
file into place for one session.
|
||||
|
||||
Profile syntax (shaders/shaders.properties, mirrored from Iris ProfileSet.parse):
|
||||
profile.NAME = tok tok ... whitespace-separated, applied in order, later wins
|
||||
profile.OTHER include another profile
|
||||
!program.X disable a program (cannot be expressed in the options file -> error)
|
||||
!opt opt=false
|
||||
opt=val/opt:val opt=val
|
||||
opt opt=true
|
||||
|
||||
python3 gen_shader_profiles.py # (re)write every B7 profile in matrix.toml
|
||||
python3 gen_shader_profiles.py --check # exit 1 if any file is missing/stale
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
import tomllib
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
OUT = HERE / "shader-profiles"
|
||||
PACKS = Path.home() / "onion-bench" / "prism" / "instances" / "OnionBench" / ".minecraft" / "shaderpacks"
|
||||
MATRIX = HERE / "matrix.toml"
|
||||
|
||||
IF_RE = re.compile(r"^\s*#\s*if(n?def)?\b")
|
||||
ENDIF_RE = re.compile(r"^\s*#\s*endif\b")
|
||||
NAME_RE = re.compile(r"^[A-Za-z0-9_.\-]+$")
|
||||
SRC_EXT = (".glsl", ".fsh", ".vsh", ".gsh", ".csh", ".tcs", ".tes", ".inc")
|
||||
DEFINE_RE = re.compile(r"^\s*(?://\s*)?#define\s+(\w+)", re.M)
|
||||
CONST_RE = re.compile(r"\bconst\s+\w+\s+(\w+)\s*=")
|
||||
|
||||
|
||||
class ProfileError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def logical_lines(text):
|
||||
"""Java-properties logical lines: CRLF normalised, trailing-backslash continuation."""
|
||||
out, buf = [], ""
|
||||
for raw in text.replace("\r\n", "\n").replace("\r", "\n").split("\n"):
|
||||
line = raw if not buf else raw.lstrip()
|
||||
if line.endswith("\\") and not line.endswith("\\\\"):
|
||||
buf += line[:-1]
|
||||
continue
|
||||
out.append(buf + line)
|
||||
buf = ""
|
||||
if buf:
|
||||
out.append(buf)
|
||||
return out
|
||||
|
||||
|
||||
def parse_profiles(text):
|
||||
"""{name: [tokens]} for every profile.NAME line. Profiles inside preprocessor
|
||||
conditionals can't be evaluated without the pack's macro environment -> error."""
|
||||
profiles, depth = {}, 0
|
||||
for line in logical_lines(text):
|
||||
s = line.strip()
|
||||
if IF_RE.match(s):
|
||||
depth += 1
|
||||
continue
|
||||
if ENDIF_RE.match(s):
|
||||
depth = max(0, depth - 1)
|
||||
continue
|
||||
if not s or s[0] in "#!":
|
||||
continue
|
||||
m = re.match(r"([^=:\s]+)\s*[=:]\s*(.*)$", s)
|
||||
if not m or not m.group(1).startswith("profile."):
|
||||
continue
|
||||
name = m.group(1)[len("profile."):]
|
||||
if depth:
|
||||
raise ProfileError(f"profile.{name} is inside a preprocessor conditional")
|
||||
profiles[name] = m.group(2).split()
|
||||
return profiles
|
||||
|
||||
|
||||
def resolve(name, profiles, _stack=()):
|
||||
"""(options dict in application order, disabled programs) for one profile."""
|
||||
if name not in profiles:
|
||||
raise ProfileError(f"unknown profile {name!r} (have {sorted(profiles)})")
|
||||
if name in _stack:
|
||||
raise ProfileError("profile cycle: " + " -> ".join((*_stack, name)))
|
||||
opts, programs = {}, []
|
||||
for tok in profiles[name]:
|
||||
if tok.startswith("!program."):
|
||||
programs.append(tok[len("!program."):])
|
||||
elif tok.startswith("profile."):
|
||||
sub, subprog = resolve(tok[len("profile."):], profiles, (*_stack, name))
|
||||
for k, v in sub.items():
|
||||
opts.pop(k, None)
|
||||
opts[k] = v
|
||||
programs += subprog
|
||||
else:
|
||||
if tok.startswith("!"):
|
||||
k, v = tok[1:], "false"
|
||||
elif "=" in tok:
|
||||
k, v = tok.split("=", 1)
|
||||
elif ":" in tok:
|
||||
k, v = tok.split(":", 1)
|
||||
else:
|
||||
k, v = tok, "true"
|
||||
opts.pop(k, None)
|
||||
opts[k] = v
|
||||
return opts, programs
|
||||
|
||||
|
||||
def render(opts, header=()):
|
||||
lines = [f"# {h}" for h in header]
|
||||
for k, v in opts.items():
|
||||
if not NAME_RE.match(k) or not v or any(c.isspace() for c in v) or "\\" in v:
|
||||
raise ProfileError(f"option {k}={v!r} would need properties escaping")
|
||||
lines.append(f"{k}={v}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def pack_sources(zf):
|
||||
props = [n for n in zf.namelist() if n.endswith("shaders/shaders.properties")]
|
||||
if len(props) != 1:
|
||||
raise ProfileError(f"expected one shaders/shaders.properties, found {props}")
|
||||
return zf.read(props[0]).decode("utf-8", "replace")
|
||||
|
||||
|
||||
def known_options(zf):
|
||||
names = set()
|
||||
for n in zf.namelist():
|
||||
if n.endswith(SRC_EXT):
|
||||
t = zf.read(n).decode("utf-8", "replace")
|
||||
names |= set(DEFINE_RE.findall(t)) | set(CONST_RE.findall(t))
|
||||
return names
|
||||
|
||||
|
||||
def out_name(pack, profile):
|
||||
stem = pack[:-4] if pack.endswith(".zip") else pack
|
||||
return f"{stem}.{profile}.txt"
|
||||
|
||||
|
||||
def generate(pack_path, profile):
|
||||
"""(file text, option count, unknown option names) for one pack/profile."""
|
||||
pack_path = Path(pack_path)
|
||||
with zipfile.ZipFile(pack_path) as zf:
|
||||
opts, programs = resolve(profile, parse_profiles(pack_sources(zf)))
|
||||
if programs:
|
||||
raise ProfileError(f"{profile} disables programs {programs}; an options file "
|
||||
"cannot express that")
|
||||
unknown = sorted(set(opts) - known_options(zf))
|
||||
sha = hashlib.sha256(pack_path.read_bytes()).hexdigest()
|
||||
header = [f"Iris options reproducing profile.{profile} of {pack_path.name}",
|
||||
f"pack sha256 {sha}",
|
||||
"generated by gen_shader_profiles.py - do not edit; Iris rewrites the live",
|
||||
"copy keeping only non-default values (bench.py journals and restores it)"]
|
||||
return render(opts, header), len(opts), unknown
|
||||
|
||||
|
||||
def matrix_targets(path=MATRIX):
|
||||
"""(pack, profile) for every session in matrix.toml with a shader_profile."""
|
||||
with open(path, "rb") as f:
|
||||
m = tomllib.load(f)
|
||||
seen = []
|
||||
for b in m.get("block", []):
|
||||
for s in b.get("sessions", []):
|
||||
if s.get("shader_profile"):
|
||||
t = (s["shader"], s["shader_profile"])
|
||||
if t not in seen:
|
||||
seen.append(t)
|
||||
return seen
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("--packs", type=Path, default=PACKS, help="shaderpacks directory")
|
||||
ap.add_argument("--out", type=Path, default=OUT)
|
||||
ap.add_argument("--target", action="append", metavar="PACK:PROFILE",
|
||||
help="instead of the matrix.toml B7 rows")
|
||||
ap.add_argument("--check", action="store_true", help="verify files are current, write nothing")
|
||||
a = ap.parse_args(argv)
|
||||
targets = ([tuple(t.rsplit(":", 1)) for t in a.target] if a.target else matrix_targets())
|
||||
rc = 0
|
||||
for pack, profile in targets:
|
||||
text, n, unknown = generate(a.packs / pack, profile)
|
||||
dst = a.out / out_name(pack, profile)
|
||||
note = f" (not found in pack sources: {', '.join(unknown)})" if unknown else ""
|
||||
if a.check:
|
||||
ok = dst.is_file() and dst.read_text() == text
|
||||
print(f"{'ok ' if ok else 'STALE'} {dst.name}: {n} options{note}")
|
||||
rc |= 0 if ok else 1
|
||||
else:
|
||||
a.out.mkdir(parents=True, exist_ok=True)
|
||||
dst.write_text(text)
|
||||
print(f"wrote {dst.relative_to(HERE) if dst.is_relative_to(HERE) else dst}: "
|
||||
f"{n} options{note}")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,622 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the BENCH-MATRIX blocks declared in matrix.toml through `bench.py session`.
|
||||
|
||||
python3 matrix.py list # blocks, session counts, est. time
|
||||
python3 matrix.py plan B3 [--param k=v] # the bench.py commands a block runs
|
||||
python3 matrix.py run B0,B1 [--dry-run] # sequential, resumable
|
||||
python3 matrix.py status # progress for the current mod set
|
||||
|
||||
Resume: ~/onion-bench/results/matrix-state.json records each session (key
|
||||
"<block>#<index>") with a hash of its exact command and the instance mods/ hash; a
|
||||
session is skipped only when both still match. A failed session (non-zero exit: crash,
|
||||
timeout, no join) is retried once, then the run stops. Running any block other than B0
|
||||
warns loudly when B0 was not run under the current mod set. Stdlib only.
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
MATRIX = HERE / "matrix.toml"
|
||||
OUT_DIR = HERE.parent / "proposed-configs" / "out"
|
||||
GAME = Path.home() / "onion-bench" / "prism" / "instances" / "OnionBench" / ".minecraft"
|
||||
STATE = Path.home() / "onion-bench" / "results" / "matrix-state.json"
|
||||
|
||||
SCENE_ORDER = ["s1", "s2", "s2s", "s3", "s4", "s5"]
|
||||
S5_SECONDS = 10 + 120 # bench.py: at S1, wait 10 s, then the 120 s tour (all captured)
|
||||
JVM_SETS = {"J0", "J1", "J2", "J3"}
|
||||
DISPLAYS = {"desktop", "gamescope", "gamescope-headless"}
|
||||
PARAM_RE = re.compile(r"\$\{(\w+)\}")
|
||||
|
||||
TOP_KEYS = {"defaults", "worlds", "trials", "missing_variants", "block"}
|
||||
DEFAULT_KEYS = {"reps", "warmup", "capture", "settle", "display", "res", "world", "shader", "dh",
|
||||
"launch_overhead_s"}
|
||||
BLOCK_KEYS = {"id", "title", "rule", "proxy_note", "manual_checks", "sessions", "params",
|
||||
"deferred", "manual", "interleave"}
|
||||
SESSION_KEYS = {"variant", "plan", "reps", "repeat", "tag", "jvm", "heap", "shader",
|
||||
"shader_profile", "shader_options", "dh", "disable_mods", "add_mods", "world", "warmup",
|
||||
"capture", "settle", "manual", "note", "display", "res"}
|
||||
MISSING_KEYS = {"id", "blocks", "contents"}
|
||||
# Flags `bench.py session` accepts; bench_argv must emit only these.
|
||||
BENCH_SESSION_FLAGS = {
|
||||
"--variant", "--plan", "--reps", "--warmup", "--capture", "--settle", "--label", "--jvm",
|
||||
"--heap", "--boot-timeout", "--timeout", "--any-order", "--kill-stale", "--dry-run",
|
||||
"--display", "--res", "--shader", "--shader-options", "--dh", "--disable-mods",
|
||||
"--add-mods", "--world-zip",
|
||||
}
|
||||
|
||||
|
||||
class MatrixError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def log(msg):
|
||||
print(f"[matrix] {msg}", flush=True)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ loading / validation
|
||||
|
||||
def load(path=MATRIX, out_dir=OUT_DIR):
|
||||
"""Parse + validate matrix.toml. Raises MatrixError listing every problem."""
|
||||
with open(path, "rb") as f:
|
||||
m = tomllib.load(f)
|
||||
errors = validate(m, out_dir)
|
||||
if errors:
|
||||
raise MatrixError("matrix.toml invalid:\n " + "\n ".join(errors))
|
||||
return m
|
||||
|
||||
|
||||
def split_plan(plan):
|
||||
if isinstance(plan, str):
|
||||
return [s.strip() for s in plan.split(",") if s.strip()]
|
||||
return list(plan)
|
||||
|
||||
|
||||
def _pos_int(v):
|
||||
return isinstance(v, int) and not isinstance(v, bool) and v > 0
|
||||
|
||||
|
||||
def validate(m, out_dir=OUT_DIR):
|
||||
err = []
|
||||
for k in m:
|
||||
if k not in TOP_KEYS:
|
||||
err.append(f"unknown top-level key {k!r}")
|
||||
d = m.get("defaults", {})
|
||||
for k in d:
|
||||
if k not in DEFAULT_KEYS:
|
||||
err.append(f"defaults: unknown key {k!r}")
|
||||
worlds, trials = m.get("worlds", {}), m.get("trials", {})
|
||||
missing_ids = set()
|
||||
for mv in m.get("missing_variants", []):
|
||||
for k in mv:
|
||||
if k not in MISSING_KEYS:
|
||||
err.append(f"missing_variants {mv.get('id')}: unknown key {k!r}")
|
||||
if not mv.get("id") or not mv.get("contents"):
|
||||
err.append("missing_variants entry needs id and contents")
|
||||
missing_ids.add(mv.get("id"))
|
||||
_check_common(d, "defaults", worlds, trials, err)
|
||||
|
||||
seen = set()
|
||||
for b in m.get("block", []):
|
||||
bid = b.get("id")
|
||||
where = f"block {bid}"
|
||||
if not bid:
|
||||
err.append("block without id")
|
||||
continue
|
||||
if bid in seen:
|
||||
err.append(f"{where}: duplicate id")
|
||||
seen.add(bid)
|
||||
for k in b:
|
||||
if k not in BLOCK_KEYS:
|
||||
err.append(f"{where}: unknown key {k!r}")
|
||||
if not b.get("rule"):
|
||||
err.append(f"{where}: missing decision rule")
|
||||
params = set(b.get("params", []))
|
||||
sessions = b.get("sessions", [])
|
||||
if not sessions and not (b.get("manual") or b.get("deferred")):
|
||||
err.append(f"{where}: no sessions (mark manual or deferred)")
|
||||
groups = set()
|
||||
for i, s in enumerate(sessions):
|
||||
sw = f"{where} session {i}"
|
||||
for k in s:
|
||||
if k not in SESSION_KEYS:
|
||||
err.append(f"{sw}: unknown key {k!r}")
|
||||
for k in ("variant", "jvm", "plan"):
|
||||
v = s.get(k)
|
||||
for p in PARAM_RE.findall(v if isinstance(v, str) else ",".join(v or [])):
|
||||
if p not in params:
|
||||
err.append(f"{sw}: ${{{p}}} not declared in params")
|
||||
var = s.get("variant")
|
||||
if not var:
|
||||
err.append(f"{sw}: missing variant")
|
||||
elif not PARAM_RE.search(var) and not (Path(out_dir) / var).is_dir() \
|
||||
and var not in missing_ids:
|
||||
err.append(f"{sw}: variant {var!r} not in {out_dir} nor in missing_variants")
|
||||
plan = split_plan(s.get("plan", []))
|
||||
if not plan:
|
||||
err.append(f"{sw}: empty plan")
|
||||
concrete = [p for p in plan if not PARAM_RE.search(p)]
|
||||
for p in concrete:
|
||||
if p not in SCENE_ORDER:
|
||||
err.append(f"{sw}: unknown scene {p!r} (S6 is manual-only)")
|
||||
idx = [SCENE_ORDER.index(p) for p in concrete if p in SCENE_ORDER]
|
||||
if idx != sorted(set(idx)):
|
||||
err.append(f"{sw}: plan must be in S1->S5 order without duplicates")
|
||||
if "repeat" in s and not _pos_int(s["repeat"]):
|
||||
err.append(f"{sw}: repeat must be a positive int")
|
||||
if "heap" in s and not _pos_int(s["heap"]):
|
||||
err.append(f"{sw}: heap must be a positive int (MB)")
|
||||
jvm = s.get("jvm")
|
||||
if jvm is not None and jvm not in JVM_SETS and not PARAM_RE.search(jvm):
|
||||
err.append(f"{sw}: jvm {jvm!r} not in {sorted(JVM_SETS)}")
|
||||
for t in s.get("add_mods", []):
|
||||
if t not in trials:
|
||||
err.append(f"{sw}: add_mods {t!r} not in [trials]")
|
||||
if not isinstance(s.get("disable_mods", []), list):
|
||||
err.append(f"{sw}: disable_mods must be a list")
|
||||
_check_common(s, sw, worlds, trials, err)
|
||||
g = (var, s.get("tag"))
|
||||
if g in groups:
|
||||
err.append(f"{sw}: variant {var!r} repeated without a distinct tag "
|
||||
f"(analyze.py would merge them); use repeat or tag")
|
||||
groups.add(g)
|
||||
return err
|
||||
|
||||
|
||||
def _check_common(s, where, worlds, trials, err):
|
||||
for k in ("reps", "warmup", "capture", "settle"):
|
||||
if k in s and not _pos_int(s[k]):
|
||||
err.append(f"{where}: {k} must be a positive int")
|
||||
if "display" in s and s["display"] not in DISPLAYS:
|
||||
err.append(f"{where}: display {s['display']!r} not in {sorted(DISPLAYS)}")
|
||||
if "res" in s and not re.fullmatch(r"\d+x\d+", str(s["res"])):
|
||||
err.append(f"{where}: res must be WxH")
|
||||
if "dh" in s and s["dh"] not in ("on", "off"):
|
||||
err.append(f"{where}: dh must be on/off")
|
||||
if "world" in s and s["world"] not in worlds:
|
||||
err.append(f"{where}: world {s['world']!r} not in [worlds]")
|
||||
if "shader_options" in s and s.get("shader", "off") == "off":
|
||||
err.append(f"{where}: shader_options needs shader = <pack>")
|
||||
|
||||
|
||||
def block_map(m):
|
||||
return {b["id"]: b for b in m.get("block", [])}
|
||||
|
||||
|
||||
def missing_variant_ids(m):
|
||||
return {mv["id"] for mv in m.get("missing_variants", [])}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ expansion / commands
|
||||
|
||||
def _subst(v, params):
|
||||
if not isinstance(v, str):
|
||||
return v
|
||||
return PARAM_RE.sub(lambda mo: params.get(mo.group(1), mo.group(0)), v)
|
||||
|
||||
|
||||
def expand(m, block, params=None, overrides=None):
|
||||
"""Flat list of concrete sessions for a block (repeat / interleave applied)."""
|
||||
params, overrides = params or {}, overrides or {}
|
||||
d = dict(m.get("defaults", {}))
|
||||
d.update({k: v for k, v in overrides.items() if v is not None})
|
||||
groups = []
|
||||
for si, s in enumerate(block.get("sessions", [])):
|
||||
merged = {k: v for k, v in d.items() if k != "launch_overhead_s"}
|
||||
merged.update(s)
|
||||
for k in ("display", "res"): # CLI overrides beat per-session values too
|
||||
if overrides.get(k) is not None:
|
||||
merged[k] = overrides[k]
|
||||
merged["variant"] = _subst(merged["variant"], params)
|
||||
if merged.get("jvm") is not None:
|
||||
merged["jvm"] = _subst(merged["jvm"], params)
|
||||
merged["plan"] = [_subst(p, params) for p in split_plan(merged["plan"])]
|
||||
tag = s.get("tag")
|
||||
merged["label"] = f"{block['id']}.{tag}" if tag else block["id"]
|
||||
merged["source"] = si
|
||||
groups.append([dict(merged, run=r + 1) for r in range(s.get("repeat", 1))])
|
||||
out = []
|
||||
if block.get("interleave"):
|
||||
for r in range(max((len(g) for g in groups), default=0)):
|
||||
out += [g[r] for g in groups if r < len(g)]
|
||||
else:
|
||||
for g in groups:
|
||||
out += g
|
||||
for i, s in enumerate(out):
|
||||
s["index"] = i
|
||||
s["block"] = block["id"]
|
||||
return out
|
||||
|
||||
|
||||
def unresolved(sess):
|
||||
"""Placeholder names still present in a session (missing --param)."""
|
||||
vals = [sess["variant"], sess.get("jvm") or "", *sess["plan"]]
|
||||
return sorted({p for v in vals for p in PARAM_RE.findall(v)})
|
||||
|
||||
|
||||
def resolve_path(p):
|
||||
p = os.path.expanduser(p)
|
||||
hits = sorted(glob.glob(p))
|
||||
return hits[-1] if hits else None
|
||||
|
||||
|
||||
def shader_options_path(p):
|
||||
"""shader_options are harness-relative unless absolute / ~."""
|
||||
p = Path(os.path.expanduser(p))
|
||||
return p if p.is_absolute() else HERE / p
|
||||
|
||||
|
||||
def bench_argv(m, sess, live_world=False):
|
||||
"""`bench.py session` argv for one session, plus a list of missing inputs."""
|
||||
missing = []
|
||||
a = ["session", "--variant", sess["variant"], "--plan", ",".join(sess["plan"]),
|
||||
"--reps", str(sess.get("reps", 3)), "--label", sess["label"]]
|
||||
for k in ("warmup", "capture", "settle"):
|
||||
if k in sess:
|
||||
a += [f"--{k}", str(sess[k])]
|
||||
for k in ("display", "res", "shader", "dh", "jvm"):
|
||||
if sess.get(k) is not None:
|
||||
a += [f"--{k}", str(sess[k])]
|
||||
if sess.get("heap"):
|
||||
a += ["--heap", str(sess["heap"])]
|
||||
if sess.get("shader_options"):
|
||||
so = shader_options_path(sess["shader_options"])
|
||||
if not so.is_file():
|
||||
missing.append(f"shader options {so} (run gen_shader_profiles.py)")
|
||||
a += ["--shader-options", str(so)]
|
||||
if sess.get("disable_mods"):
|
||||
a += ["--disable-mods", ",".join(sess["disable_mods"])]
|
||||
if sess.get("add_mods"):
|
||||
jars = []
|
||||
for t in sess["add_mods"]:
|
||||
pat = m["trials"][t]
|
||||
hit = resolve_path(pat)
|
||||
if hit is None:
|
||||
missing.append(f"trial jar {t}: {pat}")
|
||||
hit = os.path.expanduser(pat)
|
||||
jars.append(hit)
|
||||
a += ["--add-mods", ",".join(jars)]
|
||||
if sess.get("world") and not live_world:
|
||||
pat = m["worlds"][sess["world"]]
|
||||
hit = resolve_path(pat)
|
||||
if hit is None:
|
||||
missing.append(f"world {sess['world']}: {pat} (or run with --live-world)")
|
||||
hit = os.path.expanduser(pat)
|
||||
a += ["--world-zip", hit]
|
||||
return a, missing
|
||||
|
||||
|
||||
def variant_missing(sess, out_dir=OUT_DIR):
|
||||
return not (Path(out_dir) / sess["variant"]).is_dir()
|
||||
|
||||
|
||||
def session_seconds(sess, overhead):
|
||||
reps, w, c = sess.get("reps", 3), sess.get("warmup", 15), sess.get("capture", 30)
|
||||
scenes = sum(S5_SECONDS if p == "s5" else w + c for p in sess["plan"]) * reps
|
||||
return overhead + sess.get("settle", 10) + scenes
|
||||
|
||||
|
||||
def spec_hash(argv, sess):
|
||||
spec = {"argv": argv, "manual": bool(sess.get("manual"))}
|
||||
if sess.get("shader_options"): # a regenerated profile file means a new spec
|
||||
so = shader_options_path(sess["shader_options"])
|
||||
spec["shader_options_sha256"] = (hashlib.sha256(so.read_bytes()).hexdigest()
|
||||
if so.is_file() else None)
|
||||
blob = json.dumps(spec, sort_keys=True)
|
||||
return hashlib.sha256(blob.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def fmt_cmd(argv):
|
||||
return "python3 bench.py " + shlex.join(argv)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ state
|
||||
|
||||
def mods_hash(mods_dir):
|
||||
"""Same algorithm as bench.py mods_hash (name + size of every file in mods/)."""
|
||||
h = hashlib.sha256()
|
||||
files = sorted(p for p in Path(mods_dir).glob("*") if p.is_file())
|
||||
for p in files:
|
||||
h.update(f"{p.name}\0{p.stat().st_size}\n".encode())
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
class State:
|
||||
def __init__(self, path):
|
||||
self.path = Path(path)
|
||||
self.data = {"version": 1, "sessions": {}, "b0": None}
|
||||
if self.path.exists():
|
||||
self.data.update(json.loads(self.path.read_text()))
|
||||
|
||||
def save(self):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = self.path.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(self.data, indent=1, sort_keys=True))
|
||||
tmp.replace(self.path)
|
||||
|
||||
@staticmethod
|
||||
def key(sess):
|
||||
return f"{sess['block']}#{sess['index']}"
|
||||
|
||||
def entry(self, sess):
|
||||
return self.data["sessions"].get(self.key(sess))
|
||||
|
||||
def is_done(self, sess, spec, mods):
|
||||
e = self.entry(sess)
|
||||
return bool(e and e["status"] == "ok" and e["spec"] == spec and e["mods_hash"] == mods)
|
||||
|
||||
def record(self, sess, spec, mods, status, rc, attempts):
|
||||
self.data["sessions"][self.key(sess)] = {
|
||||
"spec": spec, "mods_hash": mods, "status": status, "rc": rc,
|
||||
"attempts": attempts, "label": sess["label"], "variant": sess["variant"],
|
||||
"ts": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
||||
|
||||
|
||||
def b0_warning(state, current):
|
||||
b0 = state.data.get("b0")
|
||||
if not b0:
|
||||
return "B0 (noise floor) has not been completed - win thresholds are undefined"
|
||||
if b0["mods_hash"] != current:
|
||||
return (f"MOD SET CHANGED since B0 (B0 under {b0['mods_hash'][:12]}, now "
|
||||
f"{current[:12]}): B0 MUST BE RE-RUN before results are comparable")
|
||||
return None
|
||||
|
||||
|
||||
def banner(msg):
|
||||
bar = "!" * 78
|
||||
print(f"{bar}\n!! {msg}\n{bar}", file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ run
|
||||
|
||||
def default_runner(argv):
|
||||
return subprocess.run([sys.executable, str(HERE / "bench.py"), *argv], cwd=HERE).returncode
|
||||
|
||||
|
||||
def run_blocks(m, block_ids, state, mods_dir, runner=default_runner, params=None,
|
||||
overrides=None, dry_run=False, include_manual=False, include_deferred=False,
|
||||
live_world=False, skip_blocked=False, out_dir=OUT_DIR):
|
||||
"""Run blocks in order. Returns 0 ok, 1 session failed, 2 preflight problem."""
|
||||
bm = block_map(m)
|
||||
for bid in block_ids:
|
||||
if bid not in bm:
|
||||
raise MatrixError(f"unknown block {bid}; known: {', '.join(bm)}")
|
||||
# preflight: build every session's command, collect what can't run
|
||||
todo, problems = [], []
|
||||
for bid in block_ids:
|
||||
b = bm[bid]
|
||||
if b.get("deferred") and not include_deferred:
|
||||
log(f"{bid}: deferred ({b.get('proxy_note', 'fleet-only')}) - skipped")
|
||||
continue
|
||||
if b.get("manual"):
|
||||
log(f"{bid}: manual block - run by hand ({b.get('proxy_note', '')})")
|
||||
continue
|
||||
for s in expand(m, b, params, overrides):
|
||||
if s.get("manual") and not include_manual:
|
||||
log(f"{bid}#{s['index']} {s['label']}: manual - skipped")
|
||||
continue
|
||||
miss = [f"param ${{{p}}} (use --param {p}=...)" for p in unresolved(s)]
|
||||
if not miss and variant_missing(s, out_dir):
|
||||
miss.append(f"variant {s['variant']} not generated (see missing_variants)")
|
||||
argv, inputs = bench_argv(m, s, live_world)
|
||||
miss += inputs
|
||||
if miss:
|
||||
problems.append((s, miss))
|
||||
continue
|
||||
todo.append((s, argv))
|
||||
for s, miss in problems:
|
||||
log(f"BLOCKED {s['block']}#{s['index']} {s['label']}: " + "; ".join(miss))
|
||||
if problems and not (skip_blocked or dry_run):
|
||||
log("fix the above, or pass --skip-blocked (and/or --live-world)")
|
||||
return 2
|
||||
mods0 = mods_hash(mods_dir)
|
||||
if "B0" not in block_ids and any(s["block"] != "B0" for s, _ in todo):
|
||||
w = b0_warning(state, mods0)
|
||||
if w:
|
||||
banner(w)
|
||||
total = len(todo)
|
||||
for n, (s, argv) in enumerate(todo, 1):
|
||||
spec = spec_hash(argv, s)
|
||||
tagline = f"[{n}/{total}] {s['block']}#{s['index']} {s['label']}"
|
||||
if state.is_done(s, spec, mods0):
|
||||
log(f"{tagline}: done - skip")
|
||||
continue
|
||||
if dry_run:
|
||||
log(f"{tagline}: {fmt_cmd(argv)}")
|
||||
continue
|
||||
now = mods_hash(mods_dir)
|
||||
if now != mods0:
|
||||
banner(f"instance mods/ changed during the run ({mods0[:12]} -> {now[:12]}); "
|
||||
"a session did not restore it - stopping")
|
||||
return 1
|
||||
rc = None
|
||||
for attempt in (1, 2):
|
||||
log(f"{tagline}: attempt {attempt}: {fmt_cmd(argv)}")
|
||||
rc = runner(argv)
|
||||
if rc == 0:
|
||||
break
|
||||
log(f"{tagline}: exit {rc}" + (" - retrying once" if attempt == 1 else ""))
|
||||
status = "ok" if rc == 0 else "failed"
|
||||
state.record(s, spec, mods0, status, rc, attempt)
|
||||
if s["block"] == "B0":
|
||||
_maybe_close_b0(m, state, mods0, params, overrides, live_world)
|
||||
state.save()
|
||||
if status != "ok":
|
||||
log(f"{tagline}: failed twice - stopping the run")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def _maybe_close_b0(m, state, mods, params, overrides, live_world):
|
||||
b = block_map(m).get("B0")
|
||||
if not b:
|
||||
return
|
||||
for s in expand(m, b, params, overrides):
|
||||
argv, _ = bench_argv(m, s, live_world)
|
||||
if not state.is_done(s, spec_hash(argv, s), mods):
|
||||
return
|
||||
state.data["b0"] = {"mods_hash": mods, "ts": time.strftime("%Y-%m-%dT%H:%M:%S")}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ list / plan / status
|
||||
|
||||
def block_summary(m, b, overhead, out_dir=OUT_DIR):
|
||||
ss = expand(m, b)
|
||||
auto = [s for s in ss if not s.get("manual") and not b.get("manual")]
|
||||
blocked = [s for s in auto if not unresolved(s) and variant_missing(s, out_dir)]
|
||||
secs = sum(session_seconds(s, overhead) for s in auto)
|
||||
return {"sessions": len(ss), "auto": len(auto), "manual": len(ss) - len(auto),
|
||||
"blocked": len(blocked), "seconds": secs}
|
||||
|
||||
|
||||
def cmd_list(m, a):
|
||||
overhead = m.get("defaults", {}).get("launch_overhead_s", 60)
|
||||
print(f"| block | title | sessions | auto | manual | blocked (variant) | est. min | notes |")
|
||||
print("| --- | --- | --- | --- | --- | --- | --- | --- |")
|
||||
tot = {"sessions": 0, "auto": 0, "seconds": 0}
|
||||
for b in m.get("block", []):
|
||||
s = block_summary(m, b, overhead)
|
||||
notes = []
|
||||
if b.get("deferred"):
|
||||
notes.append("deferred")
|
||||
if b.get("manual"):
|
||||
notes.append("manual block")
|
||||
if b.get("params"):
|
||||
notes.append("params: " + ",".join(b["params"]))
|
||||
print(f"| {b['id']} | {b.get('title', '')} | {s['sessions']} | {s['auto']} | "
|
||||
f"{s['manual']} | {s['blocked']} | {s['seconds'] / 60:.0f} | {'; '.join(notes)} |")
|
||||
if not b.get("deferred"):
|
||||
for k in tot:
|
||||
tot[k] += s[k]
|
||||
print(f"\nnot deferred: {tot['sessions']} sessions ({tot['auto']} unattended), "
|
||||
f"est. {tot['seconds'] / 3600:.1f} h unattended")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_plan(m, a):
|
||||
bm = block_map(m)
|
||||
for bid in a.blocks.split(","):
|
||||
if bid not in bm:
|
||||
raise MatrixError(f"unknown block {bid}")
|
||||
b = bm[bid]
|
||||
print(f"## {bid} {b.get('title', '')}" + (" (DEFERRED)" if b.get("deferred") else "")
|
||||
+ (" (MANUAL)" if b.get("manual") else ""))
|
||||
print(f"rule: {b['rule']}")
|
||||
if b.get("proxy_note"):
|
||||
print(f"proxy: {b['proxy_note']}")
|
||||
for c in b.get("manual_checks", []):
|
||||
print(f"manual check: {c}")
|
||||
for s in expand(m, b, a.params, overrides(a)):
|
||||
argv, miss = bench_argv(m, s, a.live_world)
|
||||
pre = "# MANUAL: " if s.get("manual") else ""
|
||||
print(f"{pre}{fmt_cmd(argv)}")
|
||||
if s.get("shader_profile") and not s.get("shader_options"):
|
||||
print(f"# set Iris profile {s['shader_profile']} for {s['shader']} first")
|
||||
elif s.get("shader_profile"):
|
||||
print(f"# Iris profile {s['shader_profile']} via shader options file")
|
||||
if s.get("note"):
|
||||
print(f"# note: {s['note']}")
|
||||
if unresolved(s):
|
||||
print(f"# needs --param {','.join(unresolved(s))}")
|
||||
if variant_missing(s) and not unresolved(s):
|
||||
print(f"# BLOCKED: variant {s['variant']} not generated")
|
||||
for x in miss:
|
||||
print(f"# missing: {x}")
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_status(m, a):
|
||||
state = State(a.state)
|
||||
mods = mods_hash(a.mods_dir) if Path(a.mods_dir).is_dir() else ""
|
||||
w = b0_warning(state, mods)
|
||||
if w:
|
||||
banner(w)
|
||||
print(f"mods hash {mods[:12]} | state {a.state}")
|
||||
print("| block | done | failed | pending | manual/deferred |")
|
||||
print("| --- | --- | --- | --- | --- |")
|
||||
for b in m.get("block", []):
|
||||
done = failed = pending = skipped = 0
|
||||
for s in expand(m, b, a.params, overrides(a)):
|
||||
if s.get("manual") or b.get("manual") or b.get("deferred"):
|
||||
skipped += 1
|
||||
continue
|
||||
argv, _ = bench_argv(m, s, a.live_world)
|
||||
e = state.entry(s)
|
||||
if state.is_done(s, spec_hash(argv, s), mods):
|
||||
done += 1
|
||||
elif e and e["status"] == "failed":
|
||||
failed += 1
|
||||
else:
|
||||
pending += 1
|
||||
print(f"| {b['id']} | {done} | {failed} | {pending} | {skipped} |")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_run(m, a):
|
||||
state = State(a.state)
|
||||
return run_blocks(m, a.blocks.split(","), state, a.mods_dir, params=a.params,
|
||||
overrides=overrides(a), dry_run=a.dry_run,
|
||||
include_manual=a.include_manual, include_deferred=a.include_deferred,
|
||||
live_world=a.live_world, skip_blocked=a.skip_blocked)
|
||||
|
||||
|
||||
def overrides(a):
|
||||
return {"display": getattr(a, "display", None), "res": getattr(a, "res", None)}
|
||||
|
||||
|
||||
def parse_params(items):
|
||||
out = {}
|
||||
for it in items or []:
|
||||
k, sep, v = it.partition("=")
|
||||
if not sep:
|
||||
raise MatrixError(f"--param needs name=value, got {it!r}")
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
||||
ap.add_argument("--matrix", default=str(MATRIX))
|
||||
ap.add_argument("--state", default=str(STATE))
|
||||
ap.add_argument("--mods-dir", default=str(GAME / "mods"))
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
sub.add_parser("list", help="blocks, session counts, estimated time")
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--param", action="append", default=[], help="name=value")
|
||||
common.add_argument("--display", choices=sorted(DISPLAYS))
|
||||
common.add_argument("--res")
|
||||
common.add_argument("--live-world", action="store_true",
|
||||
help="don't restore world zips; use saves/onionbench as-is")
|
||||
p = sub.add_parser("plan", parents=[common], help="print the bench.py commands")
|
||||
p.add_argument("blocks")
|
||||
r = sub.add_parser("run", parents=[common], help="run blocks sequentially (resumable)")
|
||||
r.add_argument("blocks", help="comma list, e.g. B0,B1")
|
||||
r.add_argument("--dry-run", action="store_true")
|
||||
r.add_argument("--include-manual", action="store_true")
|
||||
r.add_argument("--include-deferred", action="store_true")
|
||||
r.add_argument("--skip-blocked", action="store_true",
|
||||
help="skip sessions with missing variants/inputs instead of aborting")
|
||||
sub.add_parser("status", parents=[common], help="progress for the current mod set")
|
||||
a = ap.parse_args(argv)
|
||||
try:
|
||||
a.params = parse_params(getattr(a, "param", []))
|
||||
m = load(a.matrix)
|
||||
return {"list": cmd_list, "plan": cmd_plan, "run": cmd_run,
|
||||
"status": cmd_status}[a.cmd](m, a)
|
||||
except MatrixError as e:
|
||||
print(f"matrix.py: {e}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,298 @@
|
||||
# Declarative encoding of BENCH-MATRIX.md §4 (blocks B0-B14) for the proxy bench machine
|
||||
# (Ryzen 9 9900X / RX 7800 XT). Driven by matrix.py; every session becomes one
|
||||
# `python3 bench.py session ...` launch. BENCH-MATRIX.md is authoritative - if this file
|
||||
# disagrees with it, fix this file.
|
||||
#
|
||||
# Session keys:
|
||||
# variant proposed-configs/out/<ID> (or an entry in [[missing_variants]] -> blocked)
|
||||
# plan scenes in S1->S5 order: s1,s2,s2s,s3,s4,s5 (S6 is not automatable)
|
||||
# reps windows per scene inside the session (phase0: 3 runs)
|
||||
# repeat separate sessions (= cold launches) of the same config (B0: 3)
|
||||
# tag distinguishes sessions that share a variant but differ by flags; the
|
||||
# bench label becomes "<block>.<tag>" so analyze.py groups them apart
|
||||
# jvm/heap BENCH-MATRIX JVM set / MaxMemAlloc MB
|
||||
# shader shaderpack file name or "off"; shader_profile = the pack's profile name
|
||||
# shader_options Iris per-pack options file (harness-relative), copied to
|
||||
# shaderpacks/<pack>.txt; gen_shader_profiles.py builds them from the zips
|
||||
# dh "on" | "off" (dh-off row of B3)
|
||||
# disable_mods globs matched in the instance mods/ (B12a)
|
||||
# add_mods jar paths/globs added for the session only (B12b)
|
||||
# world key of [worlds]; restored from the zip before the session
|
||||
# warmup/capture per-window seconds (bench.py defaults 15 / 30; S5 is fixed 10 + 120)
|
||||
# manual true = cannot run unattended; `run` skips it unless --include-manual
|
||||
# note free text shown by `plan`
|
||||
# ${name} in variant/jvm/plan is a block parameter, supplied with `--param name=value`.
|
||||
|
||||
[defaults]
|
||||
reps = 3
|
||||
warmup = 15
|
||||
capture = 30
|
||||
settle = 45 # post-join settle; B0 v1 showed rep 1 still warming at settle 10
|
||||
display = "gamescope-headless"
|
||||
res = "1920x1080"
|
||||
world = "v1"
|
||||
shader = "off" # pin: a shader/DH state must never leak from one session to the next
|
||||
dh = "on"
|
||||
launch_overhead_s = 60 # launch->join (~31 s measured) + world-zip restore + teardown
|
||||
|
||||
[worlds]
|
||||
# BENCH-MATRIX §2. Built 2026-10-16 (sha256 in .pi/journal.md). `run --live-world`
|
||||
# uses the instance's saves/onionbench as-is (no restore).
|
||||
v1 = "~/onion-bench/worlds/bench-world-v1.zip"
|
||||
s4-0 = "~/onion-bench/worlds/bench-world-v1-s4-0.zip"
|
||||
s4-1 = "~/onion-bench/worlds/bench-world-v1-s4-1.zip"
|
||||
s4-4 = "~/onion-bench/worlds/bench-world-v1.zip" # the main zip carries all 4 ships
|
||||
|
||||
[trials]
|
||||
# B12b trial jars (NeoForge 1.21.1, Modrinth, sha512-verified 2026-10-16; see /tmp/pi-trials.md
|
||||
# or the journal). Each glob matches exactly one file in ~/onion-bench/trials/.
|
||||
gnetum = "~/onion-bench/trials/gnetum-3.3.6.jar"
|
||||
better_block_entities = "~/onion-bench/trials/bbe-neoforge-1.3.4+mc1.21.1.jar"
|
||||
flerovium = "~/onion-bench/trials/flerovium-neoforge-1.21.1-1.1.2-all.jar"
|
||||
super_resolution = "~/onion-bench/trials/super_resolution-neoforge-*-0.9.2-alpha.2+opengl.jar"
|
||||
ixeris = "~/onion-bench/trials/Ixeris-4.6.8+1.21.1-neoforge.jar"
|
||||
iris_flywheel_compat = "~/onion-bench/trials/iris-flywheel-compat-NeoForge-2.4.0.jar"
|
||||
cull_leaves = "~/onion-bench/trials/cullleaves-neoforge-4.1.1+1.21.1.jar"
|
||||
|
||||
# Variants a block needs that build.py does not generate. Sessions using them are
|
||||
# reported as blocked until they exist in proposed-configs/out/. (C1-fa, C1-fa-ext,
|
||||
# emf-lod20-fa, emf-lod8-fa and C1-bl are now generated by build.py; every bench
|
||||
# variant pins options.txt resourcePacks.)
|
||||
[[missing_variants]]
|
||||
id = "dh-off"
|
||||
blocks = []
|
||||
contents = """Not needed as a directory: BENCH-MATRIX defines dh-off as C1 with DH rendering \
|
||||
toggled off in the GUI. Encoded as variant C1 + `dh = "off"` (bench.py --dh off)."""
|
||||
|
||||
# ---------------------------------------------------------------------------------------
|
||||
|
||||
[[block]]
|
||||
id = "B0"
|
||||
title = "noise floor"
|
||||
rule = "Defines each machine's threshold. P2 also checks `MemAvailable` drift (phase0 Step 4)."
|
||||
proxy_note = "Single proxy machine. Re-run whenever the mod set changes (matrix.py warns)."
|
||||
manual_checks = ["MemAvailable drift: compare mem_available_mb before/after across the 3 sessions (recorded per row)"]
|
||||
sessions = [
|
||||
{ variant = "C0", plan = "s1", repeat = 3 },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B1"
|
||||
title = "baseline"
|
||||
rule = "Reference numbers for everything below"
|
||||
manual_checks = [
|
||||
"S6 (GUI/JEI overlay) needs a key press - run by hand",
|
||||
"/sparkc heapsummary after S1 loads",
|
||||
"VRAM peak",
|
||||
]
|
||||
sessions = [
|
||||
{ variant = "C0", plan = "s1,s2,s3,s4,s5" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B2"
|
||||
title = "composite"
|
||||
rule = "If C1 isn't a win on ≥ 2 machines, stop and bisect with B3–B11 before shipping anything"
|
||||
proxy_note = "Proxy runs all three composites; one machine = single-machine evidence, fleet confirmation pending. Baseline is C0 (pooled from B1)."
|
||||
manual_checks = ["S6 by hand", "/sparkc heapsummary after S1 loads", "VRAM peak"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s1,s2,s3,s4,s5" },
|
||||
{ variant = "C1-deck", plan = "s1,s2,s3,s4,s5" },
|
||||
{ variant = "C1-high", plan = "s1,s2,s3,s4,s5" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B3"
|
||||
title = "Distant Horizons"
|
||||
rule = """**Radius per tier** = the largest radius meeting the FPS floor on S1 *and* S5: P0 → deck, P1 → default, P2 → high. \
|
||||
| Adopt SSAO-off / LOW-quality for that tier on a win. \
|
||||
| Keep 2 threads unless 4 is a win on S5 *and* not a loss on S1"""
|
||||
proxy_note = "Absolute floors are fleet values; on the proxy use relative wins only. P0/P1-only rows (dh-ssao, dh-qlow) run here too."
|
||||
manual_checks = ["DH memory in F3"]
|
||||
sessions = [
|
||||
{ variant = "dh-r256", plan = "s1,s5" },
|
||||
{ variant = "dh-r192", plan = "s1,s5" },
|
||||
{ variant = "C1", plan = "s1,s5" },
|
||||
{ variant = "dh-r64", plan = "s1,s5" },
|
||||
{ variant = "C1", plan = "s1,s5", dh = "off", tag = "dh-off" },
|
||||
{ variant = "dh-ssao", plan = "s1" },
|
||||
{ variant = "dh-qlow", plan = "s1" },
|
||||
{ variant = "dh-thr4", plan = "s1,s5" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B4"
|
||||
title = "heap & GC: GC"
|
||||
rule = "GC = best S5 1 % low. Heap = the smallest size within the spread of the best. Feeds `PLAYER-TUNING.md` §2. P1 isn't run (32 GB, not a constraint)."
|
||||
proxy_note = "P2 values (-Xmx6G) on the proxy; the heap ladder (B4h) adds 8 G as the top rung."
|
||||
manual_checks = ["/sparkc gc after S5 (pause count / max / total)", "swap delta"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "J0", heap = 6144, tag = "J0" },
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "J1", heap = 6144, tag = "J1" },
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "J2", heap = 6144, tag = "J2" },
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "J3", heap = 6144, tag = "J3" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B4h"
|
||||
title = "heap & GC: heap ladder for the GC winner"
|
||||
rule = "GC = best S5 1 % low. Heap = the smallest size within the spread of the best. Feeds `PLAYER-TUNING.md` §2. P1 isn't run (32 GB, not a constraint)."
|
||||
params = ["gc"]
|
||||
proxy_note = "Run after B4 with --param gc=<winning J set>."
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 4096, tag = "heap4" },
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 5120, tag = "heap5" },
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 6144, tag = "heap6" },
|
||||
{ variant = "C1", plan = "s2,s5", jvm = "${gc}", heap = 8192, tag = "heap8" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B5"
|
||||
title = "Sable / SSRD"
|
||||
deferred = true
|
||||
rule = """SSRD per tier = the largest value meeting the floor with 4 ships. sable-noshade ships only on a win *and* if it looks acceptable. \
|
||||
| Decay > spread is a Sable #1011-class finding: report it, no config fix"""
|
||||
proxy_note = "Fleet-only (P0/P1) per BENCH-MATRIX; deferred on the proxy. Needs the S4 world zips. The 45-min decay run exceeds mangohud-local log_duration=1800 and needs FPS at 0/15/30/45 min + heapsummary by hand."
|
||||
manual_checks = ["sub-level count", "sable-noshade visual acceptability", "/sparkc heapsummary at 0/15/30/45 min"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s4", world = "s4-0", tag = "ships0" },
|
||||
{ variant = "C1", plan = "s4", world = "s4-1", tag = "ships1" },
|
||||
{ variant = "C1", plan = "s4", world = "s4-4", tag = "ships4" },
|
||||
{ variant = "ssrd-32", plan = "s4", world = "s4-4" },
|
||||
{ variant = "ssrd-64", plan = "s4", world = "s4-4" },
|
||||
{ variant = "ssrd-128", plan = "s4", world = "s4-4" },
|
||||
{ variant = "sable-noshade", plan = "s4", world = "s4-4" },
|
||||
{ variant = "C1", plan = "s4", world = "s4-4", jvm = "J0", reps = 1, capture = 2700, repeat = 2, tag = "decay", manual = true, note = "45-min decay; CSV capped at 30 min by mangohud-local" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B6"
|
||||
title = "ModernFix dynamic_resources"
|
||||
rule = "Ship `dynamic_resources=true` only if: heap −300 MB or more *or* startup faster beyond spread, **and** zero missing textures, **and** no crash in 3 launches"
|
||||
interleave = true
|
||||
manual_checks = [
|
||||
"S6 by hand",
|
||||
"heap after load (/sparkc heapsummary)",
|
||||
"Missing-texture check: JEI @chipped/@rechiseled/@handcrafted, 4x4 Rechiseled CTM wall",
|
||||
]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s1,s2", repeat = 3 },
|
||||
{ variant = "mf-dynres-off", plan = "s1,s2", repeat = 3 },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B7"
|
||||
title = "shaders"
|
||||
rule = "Data for the owner's shader policy, no auto-decision. Report the FPS cost per profile and whether ship corruption appears with shadow profiles"
|
||||
proxy_note = "Profiles are reproduced by shader-profiles/*.txt (gen_shader_profiles.py resolves the pack's profile.NAME into an Iris options file); rows run unattended. Visual checks stay manual."
|
||||
manual_checks = ["Flywheel backend line in F3 (expect off with shaders)", "visual check for Iris #3164 on ships (shadow profiles: makeup-medium, compl-medium)", "confirm Iris' shader screen shows the intended profile once per pack"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "off", tag = "noshader" },
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "shadowless_medium", shader_options = "shader-profiles/MakeUp-UltraFast-9.5f.shadowless_medium.txt", tag = "makeup-shadowless" },
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", shader_profile = "medium", shader_options = "shader-profiles/MakeUp-UltraFast-9.5f.medium.txt", tag = "makeup-medium" },
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "LOW", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.LOW.txt", tag = "compl-low" },
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "ComplementaryReimagined_r5.9.3.zip", shader_profile = "MEDIUM", shader_options = "shader-profiles/ComplementaryReimagined_r5.9.3.MEDIUM.txt", tag = "compl-medium" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B8"
|
||||
title = "entities"
|
||||
rule = "EMF: adopt the smallest LOD that's a win with FA on. LDL: per tier, the cheapest mode that's a win. FA guidance → `PLAYER-TUNING.md` §4"
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s3" },
|
||||
{ variant = "C1-fa", plan = "s3" },
|
||||
{ variant = "C1-fa-ext", plan = "s3" },
|
||||
{ variant = "emf-lod20-fa", plan = "s3" },
|
||||
{ variant = "emf-lod8-fa", plan = "s3" },
|
||||
{ variant = "ldl-fancy", plan = "s3" },
|
||||
{ variant = "ldl-fastest", plan = "s3" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B9"
|
||||
title = "Flywheel"
|
||||
rule = "Per tier: the smoothest value meeting the floor"
|
||||
sessions = [
|
||||
{ variant = "fw-flat", plan = "s2", shader = "off" },
|
||||
{ variant = "C1", plan = "s2", shader = "off" },
|
||||
{ variant = "fw-smooth", plan = "s2", shader = "off" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B10"
|
||||
title = "Sound Physics"
|
||||
rule = "Adopt each change that's a win. Listen for obvious audio breakage."
|
||||
manual_checks = ["listen for audio breakage"]
|
||||
sessions = [
|
||||
{ variant = "spr-up", plan = "s2s" },
|
||||
{ variant = "C1", plan = "s2s" },
|
||||
{ variant = "spr-strict", plan = "s2s" },
|
||||
{ variant = "spr-nocap", plan = "s2s" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B11"
|
||||
title = "vanilla / Sodium"
|
||||
rule = """3 threads only if S5 wins and S1 doesn't lose \
|
||||
| Lowest blend radius that's a win, per tier \
|
||||
| Mip 4 for mid/high only if VRAM peak ≤ 5.5 GB and no loss"""
|
||||
proxy_note = "C1 baseline session added in-block so analyze.py compares within the label."
|
||||
manual_checks = ["VRAM peak for opt-mip4"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s1,s2,s5", tag = "baseline" },
|
||||
{ variant = "sod-thr3", plan = "s1,s5" },
|
||||
{ variant = "opt-blend2", plan = "s5" },
|
||||
{ variant = "opt-blend0", plan = "s5" },
|
||||
{ variant = "opt-mip4", plan = "s1,s2" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B12a"
|
||||
title = "added-mod ablation"
|
||||
rule = "Keep the mod unless removing it is a win (it costs FPS) or it breaks something visible. Sodium Extra + RSO stay regardless for their per-machine options."
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s1,s2s,s3" },
|
||||
{ variant = "C1", plan = "s1,s2s,s3", disable_mods = ["BadOptimizations-*.jar"], tag = "no-badopt" },
|
||||
{ variant = "C1", plan = "s1,s2s,s3", disable_mods = ["particle_core-*.jar"], tag = "no-particlecore" },
|
||||
{ variant = "C1", plan = "s1,s2s,s3", disable_mods = ["sodium-extra-*.jar"], tag = "no-sodiumextra" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B12b"
|
||||
title = "trial mods"
|
||||
rule = "Owner pre-approved helpful adds. Add with `packwiz modrinth add` only on a win with no breakage. **Not More Culling**: its NeoForge build declares `minecraft [1.21,1.21.1)`. A new mod set means re-running B0 first."
|
||||
proxy_note = "add_mods entries name [trials] keys. Trials are temporary for the session; a permanent add changes the mod set and invalidates B0."
|
||||
manual_checks = ["breakage check per trial", "S6 for Gnetum/Flerovium (GUI paths) by hand"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "s1,s2,s3,s5", tag = "baseline" },
|
||||
{ variant = "C1", plan = "s1,s2,s3,s5", add_mods = ["gnetum"], tag = "gnetum" },
|
||||
{ variant = "C1", plan = "s2,s3", add_mods = ["better_block_entities"], tag = "bbe" },
|
||||
{ variant = "C1-fa", plan = "s3", tag = "fa-baseline" },
|
||||
{ variant = "C1-fa", plan = "s3", add_mods = ["better_block_entities"], tag = "bbe-fa" },
|
||||
{ variant = "C1", plan = "s1,s2,s3,s5", add_mods = ["flerovium"], tag = "flerovium" },
|
||||
{ variant = "C1", plan = "s1,s2,s5", add_mods = ["super_resolution"], tag = "superres" },
|
||||
{ variant = "C1", plan = "s1,s2,s5", add_mods = ["ixeris"], tag = "ixeris" },
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", tag = "shader-baseline" },
|
||||
{ variant = "C1", plan = "s1,s2,s4", world = "s4-4", shader = "MakeUp-UltraFast-9.5f.zip", add_mods = ["iris_flywheel_compat"], tag = "irisflw" },
|
||||
{ variant = "C1-bl", plan = "s1,s5", tag = "bl-baseline" },
|
||||
{ variant = "C1-bl", plan = "s1,s5", add_mods = ["cull_leaves"], tag = "cull-leaves", note = "MidnightLib is already in the pack (1.10.0)" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B13"
|
||||
title = "profile"
|
||||
rule = "Flag any mod over 5 % of render-thread time that isn't Sodium, Iris, DH or Flywheel"
|
||||
params = ["scene"]
|
||||
proxy_note = "Scene = the worst scene from B2 under C1 (--param scene=sN)."
|
||||
manual_checks = ["/sparkc profiler start --timeout 60 during the capture; save the report"]
|
||||
sessions = [
|
||||
{ variant = "C1", plan = "${scene}", reps = 1, capture = 75, manual = true, note = "/sparkc is a client command; start it by hand at BENCH_MARK start" },
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B14"
|
||||
title = "spawn tuning"
|
||||
manual = true
|
||||
rule = "Procedure and keep/back-off rule: `SPAWN-TUNING.md` §B14"
|
||||
proxy_note = "Singleplayer fresh world `onionspawn`; river, jungle, warm ocean 10 min each; per-species and total entity counts, defaults vs shipped spawn files. Not a frametime session - run by hand."
|
||||
sessions = []
|
||||
@@ -0,0 +1,18 @@
|
||||
# Iris options reproducing profile.LOW of ComplementaryReimagined_r5.9.3.zip
|
||||
# pack sha256 fed6c879e732cf7fc7bc6570ebb1def43235aa7e96f62859fbdbdbfa0b484b38
|
||||
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
|
||||
# copy keeping only non-default values (bench.py journals and restores it)
|
||||
SHADOW_QUALITY=0
|
||||
shadowDistance=96.0
|
||||
WATER_REFLECT_QUALITY=2
|
||||
BLOCK_REFLECT_QUALITY=1
|
||||
LIGHTSHAFT_QUALI_DEFINE=0
|
||||
SSAO_QUALI_DEFINE=2
|
||||
FXAA_DEFINE=-1
|
||||
DETAIL_QUALITY=2
|
||||
CLOUD_QUALITY=2
|
||||
ANISOTROPIC_FILTER=0
|
||||
COLORED_LIGHTING=0
|
||||
WORLD_SPACE_REFLECTIONS=-1
|
||||
ENTITY_SHADOW=-1
|
||||
RAIN_PUDDLES=0
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Iris options reproducing profile.MEDIUM of ComplementaryReimagined_r5.9.3.zip
|
||||
# pack sha256 fed6c879e732cf7fc7bc6570ebb1def43235aa7e96f62859fbdbdbfa0b484b38
|
||||
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
|
||||
# copy keeping only non-default values (bench.py journals and restores it)
|
||||
SHADOW_QUALITY=2
|
||||
shadowDistance=128.0
|
||||
WATER_REFLECT_QUALITY=2
|
||||
BLOCK_REFLECT_QUALITY=1
|
||||
LIGHTSHAFT_QUALI_DEFINE=1
|
||||
SSAO_QUALI_DEFINE=2
|
||||
FXAA_DEFINE=1
|
||||
DETAIL_QUALITY=2
|
||||
CLOUD_QUALITY=2
|
||||
ANISOTROPIC_FILTER=0
|
||||
COLORED_LIGHTING=0
|
||||
WORLD_SPACE_REFLECTIONS=-1
|
||||
ENTITY_SHADOW=-1
|
||||
RAIN_PUDDLES=0
|
||||
@@ -0,0 +1,33 @@
|
||||
# Iris options reproducing profile.medium of MakeUp-UltraFast-9.5f.zip
|
||||
# pack sha256 afbf622b983d29ffa5fe5f15197f321d3921915bd44584d85f14bc11b5e0e5e8
|
||||
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
|
||||
# copy keeping only non-default values (bench.py journals and restores it)
|
||||
SHADOW_CASTING=true
|
||||
SHADOW_QTY_SLIDER=2
|
||||
SHADOW_DISTANCE_SLIDER=1
|
||||
REFLECTION_SLIDER=2
|
||||
DOF=false
|
||||
DOF_STRENGTH=0.09
|
||||
AO=1
|
||||
AOSTEPS=5
|
||||
AO_STRENGTH=0.70
|
||||
AA_TYPE=2
|
||||
MOTION_BLUR=false
|
||||
REFRACTION=1
|
||||
SUN_REFLECTION=1
|
||||
WAVING=1
|
||||
V_CLOUDS=1
|
||||
CLOUD_STEPS_AVG=10
|
||||
CLOUD_VOL_STYLE=0
|
||||
BLOOM_SAMPLES=3.0
|
||||
BLOOM=true
|
||||
CHROMA_ABER=0
|
||||
VOL_LIGHT=1
|
||||
CLOUD_REFLECTION=true
|
||||
WATER_ABSORPTION=0.10
|
||||
COLOR_SCHEME=1
|
||||
VANILLA_WATER=false
|
||||
WATER_COLOR_SOURCE=0
|
||||
COLORED_SHADOW=true
|
||||
MOTION_BLUR_SAMPLES=4
|
||||
MATERIAL_GLOSS=true
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
# Iris options reproducing profile.shadowless_medium of MakeUp-UltraFast-9.5f.zip
|
||||
# pack sha256 afbf622b983d29ffa5fe5f15197f321d3921915bd44584d85f14bc11b5e0e5e8
|
||||
# generated by gen_shader_profiles.py - do not edit; Iris rewrites the live
|
||||
# copy keeping only non-default values (bench.py journals and restores it)
|
||||
SHADOW_CASTING=false
|
||||
SHADOW_QTY_SLIDER=1
|
||||
SHADOW_DISTANCE_SLIDER=0
|
||||
REFLECTION_SLIDER=1
|
||||
DOF=false
|
||||
DOF_STRENGTH=0.09
|
||||
AO=1
|
||||
AOSTEPS=5
|
||||
AO_STRENGTH=0.70
|
||||
AA_TYPE=0
|
||||
MOTION_BLUR=false
|
||||
REFRACTION=1
|
||||
SUN_REFLECTION=1
|
||||
WAVING=1
|
||||
V_CLOUDS=1
|
||||
CLOUD_STEPS_AVG=10
|
||||
CLOUD_VOL_STYLE=0
|
||||
BLOOM_SAMPLES=3.0
|
||||
BLOOM=false
|
||||
CHROMA_ABER=0
|
||||
VOL_LIGHT=0
|
||||
CLOUD_REFLECTION=false
|
||||
WATER_ABSORPTION=0.10
|
||||
COLOR_SCHEME=1
|
||||
VANILLA_WATER=false
|
||||
WATER_COLOR_SOURCE=0
|
||||
COLORED_SHADOW=false
|
||||
MOTION_BLUR_SAMPLES=4
|
||||
MATERIAL_GLOSS=true
|
||||
@@ -0,0 +1,4 @@
|
||||
[14Oct2026 22:21:53.972] [modloading-worker-0/ERROR] [net.neoforged.fml.javafmlmod.FMLModContainer/LOADING]: Failed to create mod instance. ModID: midnightlib, class eu.midnightdust.core.MidnightLib
|
||||
[14Oct2026 22:21:57.100] [Render thread/FATAL] [net.minecraft.client.Minecraft/]: Unreported exception thrown!
|
||||
[14Oct2026 22:21:57.200] [Render thread/ERROR] [net.neoforged.fml.ModLoader/]: Cowardly refusing to send event net.neoforged.neoforge.client.event.RenderFrameEvent to a broken mod state
|
||||
[14Oct2026 22:21:57.300] [Render thread/INFO] [mixin/]: harmless line mentioning FATAL in text only
|
||||
@@ -0,0 +1,11 @@
|
||||
[15Oct2026 15:25:44.104] [Render thread/INFO] [net.minecraft.client.renderer.texture.TextureAtlas/]: Created: 4096x4096x2 minecraft:textures/atlas/blocks.png-atlas
|
||||
[15Oct2026 15:25:52.383] [Render thread/WARN] [ModernFix/]: Game took 28.366 seconds to start
|
||||
[15Oct2026 15:25:53.147] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Onion4Dinner joined the game
|
||||
[15Oct2026 15:25:59.974] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: Server Backup started!
|
||||
[15Oct2026 15:26:03.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_PLAN p20261015152526 begin
|
||||
[15Oct2026 15:26:13.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_MARK warm s1 1
|
||||
[15Oct2026 15:26:13.050] [Render thread/INFO] [net.minecraft.client.gui.components.ChatComponent/]: [System] [CHAT] [Server] BENCH_MARK warm s1 1
|
||||
[15Oct2026 15:26:28.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_MARK start s1 1
|
||||
[15Oct2026 15:26:28.040] [Render thread/INFO] [net.minecraft.client.gui.components.ChatComponent/]: [System] [CHAT] [Server] BENCH_MARK start s1 1
|
||||
[15Oct2026 15:26:58.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_MARK end s1 1
|
||||
[15Oct2026 15:26:58.000] [Server thread/INFO] [net.minecraft.server.MinecraftServer/]: [Server] BENCH_DONE p20261015152526
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
os,cpu,gpu,ram,kernel,driver,cpuscheduler
|
||||
Manjaro Linux,AMD Ryzen 9 9900X 12-Core Processor,AMD Radeon RX 7800 XT ,96396388,7.1.8-1-MANJARO,,performance
|
||||
fps,frametime,cpu_load,cpu_power,gpu_load,cpu_temp,gpu_temp,gpu_core_clock,gpu_mem_clock,gpu_vram_used,gpu_power,ram_used,swap_used,process_rss,cpu_mhz,elapsed
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,1900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,2900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,3900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,4900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,5900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,6900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,7900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,8900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9000000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,9900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,10900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,11900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,12900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,13900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,14900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,15900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,16900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,17900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,18900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19000000000
|
||||
50.0000,20.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19100000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19200000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19300000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19400000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19500000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19600000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19700000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19800000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,19900000000
|
||||
250.0000,4.0000,0,0,0,0,0,0,0,0,0,0,0,0,0,20000000000
|
||||
truncated,row
|
||||
|
@@ -0,0 +1,97 @@
|
||||
"""analyze.py: tagged labels ('B3.dh-off') take their baseline from the same block.
|
||||
|
||||
python3 -m unittest discover -s analysis/client-perf/harness/tests
|
||||
"""
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
|
||||
import analyze # noqa: E402
|
||||
|
||||
|
||||
def row(label, variant, scene, session, p99, low=100.0, fps=200.0):
|
||||
return {"kind": "window", "label": label, "variant": variant, "scene": scene,
|
||||
"session": session, "flags": [],
|
||||
"metrics": {"p99_ms": p99, "one_low_fps": low, "avg_fps": fps}}
|
||||
|
||||
|
||||
def cell(table, label, variant, scene):
|
||||
for line in table.splitlines():
|
||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||
if cells[:3] == [label, variant, scene]:
|
||||
return cells
|
||||
raise AssertionError(f"no row {label}/{variant}/{scene} in\n{table}")
|
||||
|
||||
|
||||
class BlockBaseline(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.rows = [
|
||||
# noise: spread 0.2 ms / 2 fps
|
||||
row("B0", "C0", "s1", "n1", 10.0, 100), row("B0", "C0", "s1", "n2", 10.2, 102),
|
||||
# global C1 reference elsewhere (B2) is much slower than the B3 one
|
||||
row("B2", "C1", "s1", "g1", 20.0),
|
||||
row("B3", "C1", "s1", "b1", 10.0),
|
||||
row("B3", "dh-r64", "s1", "b2", 8.0),
|
||||
row("B3.dh-off", "C1", "s1", "b3", 6.0),
|
||||
]
|
||||
|
||||
def test_tagged_self_variant_uses_untagged_block_label(self):
|
||||
s = analyze.summarise(self.rows)
|
||||
b, where = analyze.self_baseline(s, "B3.dh-off", "s1", "C1")
|
||||
self.assertEqual(where, "B3")
|
||||
self.assertEqual(b["p99"], 10.0)
|
||||
|
||||
def test_untagged_block_label_is_its_own_baseline(self):
|
||||
s = analyze.summarise(self.rows)
|
||||
self.assertEqual(analyze.self_baseline(s, "B3", "s1", "C1"), (None, None))
|
||||
|
||||
def test_tagged_other_variant_prefers_block_over_global(self):
|
||||
rows = self.rows + [row("B12a.no-x", "X", "s1", "t1", 9.0)]
|
||||
rows += [row("B12a", "C1", "s1", "t0", 12.0)]
|
||||
s = analyze.summarise(rows)
|
||||
b, where = analyze.baseline_for(s, rows, "B12a.no-x", "s1", "C1")
|
||||
self.assertEqual((where, b["p99"]), ("B12a", 12.0))
|
||||
|
||||
def test_block_baseline_tag(self):
|
||||
rows = self.rows + [row("B11.baseline", "C1", "s5", "x0", 12.0),
|
||||
row("B11", "sod-thr3", "s5", "x1", 11.0),
|
||||
row("B2", "C1", "s5", "x2", 30.0)]
|
||||
s = analyze.summarise(rows)
|
||||
b, where = analyze.baseline_for(s, rows, "B11", "s5", "C1")
|
||||
# untagged candidate: own label has no C1 -> '<block>.baseline', not global
|
||||
self.assertEqual((where, b["p99"]), ("B11.baseline", 12.0))
|
||||
rows.append(row("B11.x", "opt-mip4", "s5", "x3", 11.0))
|
||||
s = analyze.summarise(rows)
|
||||
b, where = analyze.baseline_for(s, rows, "B11.x", "s5", "C1")
|
||||
self.assertEqual((where, b["p99"]), ("B11.baseline", 12.0))
|
||||
|
||||
def test_block_pool_then_global(self):
|
||||
rows = self.rows + [row("B4.J0", "C1", "s2", "j0", 10.0),
|
||||
row("B4.J1", "C1", "s2", "j1", 12.0),
|
||||
row("B4.x", "Y", "s2", "j2", 9.0),
|
||||
row("B2", "C1", "s2", "g2", 40.0)]
|
||||
s = analyze.summarise(rows)
|
||||
b, where = analyze.baseline_for(s, rows, "B4.x", "s2", "C1")
|
||||
self.assertEqual((where, b["p99"]), ("B4.*", 11.0))
|
||||
b, where = analyze.baseline_for(s, rows, "B9.z", "s2", "C1")
|
||||
self.assertEqual(where, "*")
|
||||
self.assertIsNone(analyze.baseline_for(s, rows, "B9.z", "s9", "C1")[0])
|
||||
|
||||
def test_table_verdict_against_block(self):
|
||||
t = analyze.table(analyze.summarise(self.rows), self.rows, "C1", "B0")
|
||||
c = cell(t, "B3.dh-off", "C1", "s1")
|
||||
self.assertEqual(c[-4:], ["C1@B3", "+4.00", "+0.0", "win"])
|
||||
self.assertEqual(cell(t, "B3", "C1", "s1")[-1], "baseline")
|
||||
self.assertEqual(cell(t, "B3", "dh-r64", "s1")[-4], "C1@B3")
|
||||
|
||||
def test_table_label_filter_still_finds_block_baseline(self):
|
||||
shown = [r for r in self.rows if r["label"] in ("B3.dh-off", "B0")]
|
||||
t = analyze.table(analyze.summarise(shown), self.rows, "C1", "B0")
|
||||
self.assertEqual(cell(t, "B3.dh-off", "C1", "s1")[-4], "C1@B3")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,294 @@
|
||||
"""Offline tests for bench.py session instance changes (temp dirs only, no game).
|
||||
|
||||
python3 -m unittest discover -s analysis/client-perf/harness/tests
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
|
||||
import bench # noqa: E402
|
||||
|
||||
IRIS = """#This file stores configuration options for Iris, such as the currently active shaderpack
|
||||
#Fri Oct 16 19:37:17 EDT 2026
|
||||
allowUnknownShaders=false
|
||||
enableShaders=true
|
||||
maxShadowRenderDistance=32
|
||||
shaderPack=
|
||||
"""
|
||||
|
||||
DH = """[server]
|
||||
\tenableRealTimeUpdates = true
|
||||
|
||||
[client]
|
||||
\tshowDhOptionsButtonInMinecraftUi = true
|
||||
|
||||
\t\t[client.advanced.debugging]
|
||||
\t\t\tlodOnlyMode = false
|
||||
\t\t\t# DISABLED: Disable rendering
|
||||
\t\t\trendererMode = "DEFAULT"
|
||||
|
||||
\t\t\t[client.advanced.debugging.debugWireframe]
|
||||
\t\t\t\tenableRendering = false
|
||||
\t\t\t\trendererMode = "DEFAULT"
|
||||
"""
|
||||
|
||||
|
||||
class GameDir(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self._td = tempfile.TemporaryDirectory()
|
||||
self.game = Path(self._td.name)
|
||||
(self.game / "config").mkdir()
|
||||
(self.game / "mods").mkdir()
|
||||
(self.game / "shaderpacks").mkdir()
|
||||
(self.game / "saves" / "onionbench").mkdir(parents=True)
|
||||
(self.game / "saves" / "onionbench" / "level.dat").write_bytes(b"orig")
|
||||
(self.game / "config" / "iris.properties").write_text(IRIS)
|
||||
(self.game / "config" / "DistantHorizons.toml").write_text(DH)
|
||||
(self.game / "shaderpacks" / "Pack A.zip").write_bytes(b"zip")
|
||||
for n in ("BadOptimizations-2.4.1.jar", "particle_core-0.3.3.jar", "sodium-0.8.jar"):
|
||||
(self.game / "mods" / n).write_bytes(n.encode())
|
||||
self.j = bench.Journal.begin(self.game, "ptest")
|
||||
|
||||
def tearDown(self):
|
||||
self._td.cleanup()
|
||||
|
||||
def snapshot(self):
|
||||
"""Instance contents, excluding the journal itself (setUp opened one)."""
|
||||
return {p.relative_to(self.game).as_posix(): p.read_bytes()
|
||||
for p in sorted(self.game.rglob("*")) if p.is_file()
|
||||
and p.relative_to(self.game).parts[0] not in (bench.PENDING, bench.BACKUP_DIR)}
|
||||
|
||||
|
||||
class IrisAndDh(GameDir):
|
||||
def test_properties_rewrite(self):
|
||||
out = bench.set_properties(IRIS, {"enableShaders": "false", "shaderPack": "a:b=c.zip"})
|
||||
self.assertIn("enableShaders=false\n", out)
|
||||
self.assertIn("shaderPack=a\\:b\\=c.zip\n", out)
|
||||
self.assertIn("maxShadowRenderDistance=32", out)
|
||||
self.assertTrue(out.startswith("#This file"))
|
||||
self.assertIn("newKey=1", bench.set_properties(IRIS, {"newKey": "1"}))
|
||||
|
||||
def test_shader_on_off_restore(self):
|
||||
before = self.snapshot()
|
||||
opts = self.game.parent / f"{self.game.name}-opts.txt"
|
||||
opts.write_text("SHADOW=off\n")
|
||||
try:
|
||||
bench.apply_shader(self.game, self.j, "Pack A.zip", opts)
|
||||
finally:
|
||||
text = opts.read_text()
|
||||
opts.unlink()
|
||||
cfg = (self.game / "config" / "iris.properties").read_text()
|
||||
self.assertIn("enableShaders=true", cfg)
|
||||
self.assertIn("shaderPack=Pack A.zip", cfg)
|
||||
self.assertEqual((self.game / "shaderpacks" / "Pack A.zip.txt").read_text(), text)
|
||||
self.assertEqual(bench.restore_pending(self.game), [])
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
|
||||
j = bench.Journal.begin(self.game, "p2")
|
||||
bench.apply_shader(self.game, j, "off")
|
||||
cfg = (self.game / "config" / "iris.properties").read_text()
|
||||
self.assertIn("enableShaders=false", cfg)
|
||||
self.assertIn("shaderPack=\n", cfg)
|
||||
bench.restore_pending(self.game)
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
|
||||
def test_dh_rewrite_only_debugging_section(self):
|
||||
off = bench.set_dh_rendering(DH, False)
|
||||
self.assertIn('\t\t\trendererMode = "DISABLED"\n\n', off)
|
||||
self.assertIn('\t\t\t\trendererMode = "DEFAULT"', off) # wireframe section untouched
|
||||
self.assertIn("enableRendering = false", off)
|
||||
self.assertEqual(bench.set_dh_rendering(off, True), DH)
|
||||
with self.assertRaises(ValueError):
|
||||
bench.set_dh_rendering("[client]\n\trendererMode = \"DEFAULT\"\n", False)
|
||||
|
||||
def test_apply_dh_restore(self):
|
||||
bench.apply_dh(self.game, self.j, False)
|
||||
self.assertIn('"DISABLED"', (self.game / "config" / "DistantHorizons.toml").read_text())
|
||||
bench.restore_pending(self.game)
|
||||
self.assertEqual((self.game / "config" / "DistantHorizons.toml").read_text(), DH)
|
||||
|
||||
|
||||
class Mods(GameDir):
|
||||
def test_disable_add_restore(self):
|
||||
before = self.snapshot()
|
||||
names = bench.resolve_disable(self.game, ["badopt*", "PARTICLE_*"])
|
||||
self.assertEqual(names, ["BadOptimizations-2.4.1.jar", "particle_core-0.3.3.jar"])
|
||||
with self.assertRaises(ValueError):
|
||||
bench.resolve_disable(self.game, ["badopt*", "nomatch*"])
|
||||
extra = self.game.parent / f"{self.game.name}-trial.jar"
|
||||
extra.write_bytes(b"trial")
|
||||
try:
|
||||
jars = bench.check_add_mods(self.game, [str(extra)])
|
||||
h0 = bench.mods_hash(self.game / "mods")
|
||||
bench.disable_mods(self.game, self.j, names)
|
||||
bench.add_mods(self.game, self.j, jars)
|
||||
finally:
|
||||
extra.unlink()
|
||||
mods = sorted(p.name for p in (self.game / "mods").iterdir())
|
||||
self.assertEqual(mods, sorted(["BadOptimizations-2.4.1.jar.disabled",
|
||||
f"{self.game.name}-trial.jar",
|
||||
"particle_core-0.3.3.jar.disabled", "sodium-0.8.jar"]))
|
||||
self.assertNotEqual(bench.mods_hash(self.game / "mods"), h0)
|
||||
self.assertEqual(bench.restore_pending(self.game), [])
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
self.assertEqual(bench.mods_hash(self.game / "mods"), h0)
|
||||
|
||||
def test_add_mods_validation(self):
|
||||
with self.assertRaises(ValueError):
|
||||
bench.check_add_mods(self.game, [str(self.game / "nope.jar")])
|
||||
with self.assertRaises(ValueError): # name collision
|
||||
bench.check_add_mods(self.game, [str(self.game / "mods" / "sodium-0.8.jar")])
|
||||
|
||||
|
||||
class WorldZip(GameDir):
|
||||
def make_zip(self, prefix):
|
||||
z = self.game.parent / f"{self.game.name}-{prefix.strip('/') or 'flat'}.zip"
|
||||
with zipfile.ZipFile(z, "w") as zf:
|
||||
zf.writestr(prefix + "level.dat", b"zipped")
|
||||
zf.writestr(prefix + "region/r.0.0.mca", b"region")
|
||||
self.addCleanup(z.unlink)
|
||||
return z
|
||||
|
||||
def check_swap(self, prefix):
|
||||
before = self.snapshot()
|
||||
z = self.make_zip(prefix)
|
||||
self.assertEqual(bench.zip_world_root(z), prefix)
|
||||
sha = bench.swap_world(self.game, self.j, z)
|
||||
self.assertEqual(sha, bench.sha256_file(z))
|
||||
w = self.game / "saves" / "onionbench"
|
||||
self.assertEqual((w / "level.dat").read_bytes(), b"zipped")
|
||||
self.assertTrue((w / "region" / "r.0.0.mca").exists())
|
||||
self.assertTrue((self.game / "saves" / "onionbench.harness-bak" / "level.dat").exists())
|
||||
self.assertEqual(sorted(p.name for p in (self.game / "saves").iterdir()),
|
||||
["onionbench", "onionbench.harness-bak"])
|
||||
self.assertEqual(bench.restore_pending(self.game), [])
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
self.assertEqual([p.name for p in (self.game / "saves").iterdir()], ["onionbench"])
|
||||
|
||||
def test_flat_layout(self):
|
||||
self.check_swap("")
|
||||
|
||||
def test_single_top_dir_layout(self):
|
||||
self.check_swap("bench-world-v1/")
|
||||
|
||||
def test_bad_zips(self):
|
||||
bad = self.game.parent / f"{self.game.name}-bad.zip"
|
||||
self.addCleanup(lambda: bad.unlink(missing_ok=True))
|
||||
with zipfile.ZipFile(bad, "w") as zf:
|
||||
zf.writestr("a/level.dat", b"x")
|
||||
zf.writestr("b/level.dat", b"x")
|
||||
with self.assertRaises(ValueError):
|
||||
bench.zip_world_root(bad)
|
||||
with zipfile.ZipFile(bad, "w") as zf:
|
||||
zf.writestr("../evil/level.dat", b"x")
|
||||
with self.assertRaises(ValueError):
|
||||
bench.zip_world_root(bad)
|
||||
|
||||
|
||||
class PendingJournal(GameDir):
|
||||
def test_crash_then_restore_from_disk(self):
|
||||
before = self.snapshot()
|
||||
z = WorldZip.make_zip(self, "w/")
|
||||
bench.apply_shader(self.game, self.j, "Pack A.zip")
|
||||
bench.apply_dh(self.game, self.j, False)
|
||||
bench.disable_mods(self.game, self.j, ["sodium-0.8.jar"])
|
||||
bench.swap_world(self.game, self.j, z)
|
||||
plan = self.game / "saves" / "onionbench" / "datapacks" / "autoplan"
|
||||
self.j.record({"op": "tree", "path": plan.relative_to(self.game).as_posix()})
|
||||
plan.mkdir(parents=True)
|
||||
(plan / "pack.mcmeta").write_text("{}")
|
||||
# simulate a hard kill: only the on-disk journal survives
|
||||
del self.j
|
||||
self.assertTrue(bench.pending_exists(self.game))
|
||||
data = json.loads((self.game / bench.PENDING).read_text())
|
||||
self.assertEqual([e["op"] for e in data["entries"]],
|
||||
["file", "file", "file", "rename", "world", "tree"])
|
||||
with self.assertRaises(RuntimeError):
|
||||
bench.Journal.begin(self.game, "again")
|
||||
self.assertEqual(bench.restore_pending(self.game), [])
|
||||
self.assertFalse(bench.pending_exists(self.game))
|
||||
self.assertFalse((self.game / bench.BACKUP_DIR).exists())
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
|
||||
def test_entry_written_before_mutation_is_tolerated(self):
|
||||
# journal says "renamed", but the kill came before os.rename ran
|
||||
self.j.record({"op": "rename", "src": "mods/sodium-0.8.jar",
|
||||
"dst": "mods/sodium-0.8.jar.disabled"})
|
||||
self.j.record({"op": "added", "path": "mods/never-copied.jar"})
|
||||
before = self.snapshot()
|
||||
self.assertEqual(bench.restore_pending(self.game), [])
|
||||
self.assertEqual(self.snapshot(), before)
|
||||
|
||||
def test_failed_entry_stays_journaled(self):
|
||||
self.j.record({"op": "bogus"})
|
||||
failed = bench.restore_pending(self.game)
|
||||
self.assertEqual(len(failed), 1)
|
||||
data = json.loads((self.game / bench.PENDING).read_text())
|
||||
self.assertEqual(data["entries"], [{"op": "bogus"}])
|
||||
|
||||
def test_nothing_pending(self):
|
||||
bench.restore_pending(self.game)
|
||||
self.assertEqual(bench.restore_pending(self.game), [])
|
||||
|
||||
|
||||
class OptionsAndFlags(unittest.TestCase):
|
||||
def test_resource_pack_reset(self):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
g = Path(td)
|
||||
(g / "options.txt").write_text('version:3955\nresourcePacks:["vanilla","x.zip"]\n'
|
||||
"maxFps:260\n")
|
||||
bench.reset_resource_packs(g)
|
||||
vals = bench.options_values(g, {"resourcePacks", "incompatibleResourcePacks"})
|
||||
self.assertEqual(vals, {"resourcePacks": '["vanilla","mod_resources"]',
|
||||
"incompatibleResourcePacks": "[]"})
|
||||
text = (g / "options.txt").read_text()
|
||||
self.assertTrue(text.startswith("version:3955\n"))
|
||||
self.assertIn("maxFps:260", text)
|
||||
(g / "options.txt").unlink()
|
||||
bench.reset_resource_packs(g) # absent file -> just the baseline
|
||||
self.assertEqual(len((g / "options.txt").read_text().splitlines()), 2)
|
||||
|
||||
def test_env_flags(self):
|
||||
gpus = [{"device": "0x13c0", "dpm_level": "auto"},
|
||||
{"device": bench.DGPU_DEVICE, "dpm_level": "auto"}]
|
||||
self.assertEqual(bench.env_flags(gpus, True), ["gpu_dpm_auto", "server_running"])
|
||||
gpus[1]["dpm_level"] = "high"
|
||||
self.assertEqual(bench.env_flags(gpus, False), [])
|
||||
|
||||
def test_build_parser_session_defaults(self):
|
||||
a = bench.build_parser().parse_args(["session", "--variant", "C1"])
|
||||
self.assertEqual((a.shader, a.dh, a.disable_mods, a.add_mods, a.world_zip),
|
||||
("off", "on", None, None, None))
|
||||
a = bench.build_parser().parse_args(["restore"])
|
||||
self.assertIs(a.fn, bench.cmd_restore)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class ShaderLoaded(unittest.TestCase):
|
||||
def test_flags(self):
|
||||
ok = ["[Render thread/INFO] [Iris/]: Using shaderpack: MakeUp-UltraFast-9.5f.zip"]
|
||||
bad = ok + ["[Render thread/ERROR] [Iris/]: Failed to compile shaders"]
|
||||
self.assertEqual(bench.shader_flags("off", []), [])
|
||||
self.assertEqual(bench.shader_flags("X.zip", ok), [])
|
||||
self.assertEqual(bench.shader_flags("X.zip", []), ["shader_not_loaded"])
|
||||
self.assertEqual(bench.shader_flags("X.zip", bad), ["shader_not_loaded"])
|
||||
|
||||
|
||||
class ResetJvm(unittest.TestCase):
|
||||
def test_reset(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
d = Path(d)
|
||||
(d / "instance.cfg").write_text("JvmArgs=-XX:+UseShenandoahGC\nMaxMemAlloc=6144\n")
|
||||
self.assertFalse(bench.reset_jvm(d))
|
||||
(d / "instance.cfg.harness-orig").write_text("JvmArgs=\nMaxMemAlloc=8192\n")
|
||||
self.assertTrue(bench.reset_jvm(d))
|
||||
self.assertEqual((d / "instance.cfg").read_text(), "JvmArgs=\nMaxMemAlloc=8192\n")
|
||||
@@ -0,0 +1,241 @@
|
||||
"""Offline tests for bench.py / analyze.py (no game, no display).
|
||||
|
||||
python3 -m unittest discover -s analysis/client-perf/harness/tests
|
||||
"""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
|
||||
import analyze # noqa: E402
|
||||
import bench # noqa: E402
|
||||
|
||||
FIX = HERE / "fixtures"
|
||||
CSV = FIX / "mangohud_java_2026-10-15_15-25-26.csv"
|
||||
|
||||
|
||||
def lines(name):
|
||||
return (FIX / name).read_text().splitlines()
|
||||
|
||||
|
||||
class CsvParsing(unittest.TestCase):
|
||||
def test_header_and_rows(self):
|
||||
meta, rows = bench.parse_mangohud_csv(CSV)
|
||||
self.assertIn("RX 7800 XT", meta["gpu"])
|
||||
self.assertEqual(len(rows), 200) # malformed trailing row skipped
|
||||
self.assertAlmostEqual(rows[0][0], 0.1) # elapsed ns -> s
|
||||
self.assertEqual(rows[0][1], 4.0)
|
||||
|
||||
def test_name_time(self):
|
||||
import time
|
||||
t = bench.csv_name_time("java_2026-10-15_15-25-26.csv")
|
||||
self.assertEqual(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(t)),
|
||||
"2026-10-15 15:25:26")
|
||||
self.assertIsNone(bench.csv_name_time("weird.csv"))
|
||||
|
||||
def test_file_birth_method(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = Path(d) / CSV.name
|
||||
shutil.copyfile(CSV, p)
|
||||
t, method = bench.file_birth(p)
|
||||
self.assertIn(method, {"st_birthtime", "stat-W", "filename", "mtime"})
|
||||
self.assertGreater(t, 0)
|
||||
|
||||
|
||||
class Math(unittest.TestCase):
|
||||
def test_pct(self):
|
||||
self.assertEqual(bench.pct([1, 2, 3, 4, 5], 0.5), 3)
|
||||
self.assertAlmostEqual(bench.pct(list(range(1, 101)), 0.99), 99.01)
|
||||
self.assertNotEqual(bench.pct([], 0.5), bench.pct([], 0.5)) # nan
|
||||
|
||||
def test_one_low_from_frametimes(self):
|
||||
# 200 frames: 198 x 5 ms, 2 x 25 ms -> worst 1 % (2 frames) mean 25 ms -> 40 fps
|
||||
rows = [(i * 0.005, 25.0 if i in (50, 150) else 5.0, 200.0) for i in range(200)]
|
||||
m = bench.window_metrics(rows, 1000.0, 1000.0, 1001.0)
|
||||
self.assertEqual(m["n"], 200)
|
||||
self.assertEqual(m["one_low_fps"], 40.0)
|
||||
self.assertEqual(m["p50_ms"], 5.0)
|
||||
self.assertEqual(m["peak_ms"], 25.0)
|
||||
self.assertAlmostEqual(m["avg_fps"], 1000 / (1040 / 200), places=1)
|
||||
self.assertEqual(m["one_low_fps_legacy"], 200.0) # fps column was flat
|
||||
|
||||
def test_window_slicing_alignment(self):
|
||||
_, rows = bench.parse_mangohud_csv(CSV)
|
||||
origin = 5000.0
|
||||
m = bench.window_metrics(rows, origin, origin + 10.0, origin + 20.0)
|
||||
self.assertEqual(m["n"], 101)
|
||||
self.assertEqual(m["p50_ms"], 4.0)
|
||||
self.assertEqual(m["p99_ms"], 20.0)
|
||||
self.assertEqual(m["one_low_fps"], 50.0)
|
||||
self.assertEqual(m["one_low_fps_legacy"], 50.0)
|
||||
self.assertIsNone(bench.window_metrics(rows, origin, origin + 30, origin + 40))
|
||||
# first 10 s contain no 20 ms frames
|
||||
early = bench.window_metrics(rows, origin, origin, origin + 9.9)
|
||||
self.assertEqual(early["p99_ms"], 4.0)
|
||||
|
||||
|
||||
class Markers(unittest.TestCase):
|
||||
def test_parse(self):
|
||||
marks, done = bench.parse_markers(lines("latest.log"))
|
||||
st, en = marks[("start", "s1", 1)], marks[("end", "s1", 1)]
|
||||
self.assertAlmostEqual(en - st, 30.0, places=3)
|
||||
self.assertAlmostEqual(marks[("warm", "s1", 1)] % 1, 0.0, places=3) # server line, not CHAT
|
||||
self.assertAlmostEqual(done, en)
|
||||
|
||||
def test_events_and_alignment(self):
|
||||
ln = lines("latest.log")
|
||||
ev = bench.scan_events(ln)
|
||||
self.assertEqual(ev["game_took_s"], 28.366)
|
||||
self.assertEqual(ev["atlas"], "4096x4096x2")
|
||||
self.assertIsNone(ev["crash"])
|
||||
self.assertAlmostEqual(ev["joined_t"] - ev["title_t"], 0.764, places=3)
|
||||
# CSV born 10 s before the start marker -> window covers elapsed 10..40 s,
|
||||
# CSV ends at 20 s -> 101 frames -> low_n
|
||||
marks, _ = bench.parse_markers(ln)
|
||||
ts, te = marks[("start", "s1", 1)], marks[("end", "s1", 1)]
|
||||
_, rows = bench.parse_mangohud_csv(CSV)
|
||||
m = bench.window_metrics(rows, ts - 10.0, ts, te)
|
||||
self.assertEqual(m["n"], 101)
|
||||
self.assertIn("low_n", bench.validity_flags(m, te - ts, [], bench.backup_times(ln), ts, te))
|
||||
|
||||
def test_backup_flag(self):
|
||||
ln = lines("latest.log")
|
||||
bk = bench.backup_times(ln)
|
||||
self.assertEqual(len(bk), 1)
|
||||
m = {"n": 6000, "p50_ms": 4.0, "avg_fps": 200.0}
|
||||
self.assertEqual(bench.validity_flags(m, 30, [], bk, bk[0] + 30, bk[0] + 60), [])
|
||||
self.assertIn("backup_in_window", bench.validity_flags(m, 30, [], bk, bk[0] - 1, bk[0] + 29))
|
||||
self.assertIn("backup_in_window", bench.validity_flags(m, 30, [], bk, bk[0] + 3, bk[0] + 33))
|
||||
|
||||
def test_vsync_flag(self):
|
||||
m = {"n": 1800, "p50_ms": 16.7, "avg_fps": 59.8}
|
||||
self.assertIn("vsync_suspect", bench.validity_flags(m, 30))
|
||||
m144 = {"n": 4320, "p50_ms": 6.95, "avg_fps": 144}
|
||||
self.assertIn("vsync_suspect", bench.validity_flags(m144, 30, [144.0]))
|
||||
self.assertNotIn("vsync_suspect", bench.validity_flags(m144, 30, [60.0]))
|
||||
self.assertEqual(bench.validity_flags(None, 30), ["no_data"])
|
||||
|
||||
|
||||
class Crash(unittest.TestCase):
|
||||
def test_classify(self):
|
||||
got = [bench.classify_line(l) for l in lines("crash.log")]
|
||||
self.assertEqual(got, ["mod_instance", "fatal", "broken_mod_state", None])
|
||||
self.assertIsNone(bench.classify_line(lines("latest.log")[3])) # backup line
|
||||
self.assertEqual(bench.scan_events(lines("crash.log"))["crash"], "mod_instance")
|
||||
|
||||
|
||||
class Datapack(unittest.TestCase):
|
||||
def test_generate(self):
|
||||
files = bench.generate_datapack("p1", ["s1", "s2s", "s5"], 2, 15, 30, 10)
|
||||
tag = json.loads(files["data/minecraft/tags/function/load.json"])
|
||||
self.assertEqual(tag["values"], ["bench:autoplan/p1/load"])
|
||||
self.assertEqual(json.loads(files["pack.mcmeta"])["pack"]["pack_format"], 48)
|
||||
load = files["data/bench/function/autoplan/p1/load.mcfunction"]
|
||||
self.assertIn("unless score #p1 bench_plan matches 1..", load)
|
||||
steps = sorted((k for k in files if "/step" in k),
|
||||
key=lambda k: int(k.rsplit("step", 1)[1].split(".")[0]))
|
||||
body = "".join(files[k] for k in steps)
|
||||
for kind in ("warm", "start", "end"):
|
||||
for sc in ("s1", "s2s", "s5"):
|
||||
for rep in (1, 2):
|
||||
self.assertEqual(body.count(f"say BENCH_MARK {kind} {sc} {rep}\n"), 1)
|
||||
self.assertIn("function bench:tour_start", body)
|
||||
self.assertIn("function bench:storm_on", body)
|
||||
self.assertIn("function bench:storm_off", body)
|
||||
self.assertIn("schedule function bench:autoplan/p1/step", body)
|
||||
self.assertNotIn(" 0t", body)
|
||||
self.assertTrue(body.rstrip().endswith("say BENCH_DONE p1"))
|
||||
|
||||
def test_timeline_ticks(self):
|
||||
tl = bench.build_timeline(["s1", "s5"], 1, 15, 30, "x")
|
||||
delays = [d for _, d in tl]
|
||||
self.assertEqual(delays, [300, 600, 200, 2400, 0])
|
||||
self.assertEqual(tl[1][0], ["say BENCH_MARK start s1 1"])
|
||||
self.assertIn("say BENCH_MARK end s1 1", tl[2][0]) # merged into next scene's step
|
||||
self.assertEqual(bench.plan_seconds(["s1", "s5"], 1, 15, 30), 45 + 130)
|
||||
|
||||
def test_order_rule(self):
|
||||
class A:
|
||||
plan, any_order = "s2,s1", False
|
||||
with self.assertRaises(SystemExit):
|
||||
bench.session_plan(A)
|
||||
A.any_order = True
|
||||
self.assertEqual(bench.session_plan(A), ["s2", "s1"])
|
||||
A.plan, A.any_order = "s1,s6", False
|
||||
with self.assertRaises(SystemExit):
|
||||
bench.session_plan(A)
|
||||
|
||||
def test_pins(self):
|
||||
out = bench.merge_pins("version:3955\nmaxFps:120\nenableVsync:true\nfov:0.5\n")
|
||||
self.assertIn("maxFps:260", out)
|
||||
self.assertIn("enableVsync:false", out)
|
||||
self.assertIn("fov:0.5", out)
|
||||
self.assertIn("pauseOnLostFocus:false", out)
|
||||
self.assertEqual(out.count("maxFps"), 1)
|
||||
|
||||
|
||||
def row(label, variant, session, p99, low, scene="s1", flags=()):
|
||||
return {"kind": "window", "label": label, "variant": variant, "scene": scene,
|
||||
"session": session, "flags": list(flags),
|
||||
"metrics": {"p99_ms": p99, "one_low_fps": low, "avg_fps": 200.0}}
|
||||
|
||||
|
||||
class Analyze(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.rows = [
|
||||
row("B0", "C0", "a", 10.0, 100), row("B0", "C0", "b", 10.2, 98),
|
||||
row("B0", "C0", "c", 10.4, 99),
|
||||
row("B2", "C0", "d", 10.2, 99),
|
||||
row("B2", "WIN", "e", 9.0, 100), row("B2", "TIE", "f", 10.0, 98),
|
||||
row("B2", "LOSS", "g", 9.0, 90),
|
||||
row("B2", "BAD", "h", 1.0, 999, flags=["crashed"]),
|
||||
]
|
||||
|
||||
def test_summary_spread(self):
|
||||
s = analyze.summarise(self.rows[:3])
|
||||
g = s[("B0", "C0", "s1")]
|
||||
self.assertEqual(g["sessions"], 3)
|
||||
self.assertAlmostEqual(g["p99"], 10.2)
|
||||
self.assertAlmostEqual(g["p99_spread"], 0.4)
|
||||
self.assertAlmostEqual(g["low_spread"], 2)
|
||||
|
||||
def test_verdicts(self):
|
||||
valid = [r for r in self.rows if analyze.is_valid(r)]
|
||||
self.assertEqual(len(valid), 7)
|
||||
s = analyze.summarise(valid)
|
||||
sp = analyze.noise_spread(s, "B0")
|
||||
base = s[("B2", "C0", "s1")]
|
||||
self.assertEqual(analyze.verdict(base, s[("B2", "WIN", "s1")], *sp), "win")
|
||||
self.assertEqual(analyze.verdict(base, s[("B2", "TIE", "s1")], *sp), "tie")
|
||||
self.assertEqual(analyze.verdict(base, s[("B2", "LOSS", "s1")], *sp), "loss")
|
||||
t = analyze.table(s, valid, "C0", "B0")
|
||||
self.assertIn("| B2 | WIN | s1 |", t)
|
||||
self.assertIn("| win |", t)
|
||||
self.assertNotIn("BAD", t)
|
||||
|
||||
def test_pooled_baseline_and_cli(self):
|
||||
rows = [r for r in self.rows if not (r["label"] == "B2" and r["variant"] == "C0")]
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p = Path(d) / "s.jsonl"
|
||||
p.write_text("".join(json.dumps(r) + "\n" for r in rows))
|
||||
loaded = analyze.load_rows(p)
|
||||
s = analyze.summarise([r for r in loaded if analyze.is_valid(r)])
|
||||
b, lab = analyze.baseline_for(s, loaded, "B2", "s1", "C0")
|
||||
self.assertEqual(lab, "*")
|
||||
self.assertAlmostEqual(b["p99"], 10.2)
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
rc = analyze.main(["--file", str(p), "--baseline", "C0", "--noise", "B0"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("| B2 | WIN | s1 |", buf.getvalue())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,404 @@
|
||||
"""Offline tests for matrix.py (no game; the bench.py runner is faked).
|
||||
|
||||
python3 -m unittest discover -s analysis/client-perf/harness/tests
|
||||
"""
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
|
||||
import matrix # noqa: E402
|
||||
|
||||
MINI = """
|
||||
[defaults]
|
||||
reps = 3
|
||||
warmup = 15
|
||||
capture = 30
|
||||
display = "gamescope"
|
||||
res = "1280x720"
|
||||
world = "v1"
|
||||
shader = "off"
|
||||
dh = "on"
|
||||
launch_overhead_s = 60
|
||||
|
||||
[worlds]
|
||||
v1 = "{tmp}/worlds/v1.zip"
|
||||
s4 = "{tmp}/worlds/s4-*.zip"
|
||||
|
||||
[trials]
|
||||
gnetum = "{tmp}/trials/gnetum-*.jar"
|
||||
|
||||
[[missing_variants]]
|
||||
id = "C1-fa"
|
||||
blocks = ["B8"]
|
||||
contents = "C1 + Fresh Animations"
|
||||
|
||||
[[block]]
|
||||
id = "B0"
|
||||
rule = "noise"
|
||||
sessions = [{{ variant = "C0", plan = "s1", repeat = 3 }}]
|
||||
|
||||
[[block]]
|
||||
id = "B3"
|
||||
rule = "dh"
|
||||
sessions = [
|
||||
{{ variant = "C1", plan = "s1,s5" }},
|
||||
{{ variant = "C1", plan = "s1,s5", dh = "off", tag = "dh-off" }},
|
||||
{{ variant = "C1", plan = "s2", jvm = "J2", heap = 6144, world = "s4", tag = "x" }},
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B6"
|
||||
rule = "mf"
|
||||
interleave = true
|
||||
sessions = [
|
||||
{{ variant = "C1", plan = "s1", repeat = 2 }},
|
||||
{{ variant = "mf", plan = "s1", repeat = 2 }},
|
||||
]
|
||||
|
||||
[[block]]
|
||||
id = "B8"
|
||||
rule = "entities"
|
||||
sessions = [{{ variant = "C1-fa", plan = "s3" }}]
|
||||
|
||||
[[block]]
|
||||
id = "B12b"
|
||||
rule = "trials"
|
||||
sessions = [{{ variant = "C1", plan = "s1", add_mods = ["gnetum"], disable_mods = ["a-*.jar"], tag = "g" }}]
|
||||
|
||||
[[block]]
|
||||
id = "B4h"
|
||||
rule = "heap"
|
||||
params = ["gc"]
|
||||
sessions = [{{ variant = "C1", plan = "s5", jvm = "${{gc}}", heap = 4096 }}]
|
||||
|
||||
[[block]]
|
||||
id = "B13"
|
||||
rule = "profile"
|
||||
sessions = [{{ variant = "C1", plan = "s1", manual = true }}]
|
||||
|
||||
[[block]]
|
||||
id = "B5"
|
||||
rule = "sable"
|
||||
deferred = true
|
||||
sessions = [{{ variant = "C1", plan = "s4" }}]
|
||||
|
||||
[[block]]
|
||||
id = "B14"
|
||||
rule = "spawn"
|
||||
manual = true
|
||||
sessions = []
|
||||
"""
|
||||
|
||||
|
||||
class Fixture(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.out = self.tmp / "out"
|
||||
for v in ("C0", "C1", "mf"):
|
||||
(self.out / v).mkdir(parents=True)
|
||||
self.mods = self.tmp / "mods"
|
||||
self.mods.mkdir()
|
||||
(self.mods / "a-1.jar").write_bytes(b"x")
|
||||
(self.tmp / "worlds").mkdir()
|
||||
(self.tmp / "worlds" / "v1.zip").write_bytes(b"z")
|
||||
(self.tmp / "worlds" / "s4-4.zip").write_bytes(b"z")
|
||||
(self.tmp / "trials").mkdir()
|
||||
(self.tmp / "trials" / "gnetum-1.0.jar").write_bytes(b"j")
|
||||
self.path = self.tmp / "matrix.toml"
|
||||
self.path.write_text(MINI.format(tmp=self.tmp))
|
||||
self.m = matrix.load(self.path, self.out)
|
||||
self.state_path = self.tmp / "state.json"
|
||||
self.calls = []
|
||||
|
||||
def runner(self, codes=None):
|
||||
codes = list(codes or [])
|
||||
|
||||
def fake(argv):
|
||||
self.calls.append(argv)
|
||||
return codes.pop(0) if codes else 0
|
||||
return fake
|
||||
|
||||
def run_blocks(self, ids, codes=None, **kw):
|
||||
state = matrix.State(self.state_path)
|
||||
with contextlib.redirect_stdout(io.StringIO()), \
|
||||
contextlib.redirect_stderr(io.StringIO()) as err:
|
||||
rc = matrix.run_blocks(self.m, ids, state, self.mods, runner=self.runner(codes),
|
||||
out_dir=self.out, **kw)
|
||||
return rc, state, err.getvalue()
|
||||
|
||||
|
||||
class Loading(Fixture):
|
||||
def test_real_matrix_validates(self):
|
||||
m = matrix.load() # the shipped matrix.toml against the real out/ dir
|
||||
ids = [b["id"] for b in m["block"]]
|
||||
for want in ["B0", "B1", "B2", "B3", "B4", "B5", "B6", "B7", "B8", "B9", "B10",
|
||||
"B11", "B12a", "B12b", "B13", "B14"]:
|
||||
self.assertIn(want, ids)
|
||||
bm = matrix.block_map(m)
|
||||
self.assertTrue(bm["B5"]["deferred"])
|
||||
self.assertTrue(bm["B14"]["manual"])
|
||||
b0 = matrix.expand(m, bm["B0"])
|
||||
self.assertEqual([(s["variant"], s["plan"]) for s in b0], [("C0", ["s1"])] * 3)
|
||||
|
||||
def test_real_matrix_emits_only_bench_flags(self):
|
||||
m = matrix.load()
|
||||
self.assertEqual((m["defaults"]["display"], m["defaults"]["res"]),
|
||||
("gamescope-headless", "1920x1080"))
|
||||
params = {"gc": "J1", "scene": "s2"}
|
||||
n = 0
|
||||
for b in m["block"]:
|
||||
for s in matrix.expand(m, b, params):
|
||||
argv, _ = matrix.bench_argv(m, s)
|
||||
self.assertEqual(argv[0], "session")
|
||||
flags = {a for a in argv[1:] if a.startswith("--")}
|
||||
self.assertLessEqual(flags, matrix.BENCH_SESSION_FLAGS, (b["id"], argv))
|
||||
n += 1
|
||||
self.assertGreater(n, 50)
|
||||
|
||||
def test_fa_and_leaves_variants_generated(self):
|
||||
m = matrix.load()
|
||||
missing = matrix.missing_variant_ids(m)
|
||||
for v in ("C1-fa", "C1-fa-ext", "emf-lod20-fa", "emf-lod8-fa", "C1-bl"):
|
||||
self.assertNotIn(v, missing)
|
||||
self.assertTrue((matrix.OUT_DIR / v / "options.txt").is_file(), v)
|
||||
|
||||
def test_validation_errors(self):
|
||||
d = tomllib.loads(MINI.format(tmp=self.tmp))
|
||||
d["block"][0]["sessions"][0]["plan"] = "s5,s1"
|
||||
d["block"][0]["sessions"][0]["typo"] = 1
|
||||
d["block"][1]["sessions"].append({"variant": "nope", "plan": "s6"})
|
||||
d["block"][1]["sessions"].append({"variant": "C1", "plan": "s1", "dh": "off",
|
||||
"tag": "dh-off"})
|
||||
d["block"][4]["sessions"][0]["add_mods"] = ["unknown"]
|
||||
d["block"][5]["sessions"][0]["jvm"] = "${other}"
|
||||
d["block"][0]["sessions"][0]["world"] = "nowhere"
|
||||
d["defaults"]["display"] = "tv"
|
||||
errs = "\n".join(matrix.validate(d, self.out))
|
||||
for frag in ["S1->S5 order", "unknown key 'typo'", "variant 'nope'",
|
||||
"unknown scene 's6'", "distinct tag", "not in [trials]",
|
||||
"${other} not declared", "world 'nowhere'", "display 'tv'"]:
|
||||
self.assertIn(frag, errs)
|
||||
|
||||
def test_missing_variant_is_valid_but_blocked(self):
|
||||
s = matrix.expand(self.m, matrix.block_map(self.m)["B8"])[0]
|
||||
self.assertTrue(matrix.variant_missing(s, self.out))
|
||||
|
||||
|
||||
class Commands(Fixture):
|
||||
def test_argv(self):
|
||||
b3 = matrix.expand(self.m, matrix.block_map(self.m)["B3"])
|
||||
argv, miss = matrix.bench_argv(self.m, b3[1])
|
||||
self.assertEqual(miss, [])
|
||||
self.assertEqual(argv[:9], ["session", "--variant", "C1", "--plan", "s1,s5",
|
||||
"--reps", "3", "--label", "B3.dh-off"])
|
||||
self.assertIn("--dh", argv)
|
||||
self.assertEqual(argv[argv.index("--dh") + 1], "off")
|
||||
self.assertEqual(argv[argv.index("--world-zip") + 1], str(self.tmp / "worlds/v1.zip"))
|
||||
argv, _ = matrix.bench_argv(self.m, b3[2])
|
||||
for flag, val in [("--jvm", "J2"), ("--heap", "6144"), ("--display", "gamescope"),
|
||||
("--res", "1280x720"), ("--shader", "off"),
|
||||
("--world-zip", str(self.tmp / "worlds/s4-4.zip"))]:
|
||||
self.assertEqual(argv[argv.index(flag) + 1], val)
|
||||
|
||||
def test_trials_and_disable(self):
|
||||
s = matrix.expand(self.m, matrix.block_map(self.m)["B12b"])[0]
|
||||
argv, miss = matrix.bench_argv(self.m, s)
|
||||
self.assertEqual(argv[argv.index("--add-mods") + 1],
|
||||
str(self.tmp / "trials/gnetum-1.0.jar"))
|
||||
self.assertEqual(argv[argv.index("--disable-mods") + 1], "a-*.jar")
|
||||
(self.tmp / "trials" / "gnetum-1.0.jar").unlink()
|
||||
_, miss = matrix.bench_argv(self.m, s)
|
||||
self.assertTrue(any("gnetum" in x for x in miss))
|
||||
|
||||
def test_live_world_and_overrides(self):
|
||||
s = matrix.expand(self.m, matrix.block_map(self.m)["B0"],
|
||||
overrides={"display": "desktop", "res": None})[0]
|
||||
argv, _ = matrix.bench_argv(self.m, s, live_world=True)
|
||||
self.assertNotIn("--world-zip", argv)
|
||||
self.assertEqual(argv[argv.index("--display") + 1], "desktop")
|
||||
self.assertEqual(argv[argv.index("--res") + 1], "1280x720")
|
||||
|
||||
def test_interleave_and_params(self):
|
||||
bm = matrix.block_map(self.m)
|
||||
self.assertEqual([s["variant"] for s in matrix.expand(self.m, bm["B6"])],
|
||||
["C1", "mf", "C1", "mf"])
|
||||
s = matrix.expand(self.m, bm["B4h"])[0]
|
||||
self.assertEqual(matrix.unresolved(s), ["gc"])
|
||||
s = matrix.expand(self.m, bm["B4h"], params={"gc": "J2"})[0]
|
||||
self.assertEqual(matrix.unresolved(s), [])
|
||||
self.assertEqual(s["jvm"], "J2")
|
||||
|
||||
def test_seconds(self):
|
||||
b3 = matrix.expand(self.m, matrix.block_map(self.m)["B3"])
|
||||
self.assertEqual(matrix.session_seconds(b3[0], 60), 60 + 10 + 3 * 45 + 3 * 130)
|
||||
|
||||
|
||||
class ShaderOptions(Fixture):
|
||||
def b7(self, **extra):
|
||||
d = tomllib.loads(MINI.format(tmp=self.tmp))
|
||||
sess = {"variant": "C1", "plan": "s1,s4", "shader": "Pack.zip",
|
||||
"shader_profile": "low", "tag": "p-low", **extra}
|
||||
d["block"].append({"id": "B7", "rule": "shaders", "sessions": [sess]})
|
||||
return d
|
||||
|
||||
def test_emitted(self):
|
||||
f = self.tmp / "Pack.low.txt"
|
||||
f.write_text("A=1\n")
|
||||
d = self.b7(shader_options=str(f))
|
||||
self.assertEqual(matrix.validate(d, self.out), [])
|
||||
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
|
||||
argv, miss = matrix.bench_argv(d, s)
|
||||
self.assertEqual(miss, [])
|
||||
self.assertEqual(argv[argv.index("--shader") + 1], "Pack.zip")
|
||||
self.assertEqual(argv[argv.index("--shader-options") + 1], str(f))
|
||||
self.assertIn("--shader-options", matrix.BENCH_SESSION_FLAGS)
|
||||
|
||||
def test_missing_file_and_relative_path(self):
|
||||
d = self.b7(shader_options="shader-profiles/does-not-exist.txt")
|
||||
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
|
||||
argv, miss = matrix.bench_argv(d, s)
|
||||
self.assertEqual(argv[argv.index("--shader-options") + 1],
|
||||
str(matrix.HERE / "shader-profiles/does-not-exist.txt"))
|
||||
self.assertTrue(any("shader options" in x for x in miss))
|
||||
|
||||
def test_needs_shader(self):
|
||||
d = self.b7(shader_options="x.txt", shader="off")
|
||||
self.assertIn("shader_options needs shader", "\n".join(matrix.validate(d, self.out)))
|
||||
|
||||
def test_spec_hash_tracks_file_content(self):
|
||||
f = self.tmp / "Pack.low.txt"
|
||||
f.write_text("A=1\n")
|
||||
d = self.b7(shader_options=str(f))
|
||||
s = matrix.expand(d, matrix.block_map(d)["B7"])[0]
|
||||
argv, _ = matrix.bench_argv(d, s)
|
||||
h1 = matrix.spec_hash(argv, s)
|
||||
f.write_text("A=2\n")
|
||||
self.assertNotEqual(h1, matrix.spec_hash(argv, s))
|
||||
# sessions without shader_options keep the original hash formula (resume state)
|
||||
b0 = matrix.expand(self.m, matrix.block_map(self.m)["B0"])[0]
|
||||
a0, _ = matrix.bench_argv(self.m, b0)
|
||||
blob = json.dumps({"argv": a0, "manual": False}, sort_keys=True)
|
||||
import hashlib
|
||||
self.assertEqual(matrix.spec_hash(a0, b0),
|
||||
hashlib.sha256(blob.encode()).hexdigest()[:16])
|
||||
|
||||
def test_real_b7_automated(self):
|
||||
m = matrix.load()
|
||||
rows = matrix.expand(m, matrix.block_map(m)["B7"])
|
||||
shaded = [s for s in rows if s["shader"] != "off"]
|
||||
self.assertEqual(len(shaded), 4)
|
||||
for s in rows:
|
||||
self.assertFalse(s.get("manual"), s["label"])
|
||||
for s in shaded:
|
||||
argv, miss = matrix.bench_argv(m, s)
|
||||
self.assertEqual([x for x in miss if "shader options" in x], [], s["label"])
|
||||
self.assertTrue(Path(argv[argv.index("--shader-options") + 1]).is_file())
|
||||
|
||||
|
||||
class Resume(Fixture):
|
||||
def test_runs_then_skips(self):
|
||||
rc, state, _ = self.run_blocks(["B0"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertEqual(len(self.calls), 3)
|
||||
self.assertEqual(state.data["b0"]["mods_hash"], matrix.mods_hash(self.mods))
|
||||
saved = json.loads(self.state_path.read_text())
|
||||
self.assertEqual({e["status"] for e in saved["sessions"].values()}, {"ok"})
|
||||
self.calls.clear()
|
||||
rc, _, _ = self.run_blocks(["B0"])
|
||||
self.assertEqual((rc, self.calls), (0, []))
|
||||
|
||||
def test_mod_change_invalidates_and_warns(self):
|
||||
self.run_blocks(["B0"])
|
||||
(self.mods / "new-mod.jar").write_bytes(b"y")
|
||||
self.calls.clear()
|
||||
rc, _, err = self.run_blocks(["B3"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("B0 MUST BE RE-RUN", err)
|
||||
self.calls.clear()
|
||||
rc, _, _ = self.run_blocks(["B0"])
|
||||
self.assertEqual(len(self.calls), 3) # B0 sessions are stale under the new set
|
||||
|
||||
def test_warns_without_b0(self):
|
||||
_, _, err = self.run_blocks(["B3"])
|
||||
self.assertIn("B0 (noise floor) has not been completed", err)
|
||||
|
||||
def test_spec_change_reruns(self):
|
||||
self.run_blocks(["B0"])
|
||||
self.calls.clear()
|
||||
self.run_blocks(["B0"], live_world=True) # different command -> not done
|
||||
self.assertEqual(len(self.calls), 3)
|
||||
|
||||
def test_retry_once_then_ok(self):
|
||||
rc, state, _ = self.run_blocks(["B0"], codes=[1, 0])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertEqual(len(self.calls), 4)
|
||||
self.assertEqual(state.data["sessions"]["B0#0"]["attempts"], 2)
|
||||
|
||||
def test_stop_after_two_failures(self):
|
||||
rc, state, _ = self.run_blocks(["B0"], codes=[1, 1])
|
||||
self.assertEqual(rc, 1)
|
||||
self.assertEqual(len(self.calls), 2)
|
||||
self.assertEqual(state.data["sessions"]["B0#0"]["status"], "failed")
|
||||
self.assertNotIn("B0#1", state.data["sessions"])
|
||||
self.calls.clear()
|
||||
rc, _, _ = self.run_blocks(["B0"]) # resume retries the failed one
|
||||
self.assertEqual((rc, len(self.calls)), (0, 3))
|
||||
|
||||
def test_dry_run_runs_nothing(self):
|
||||
rc, _, _ = self.run_blocks(["B0", "B3"], dry_run=True)
|
||||
self.assertEqual((rc, self.calls), (0, []))
|
||||
self.assertFalse(self.state_path.exists())
|
||||
|
||||
def test_blocked_aborts_or_skips(self):
|
||||
rc, _, _ = self.run_blocks(["B8"])
|
||||
self.assertEqual((rc, self.calls), (2, []))
|
||||
rc, _, _ = self.run_blocks(["B4h"]) # unresolved ${gc}
|
||||
self.assertEqual((rc, self.calls), (2, []))
|
||||
rc, _, _ = self.run_blocks(["B8", "B6"], skip_blocked=True)
|
||||
self.assertEqual((rc, len(self.calls)), (0, 4))
|
||||
|
||||
def test_manual_deferred_skipped(self):
|
||||
rc, _, _ = self.run_blocks(["B13", "B5", "B14"])
|
||||
self.assertEqual((rc, self.calls), (0, []))
|
||||
rc, _, _ = self.run_blocks(["B13", "B5"], include_manual=True, include_deferred=True)
|
||||
self.assertEqual(len(self.calls), 2)
|
||||
|
||||
def test_mods_restored_check(self):
|
||||
def mutating(argv):
|
||||
self.calls.append(argv)
|
||||
(self.mods / f"leak{len(self.calls)}.jar").write_bytes(b"l")
|
||||
return 0
|
||||
state = matrix.State(self.state_path)
|
||||
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
||||
rc = matrix.run_blocks(self.m, ["B0"], state, self.mods, runner=mutating,
|
||||
out_dir=self.out)
|
||||
self.assertEqual((rc, len(self.calls)), (1, 1))
|
||||
|
||||
def test_unknown_block(self):
|
||||
with self.assertRaises(matrix.MatrixError):
|
||||
self.run_blocks(["B99"])
|
||||
|
||||
|
||||
class Cli(unittest.TestCase):
|
||||
def test_list_and_plan(self):
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
self.assertEqual(matrix.main(["list"]), 0)
|
||||
self.assertEqual(matrix.main(["plan", "B4h", "--param", "gc=J1",
|
||||
"--display", "desktop"]), 0)
|
||||
out = buf.getvalue()
|
||||
self.assertIn("| B0 | noise floor | 3 |", out)
|
||||
self.assertIn("--jvm J1 --heap 4096", out)
|
||||
self.assertIn("--display desktop", out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Offline tests for gen_shader_profiles.py (synthetic packs; no game)."""
|
||||
import contextlib
|
||||
import io
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(HERE.parent))
|
||||
|
||||
import gen_shader_profiles as g # noqa: E402
|
||||
|
||||
PROPS = (
|
||||
"# Profiles\r\n"
|
||||
" profile.BASE = A=1 !B C D:4 \\\r\n"
|
||||
" E=x\r\n"
|
||||
"profile.LOW = profile.BASE A=2 !C\r\n"
|
||||
"profile.TOP = profile.LOW B E=y\r\n"
|
||||
"screen=<profile> A B\r\n"
|
||||
"#if A == 1\r\n"
|
||||
" program.world0/shadow.enabled=false\r\n"
|
||||
"#endif\r\n"
|
||||
)
|
||||
|
||||
|
||||
class Parse(unittest.TestCase):
|
||||
def test_tokens_and_continuation(self):
|
||||
p = g.parse_profiles(PROPS)
|
||||
self.assertEqual(sorted(p), ["BASE", "LOW", "TOP"])
|
||||
self.assertEqual(p["BASE"], ["A=1", "!B", "C", "D:4", "E=x"])
|
||||
|
||||
def test_resolve_semantics(self):
|
||||
p = g.parse_profiles(PROPS)
|
||||
opts, prog = g.resolve("BASE", p)
|
||||
self.assertEqual(opts, {"A": "1", "B": "false", "C": "true", "D": "4", "E": "x"})
|
||||
self.assertEqual(prog, [])
|
||||
opts, _ = g.resolve("TOP", p) # includes recurse; later tokens win
|
||||
self.assertEqual(opts, {"D": "4", "A": "2", "C": "false", "B": "true", "E": "y"})
|
||||
|
||||
def test_errors(self):
|
||||
p = {"X": ["profile.Y"], "Y": ["profile.X"]}
|
||||
with self.assertRaisesRegex(g.ProfileError, "cycle"):
|
||||
g.resolve("X", p)
|
||||
with self.assertRaisesRegex(g.ProfileError, "unknown profile"):
|
||||
g.resolve("NOPE", g.parse_profiles(PROPS))
|
||||
with self.assertRaisesRegex(g.ProfileError, "conditional"):
|
||||
g.parse_profiles("#ifdef MC_OS_MAC\nprofile.M = A=1\n#endif\n")
|
||||
with self.assertRaisesRegex(g.ProfileError, "escaping"):
|
||||
g.render({"A": "has space"})
|
||||
|
||||
def test_programs_collected(self):
|
||||
_, prog = g.resolve("P", {"P": ["!program.world0/shadow", "A"]})
|
||||
self.assertEqual(prog, ["world0/shadow"])
|
||||
|
||||
|
||||
class Generate(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.zip = self.tmp / "Pack_1.0.zip"
|
||||
with zipfile.ZipFile(self.zip, "w") as z:
|
||||
z.writestr("shaders/shaders.properties", PROPS)
|
||||
z.writestr("shaders/lib/settings.glsl",
|
||||
"#define A 1 // [1 2]\n//#define B\n#define C\nconst int D = 4; // [4 8]\n")
|
||||
|
||||
def test_file_and_unknowns(self):
|
||||
text, n, unknown = g.generate(self.zip, "LOW")
|
||||
self.assertEqual(n, 5)
|
||||
self.assertEqual(unknown, ["E"])
|
||||
body = [ln for ln in text.splitlines() if not ln.startswith("#")]
|
||||
self.assertEqual(body, ["B=false", "D=4", "E=x", "A=2", "C=false"])
|
||||
self.assertIn("profile.LOW of Pack_1.0.zip", text)
|
||||
self.assertEqual(g.out_name("Pack_1.0.zip", "LOW"), "Pack_1.0.LOW.txt")
|
||||
|
||||
def test_program_disabling_profile_rejected(self):
|
||||
z2 = self.tmp / "P2.zip"
|
||||
with zipfile.ZipFile(z2, "w") as z:
|
||||
z.writestr("shaders/shaders.properties", "profile.X = !program.composite A\n")
|
||||
with self.assertRaisesRegex(g.ProfileError, "disables programs"):
|
||||
g.generate(z2, "X")
|
||||
|
||||
def test_cli_write_then_check(self):
|
||||
out = self.tmp / "out"
|
||||
args = ["--packs", str(self.tmp), "--out", str(out), "--target", "Pack_1.0.zip:TOP"]
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
self.assertEqual(g.main(args), 0)
|
||||
self.assertTrue((out / "Pack_1.0.TOP.txt").is_file())
|
||||
self.assertEqual(g.main(args + ["--check"]), 0)
|
||||
(out / "Pack_1.0.TOP.txt").write_text("stale\n")
|
||||
self.assertEqual(g.main(args + ["--check"]), 1)
|
||||
|
||||
def test_matrix_targets(self):
|
||||
t = g.matrix_targets()
|
||||
self.assertIn(("MakeUp-UltraFast-9.5f.zip", "medium"), t)
|
||||
self.assertIn(("ComplementaryReimagined_r5.9.3.zip", "LOW"), t)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -11,7 +11,8 @@ pinned jar's source (see README.md, "Key provenance").
|
||||
Output: out/<variant>/<path relative to the game dir>, plus out/MANIFEST.md.
|
||||
|
||||
python3 build.py # from this directory; stdlib only
|
||||
python3 build.py --ship # also copy T-default into the repo (see SHIP), then
|
||||
python3 build.py --ship # also copy T-default into the repo (see SHIP) and the
|
||||
# player tiers into config/onion-perf/ (see PERF), then
|
||||
# run `packwiz refresh` from the repo root
|
||||
"""
|
||||
import json
|
||||
@@ -29,10 +30,10 @@ REPO = HERE.parents[2]
|
||||
# What `--ship` copies from out/T-default into the repo, and where.
|
||||
# Default Options seeds these client-side only, and only if the file is absent
|
||||
# (see README §1). Not shipped: sable-client.toml and sodium-options.json (no change
|
||||
# from upstream). modernfix-mixins.properties is held back until bench block B6
|
||||
# passes; when it does, add it here with destination "config/modernfix-mixins.properties"
|
||||
# (enforced packwiz file, because ModernFix reads it before Default Options runs) and
|
||||
# re-include it in .packwizignore.
|
||||
# from upstream). modernfix-mixins.properties ships as an *enforced* packwiz file
|
||||
# (ModernFix reads it before Default Options runs): bench B6 (proxy) showed
|
||||
# dynamic_resources -7.6 s startup and better S1/S2 frametimes; owner waived the
|
||||
# missing-texture check (2026-10-18) - report issues from play.
|
||||
DO = "config/defaultoptions"
|
||||
SHIP = {
|
||||
"options.txt": f"{DO}/options.txt",
|
||||
@@ -45,8 +46,18 @@ SHIP = {
|
||||
f"{DO}/extra/config/sound_physics_remastered/soundphysics.properties",
|
||||
"config/sound_physics_remastered/sound_rates.properties":
|
||||
f"{DO}/extra/config/sound_physics_remastered/sound_rates.properties",
|
||||
"config/modernfix-mixins.properties": "config/modernfix-mixins.properties",
|
||||
}
|
||||
|
||||
# Player tiers `--ship` also writes to config/onion-perf/<tier>/ (paths relative to
|
||||
# the game dir) for onion-perf-setup.sh/.ps1, which existing players run to apply a
|
||||
# tier over their configs. A tier carries only the files its layers tune, so
|
||||
# untuned files (sable-client.toml; sodium-options.json except on deck) keep the
|
||||
# player's own settings. modernfix-mixins.properties is enforced by packwiz already.
|
||||
PERF = "config/onion-perf"
|
||||
PERF_TIERS = {"deck": "T-deck", "mid": "T-default", "high": "T-high"} # mid == pack default
|
||||
PERF_SKIP = {"config/modernfix-mixins.properties"}
|
||||
|
||||
# game-dir path -> (base file, format, may_append_new_keys)
|
||||
FILES = {
|
||||
"options.txt": (TEMPLATES / "options.txt", "options", False),
|
||||
@@ -190,8 +201,27 @@ BENCH = {
|
||||
}
|
||||
|
||||
|
||||
def c1(extra):
|
||||
return [DEFAULT, extra]
|
||||
# Resource packs (names = zips in the bench instance's resourcepacks/). Every bench
|
||||
# variant pins the list: apply_variant.py merges options.txt key by key, so a variant
|
||||
# without the key would keep the previous session's packs. Later entries win.
|
||||
FA = "file/FreshAnimations_v1.10.4.zip"
|
||||
FA_EXT = "file/FA+All_Extensions-v1.8.1.zip"
|
||||
BETTER_LEAVES = "file/Better-Leaves-9.6.zip"
|
||||
|
||||
|
||||
def packs(*extra):
|
||||
return {"options.txt": {"resourcePacks": json.dumps(["vanilla", "mod_resources", *extra],
|
||||
separators=(",", ":"))}}
|
||||
|
||||
|
||||
PACKS_BASE = packs()
|
||||
# Keys the bench layers may add to a base that lacks them (the options.txt template
|
||||
# has no resourcePacks line; tiers never set it, so shipped output is unaffected).
|
||||
ALLOW_NEW = {"options.txt": {"resourcePacks"}}
|
||||
|
||||
|
||||
def c1(*extra):
|
||||
return [DEFAULT, PACKS_BASE, *extra]
|
||||
|
||||
|
||||
VARIANTS = {
|
||||
@@ -201,10 +231,10 @@ VARIANTS = {
|
||||
"T-mid": ([DEFAULT], False),
|
||||
"T-high": ([DEFAULT, HIGH], False),
|
||||
# bench: composites
|
||||
"C0": ([], True),
|
||||
"C1": ([DEFAULT], True),
|
||||
"C1-deck": ([DEFAULT, DECK], True),
|
||||
"C1-high": ([DEFAULT, HIGH], True),
|
||||
"C0": ([PACKS_BASE], True),
|
||||
"C1": (c1(), True),
|
||||
"C1-deck": (c1(DECK), True),
|
||||
"C1-high": (c1(HIGH), True),
|
||||
# bench: Distant Horizons
|
||||
"dh-r256": (c1({"config/DistantHorizons.toml": {(DH_Q, "lodChunkRenderDistanceRadius"): 256}}), True),
|
||||
"dh-r192": (c1({"config/DistantHorizons.toml": {(DH_Q, "lodChunkRenderDistanceRadius"): 192}}), True),
|
||||
@@ -226,6 +256,15 @@ VARIANTS = {
|
||||
"modelUpdateFrequency": "Average"}}), True),
|
||||
"emf-lod8": (c1({"config/entity_model_features.json": {"animationLODDistance": 8,
|
||||
"retainDetailOnLowFps": False}}), True),
|
||||
"C1-fa": (c1(packs(FA)), True),
|
||||
"C1-fa-ext": (c1(packs(FA, FA_EXT)), True),
|
||||
"emf-lod20-fa": (c1({"config/entity_model_features.json": {"animationLODDistance": 20,
|
||||
"modelUpdateFrequency": "Average"}},
|
||||
packs(FA)), True),
|
||||
"emf-lod8-fa": (c1({"config/entity_model_features.json": {"animationLODDistance": 8,
|
||||
"retainDetailOnLowFps": False}},
|
||||
packs(FA)), True),
|
||||
"C1-bl": (c1(packs(BETTER_LEAVES)), True),
|
||||
"ldl-fancy": (c1({"config/lambdynlights.toml": {("", "mode"): "fancy"}}), True),
|
||||
"ldl-fastest": (c1({"config/lambdynlights.toml": {("", "mode"): "fastest",
|
||||
("light_sources", "entities"): False}}), True),
|
||||
@@ -290,7 +329,8 @@ def apply_kv(text, ov, name, sep, append):
|
||||
sval = toml_val(val) if isinstance(val, bool) else str(val)
|
||||
rx = re.compile(rf"^{re.escape(key)}{re.escape(sep)}")
|
||||
hits = [i for i, ln in enumerate(lines) if rx.match(ln)]
|
||||
if len(hits) > 1 or (not hits and not append):
|
||||
may_add = append if isinstance(append, bool) else key in append
|
||||
if len(hits) > 1 or (not hits and not may_add):
|
||||
raise SystemExit(f"{name}: {key}: {len(hits)} matches in base")
|
||||
if hits:
|
||||
lines[hits[0]] = f"{key}{sep}{sval}"
|
||||
@@ -327,7 +367,7 @@ def render(layers, name):
|
||||
if fmt == "json":
|
||||
return apply_json(text, merged, name), merged
|
||||
sep = ":" if fmt == "options" else "="
|
||||
return apply_kv(text, merged, name, sep, append), merged
|
||||
return apply_kv(text, merged, name, sep, append or ALLOW_NEW.get(name, set())), merged
|
||||
|
||||
|
||||
def main():
|
||||
@@ -354,6 +394,15 @@ def main():
|
||||
(REPO / dst).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(OUT / "T-default" / src, REPO / dst)
|
||||
print(f"shipped {len(SHIP)} files to {REPO / DO}; now run `packwiz refresh`")
|
||||
shutil.rmtree(REPO / PERF, ignore_errors=True)
|
||||
for tier, vid in PERF_TIERS.items():
|
||||
layers, _ = VARIANTS[vid]
|
||||
names = sorted({n for layer in layers for n in layer} - PERF_SKIP)
|
||||
for name in names:
|
||||
dst = REPO / PERF / tier / name
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(OUT / vid / name, dst)
|
||||
print(f"shipped tier {tier} ({vid}): {len(names)} files to {REPO / PERF / tier}")
|
||||
|
||||
|
||||
main()
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Tests for onion-perf-setup.sh (and .ps1 when pwsh exists) against temp instance dirs.
|
||||
|
||||
Needs `python3 build.py --ship` to have generated config/onion-perf/ in the repo.
|
||||
Run: python3 -m unittest discover -s analysis/client-perf/proposed-configs/tests
|
||||
"""
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[4]
|
||||
PERF = REPO / "config" / "onion-perf"
|
||||
SCRIPTS = ["onion-perf-setup.sh", "onion-perf-setup.ps1", "onion-perf-setup.bat"]
|
||||
|
||||
OLD_OPTIONS = (
|
||||
"version:3955\n"
|
||||
"maxFps:144\n"
|
||||
"enableVsync:false\n"
|
||||
"renderDistance:12\n"
|
||||
"mipmapLevels:4\n"
|
||||
"soundCategory_master:0.5\n"
|
||||
"key_key.attack:key.mouse.left\n"
|
||||
"key_key.jump:key.keyboard.space\n"
|
||||
'resourcePacks:["vanilla","file/MyPack.zip"]\n'
|
||||
)
|
||||
|
||||
|
||||
def runners():
|
||||
"""(name, argv prefix) for every available POSIX-sh implementation, plus pwsh."""
|
||||
out = [("sh", ["sh"])]
|
||||
if shutil.which("dash"):
|
||||
out.append(("dash", ["dash"]))
|
||||
if shutil.which("zsh"):
|
||||
out.append(("zsh-emulate-sh", ["zsh", "--emulate", "sh"]))
|
||||
return out
|
||||
|
||||
|
||||
PWSH = shutil.which("pwsh")
|
||||
|
||||
|
||||
def tier_options(tier):
|
||||
kv = {}
|
||||
for line in (PERF / tier / "options.txt").read_text().splitlines():
|
||||
k, _, v = line.partition(":")
|
||||
kv[k] = v
|
||||
return kv
|
||||
|
||||
|
||||
class Base:
|
||||
"""Mixin; subclasses set self.cmd(script, args) -> argv."""
|
||||
|
||||
def setUp(self):
|
||||
if not (PERF / "mid").is_dir():
|
||||
self.skipTest("config/onion-perf not generated (run build.py --ship)")
|
||||
self.tmp = Path(tempfile.mkdtemp())
|
||||
self.game = self.tmp / "instance"
|
||||
(self.game / "config").mkdir(parents=True)
|
||||
shutil.copytree(PERF, self.game / "config" / "onion-perf")
|
||||
for s in SCRIPTS:
|
||||
shutil.copy(REPO / s, self.game / s)
|
||||
(self.game / "config" / "DistantHorizons.toml").write_text("old dh\n")
|
||||
(self.game / "config" / "unrelated.toml").write_text("keep me\n")
|
||||
(self.game / "options.txt").write_text(OLD_OPTIONS)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tmp, ignore_errors=True)
|
||||
|
||||
def run_setup(self, *args, cwd=None):
|
||||
return subprocess.run(self.cmd(*args), cwd=cwd or self.tmp, stdin=subprocess.DEVNULL,
|
||||
capture_output=True, text=True, timeout=60)
|
||||
|
||||
def backups(self):
|
||||
d = self.game / "onion-perf-backup"
|
||||
return sorted(d.iterdir()) if d.is_dir() else []
|
||||
|
||||
def test_tier_copy_backup_and_merge(self):
|
||||
for tier in ("deck", "mid", "high"):
|
||||
with self.subTest(tier=tier):
|
||||
if tier != "deck":
|
||||
self.tearDown()
|
||||
self.setUp()
|
||||
r = self.run_setup(tier, "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
|
||||
src = PERF / tier
|
||||
for f in (src / "config").rglob("*"):
|
||||
if f.is_file():
|
||||
rel = f.relative_to(src)
|
||||
self.assertEqual((self.game / rel).read_bytes(), f.read_bytes(), rel)
|
||||
self.assertEqual((self.game / "config/unrelated.toml").read_text(), "keep me\n")
|
||||
# backup holds the pre-run files
|
||||
(b,) = self.backups()
|
||||
self.assertEqual((b / "config/DistantHorizons.toml").read_text(), "old dh\n")
|
||||
self.assertEqual((b / "options.txt").read_text(), OLD_OPTIONS)
|
||||
self.assertFalse((b / "config/unrelated.toml").exists())
|
||||
# options merged key by key
|
||||
want = tier_options(tier)
|
||||
lines = (self.game / "options.txt").read_text().splitlines()
|
||||
kv = dict(line.partition(":")[::2] for line in lines)
|
||||
for k in ("version", "maxFps", "enableVsync"):
|
||||
self.assertEqual(kv[k], dict(x.partition(":")[::2]
|
||||
for x in OLD_OPTIONS.splitlines())[k], k)
|
||||
for k, v in want.items():
|
||||
if k not in ("version", "maxFps", "enableVsync"):
|
||||
self.assertEqual(kv[k], v, k)
|
||||
for keep in ("soundCategory_master:0.5", "key_key.attack:key.mouse.left",
|
||||
"key_key.jump:key.keyboard.space",
|
||||
'resourcePacks:["vanilla","file/MyPack.zip"]'):
|
||||
self.assertIn(keep, lines)
|
||||
self.assertEqual(len(lines), len(set(line.partition(":")[0] for line in lines)))
|
||||
# existing lines keep their positions; appended keys go at the end
|
||||
self.assertEqual(lines[0], "version:3955")
|
||||
self.assertTrue(lines[3].startswith("renderDistance:"))
|
||||
if want["mipmapLevels"] != "4":
|
||||
self.assertIn(f"mipmapLevels: 4 -> {want['mipmapLevels']}", r.stdout)
|
||||
else:
|
||||
self.assertNotIn("mipmapLevels:", r.stdout)
|
||||
self.assertIn("(unset) ->", r.stdout)
|
||||
|
||||
def test_value_with_colon_replaced_whole(self):
|
||||
# split on the FIRST ':' only: a tier value containing ':' replaces as a whole
|
||||
tier_opt = self.game / "config/onion-perf/high/options.txt"
|
||||
tier_opt.write_text(tier_opt.read_text() + "onionProbe:part1:part2\n")
|
||||
(self.game / "options.txt").write_text("version:3955\nonionProbe:wrong\n")
|
||||
r = self.run_setup("high", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
lines = (self.game / "options.txt").read_text().splitlines()
|
||||
self.assertIn("onionProbe:part1:part2", lines)
|
||||
self.assertNotIn("onionProbe:wrong", lines)
|
||||
self.assertIn("onionProbe: wrong -> part1:part2", r.stdout)
|
||||
|
||||
def test_idempotent(self):
|
||||
self.assertEqual(self.run_setup("deck", "--yes").returncode, 0)
|
||||
snap = {p: p.read_bytes() for p in self.game.rglob("*") if p.is_file()
|
||||
and "onion-perf-backup" not in p.parts}
|
||||
r = self.run_setup("deck", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("nothing changed", r.stdout)
|
||||
self.assertEqual(len(self.backups()), 1)
|
||||
for p, data in snap.items():
|
||||
self.assertEqual(p.read_bytes(), data, p)
|
||||
|
||||
def test_default_tier_is_mid_when_noninteractive(self):
|
||||
r = self.run_setup("--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("tier 'mid'", r.stdout)
|
||||
self.assertEqual((self.game / "config/DistantHorizons.toml").read_bytes(),
|
||||
(PERF / "mid/config/DistantHorizons.toml").read_bytes())
|
||||
|
||||
def test_missing_options(self):
|
||||
(self.game / "options.txt").unlink()
|
||||
r = self.run_setup("high", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("options.txt not found", r.stdout)
|
||||
self.assertFalse((self.game / "options.txt").exists())
|
||||
|
||||
def test_crlf_options_preserved(self):
|
||||
(self.game / "options.txt").write_bytes(OLD_OPTIONS.replace("\n", "\r\n").encode())
|
||||
r = self.run_setup("mid", "--yes")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
data = (self.game / "options.txt").read_bytes()
|
||||
self.assertNotIn(b"\r\r", data)
|
||||
self.assertEqual(data.count(b"\n"), data.count(b"\r\n"))
|
||||
self.assertIn(b"key_key.attack:key.mouse.left\r\n", data)
|
||||
self.assertIn(b"biomeBlendRadius:1\r\n", data)
|
||||
|
||||
def test_lf_options_preserved(self):
|
||||
self.run_setup("mid", "--yes")
|
||||
self.assertNotIn(b"\r", (self.game / "options.txt").read_bytes())
|
||||
|
||||
def test_bad_tier(self):
|
||||
r = self.run_setup("ultra", "--yes")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertEqual((self.game / "config/DistantHorizons.toml").read_text(), "old dh\n")
|
||||
|
||||
def test_missing_onion_perf(self):
|
||||
shutil.rmtree(self.game / "config" / "onion-perf")
|
||||
r = self.run_setup("mid", "--yes")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("Update the modpack", r.stderr)
|
||||
self.assertEqual(self.backups(), [])
|
||||
|
||||
def test_needs_yes_when_noninteractive(self):
|
||||
r = self.run_setup("mid")
|
||||
self.assertNotEqual(r.returncode, 0)
|
||||
self.assertIn("--yes", r.stderr)
|
||||
self.assertEqual((self.game / "config/DistantHorizons.toml").read_text(), "old dh\n")
|
||||
|
||||
|
||||
def make_sh_case(name, prefix):
|
||||
class Case(Base, unittest.TestCase):
|
||||
def cmd(self, *args):
|
||||
return [*prefix, str(self.game / "onion-perf-setup.sh"), *args]
|
||||
|
||||
def test_run_from_game_dir_with_relative_path(self):
|
||||
r = subprocess.run([*prefix, "onion-perf-setup.sh", "mid", "--yes"], cwd=self.game,
|
||||
stdin=subprocess.DEVNULL, capture_output=True, text=True)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
|
||||
Case.__name__ = Case.__qualname__ = f"Sh_{name.replace('-', '_')}"
|
||||
return Case
|
||||
|
||||
|
||||
for _n, _p in runners():
|
||||
globals()[f"Sh_{_n.replace('-', '_')}"] = make_sh_case(_n, _p)
|
||||
|
||||
|
||||
@unittest.skipUnless(PWSH, "pwsh not installed: onion-perf-setup.ps1 untested here")
|
||||
class Pwsh(Base, unittest.TestCase):
|
||||
def cmd(self, *args):
|
||||
return [PWSH, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File",
|
||||
str(self.game / "onion-perf-setup.ps1"), *args]
|
||||
|
||||
|
||||
class Packaging(unittest.TestCase):
|
||||
def test_bat_is_crlf_and_calls_ps1(self):
|
||||
data = (REPO / "onion-perf-setup.bat").read_bytes()
|
||||
self.assertEqual(data.count(b"\n"), data.count(b"\r\n"))
|
||||
self.assertIn(b'-File "%~dp0onion-perf-setup.ps1" %*', data)
|
||||
|
||||
def test_scripts_ascii(self):
|
||||
for s in SCRIPTS:
|
||||
(REPO / s).read_bytes().decode("ascii")
|
||||
|
||||
def test_mid_matches_default_options(self):
|
||||
do = REPO / "config" / "defaultoptions"
|
||||
self.assertEqual((PERF / "mid/options.txt").read_bytes(),
|
||||
(do / "options.txt").read_bytes())
|
||||
for f in (PERF / "mid/config").rglob("*"):
|
||||
if f.is_file():
|
||||
rel = f.relative_to(PERF / "mid")
|
||||
self.assertEqual(f.read_bytes(), (do / "extra" / rel).read_bytes(), rel)
|
||||
self.assertFalse((PERF / "mid/config/modernfix-mixins.properties").exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,73 @@
|
||||
# Pack and environment
|
||||
|
||||
## The pack
|
||||
|
||||
- **Onion MC** — curated NeoForge modpack managed with **packwiz**; MC **1.21.1**,
|
||||
NeoForge **21.1.251**, **217 mods** at HEAD `e5e369a`.
|
||||
- The repo **doubles as the dedicated-server directory** (`/home/logan/onion-mc`).
|
||||
The live server runs from here (see `03-current-state.md`). `world/`, `libraries/`,
|
||||
`logs/`, `crash-reports/`, `*.jar` are `.packwizignore`d — never blanket-un-ignore.
|
||||
- Remote: `ssh://git@git.ljones.me:2222/logan/onion-mc.git`, branch `main`.
|
||||
- **Conventional Commits**, imperative subject, body explains *why*. Don't commit or
|
||||
push unless asked / plan calls for it.
|
||||
- packwiz lives at `~/go/bin/packwiz` (build `ef87d96`). On this machine it is on PATH.
|
||||
- Config shipping is **opt-in**: `.packwizignore` ignores `config/*`; shipping a file
|
||||
needs a negation at the **bottom** of `.packwizignore` (last match wins; negations
|
||||
must not end in `/` or packwiz SkipDirs the parent).
|
||||
- Hand-set `side = "server"` on **petprotect, c2me-neoforge, noisium** is silently
|
||||
reset by bulk `packwiz update` (single-mod update is safe).
|
||||
- Tuned defaults ship via the **Default Options** mod (`config/defaultoptions/`),
|
||||
copy-if-absent → reaches fresh installs only. Change values **only** through
|
||||
`analysis/client-perf/proposed-configs/build.py`, never by hand-editing outputs.
|
||||
- `.pi/` and `analysis/` are packwizignored — safe to commit to git, never shipped
|
||||
to players.
|
||||
|
||||
## This machine — the Phase 2 "proxy" bench box (Manjaro)
|
||||
|
||||
The three fleet machines (Steam Deck, 7700K/1060, 6700K/1080 Ti — see `phase0/`) are
|
||||
**unreachable**; all Phase 2 data so far is from this **high-tier proxy**. Label all
|
||||
results as proxy data: absolute FPS floors do not map to tier decisions.
|
||||
|
||||
| Item | Value |
|
||||
| --- | --- |
|
||||
| OS | Manjaro Linux, kernel 7.1.8 |
|
||||
| CPU | AMD Ryzen 9 9900X, 12C/24T |
|
||||
| GPU | AMD RX 7800 XT (DRI_PRIME=1 used in launch scripts) + Granite Ridge iGPU |
|
||||
| RAM | 91 GB, **no swap** (good for p99 validity) |
|
||||
| Disk | `/` 84% used, 140 GB free |
|
||||
| Java | OpenJDK **21.0.12** at `/usr/lib/jvm/java-21-openjdk` |
|
||||
| Display | Harness hardcodes `DISPLAY=:1`, `XAUTHORITY=/run/user/1000/xauth_HvvPAq`; runs must happen on the desktop session owning :1. X sockets X0/X1/X99 exist. |
|
||||
| git / python | git 2.55.0, python 3.14.6 |
|
||||
|
||||
## Tool and data paths on this machine
|
||||
|
||||
| What | Path |
|
||||
| --- | --- |
|
||||
| packwiz | `~/go/bin/packwiz` |
|
||||
| Prism data dir (bench, launched with `-d`) | `~/onion-bench/prism` |
|
||||
| Bench Prism instance | `~/onion-bench/prism/instances/OnionBench` |
|
||||
| Instance game dir | `.../OnionBench/.minecraft` (provisioned; `saves/onionbench` 26 MB; Default Options seeded; `BENCH_VARIANT.txt` = `C1` = last applied variant; MaxMemAlloc 8192) |
|
||||
| Instance PreLaunchCommand | **empty** (intentional — no packwiz self-update mid-matrix, per BENCH-MATRIX §1) |
|
||||
| Bench worldgen server | `~/onion-bench/server-bench` (full NeoForge server; `benchworld/` = the generated world source, 26 MB) |
|
||||
| MangoHud | user-local build: `~/.local/mangohud`, wrapper `~/.local/bin/mangohud-local` (sets LD_PRELOAD shim, `output_folder=~/onion-bench/mangologs`, autostart, 1800 s cap, fps_limit=0, vblank off). **System `mangohud` NOT in PATH.** |
|
||||
| MangoHud frametime CSVs | `~/onion-bench/mangologs/` |
|
||||
| Harness results CSV | `~/onion-bench/harness-results.csv` (append via `run_scene.py --record`) |
|
||||
| Bench launch scripts | `~/onion-bench/launch-{bench,gui,online,xvfb,...}.sh` (`launch-bench.sh` exports DISPLAY/XAUTHORITY/DRI_PRIME and launches OnionBench offline as user `Tester`) |
|
||||
| Jar cache for jarscan | `/tmp/onion/jars` — **currently absent** (ephemeral). Refetch with `analysis/audit/fetch.py` only if re-running jarscan. |
|
||||
| xdotool | `/usr/bin/xdotool` (present) |
|
||||
|
||||
## Harness wiring (quick reference)
|
||||
|
||||
- `analysis/client-perf/harness/run_scene.py` — apply variant → launch Prism into
|
||||
world `onionbench` under MangoHud → drive scenes via xdotool → slice p50/p95/p99/
|
||||
peak/1%-low from the MangoHud CSV. Constants: `ROOT=~/onion-bench/prism`,
|
||||
`INSTANCE=OnionBench`, `GAME=$ROOT/instances/OnionBench/.minecraft`,
|
||||
`MANGOLOGS=~/onion-bench/mangologs`, `WORLD=onionbench`. CLI:
|
||||
`python3 run_scene.py --variant C1 --scene s1 [--warmup 15] [--capture 30]
|
||||
[--runs 1] [--record FILE] [--pre storm_on]`. Scenes: `s1 s2 s2s s3 s4 s5`.
|
||||
Waits for "Sound engine started" in the instance log. `--record` appends one CSV row.
|
||||
- `analysis/client-perf/harness/rcon.py` — stdlib RCON client; args are
|
||||
`host port password command`. No config file.
|
||||
- `analysis/client-perf/proposed-configs/apply_variant.py` — applies variant
|
||||
`<ID>` from `proposed-configs/out/<ID>/` into the game dir and writes
|
||||
`BENCH_VARIANT.txt`. **Relaunch for every config change.**
|
||||
@@ -0,0 +1,45 @@
|
||||
# Timeline of the optimization effort
|
||||
|
||||
Condensed from `.pi/journal.md` (the authoritative running log — read it for detail).
|
||||
Commit hashes are on `main`.
|
||||
|
||||
## Background (pre-effort)
|
||||
|
||||
- 2026-09: pack curation era — FTB Chunks → Open Parties & Claims + Xaero's swap,
|
||||
FTB Quests/Teams removed, map migration tooling (`analysis/FTB-XAERO-MAP-MIGRATION.md`).
|
||||
- 2026-10-06: 60-mod expansion to 209/212 mods + full static audit of all jars
|
||||
(`analysis/audit/`), then the **Paxi `onion-fixes` datapack** (`fc649b4`) generated by
|
||||
`analysis/audit/gen_fixes.py` to resolve cross-mod recipe collisions. Rules: edit the
|
||||
generator, not the JSON.
|
||||
|
||||
## Client-performance initiative
|
||||
|
||||
| Date | Phase | What happened |
|
||||
| --- | --- | --- |
|
||||
| 2026-10-14 | **Phase 1 — static baseline** | `analysis/client-perf/` created: jarscan (951 per-frame mixin classes; Handcrafted+Chipped+Rechiseled = 58% of block atlas), 9 research briefs, **LEVERS.md** (ranked levers), observed-default configs. Nothing changed in the pack. |
|
||||
| 2026-10-14 | **Phase 1b — delivery + bench design** | `proposed-configs/` generator (`build.py`; tiers T-default/deck/mid/high + 32 bench variants C0…spr-strict), **BENCH-MATRIX.md** (run sheet), bench datapack `bench/onion-bench/`, PLAYER-TUNING.md. Verified packwiz `preserve` semantics and Default Options 21.1.8 behavior. Puzzles Lib 21.1.64 patch prepared. |
|
||||
| 2026-10-14 | **Owner decisions applied, shipped** | Owner settled the pending list (see `05-decisions-and-rules.md`). Commits: `a01bc3b` (Default Options + 8 provisional mid-tier files + Sodium Extra, Reese's Sodium Options, BadOptimizations, Particle Core + Puzzles Lib 64 → **217 mods**), `d80f85b` (spawn tuning: alexsmobs/fowlplay/critters partial files), `5919955` (archive cleanup), docs commit (client-perf, audit, phase0, journal). |
|
||||
| 2026-10-14/15 | **Phase 2 begins — boot + harness** | Machine moved to Manjaro (client-capable). Client boot check **essentially PASS** (main menu clean, 29.3 s, atlas 4096×4096×2, Default Options seeded). The one real blocker — **Critters & Companions camelCase spawn keys** NPE — fixed by owner as `8e8483d`. A 20:34 FML crash report is that same CAC bug (not WATUT). Harness built (`harness/run_scene.py`, `rcon.py`), user-local MangoHud 0.8.4, bench world generated via temp server `~/onion-bench/server-bench` (S2 Create floor + S3 mob pen placed and verified; **S4 airships not built**). One end-to-end harness run (C1/S1) — **vsync-locked, non-decision-grade**; `vblank_mode=0` added to the wrapper afterwards, never re-verified. Dedicated server booted clean from the repo at 21:01 (idle, RCON-keepalive). Committed as `e5e369a`. |
|
||||
| 2026-10-14 late | **Session interrupted** | After `e5e369a` (23:16): server kept running in tmux `onion-mc`, mods re-saved two config files (benign), RCON poller pinging every 30 s until ~23:58. **No matrix block was ever run.** State captured into `analysis/state/` (this directory). |
|
||||
|
||||
## HEAD ancestry (optimization-relevant commits)
|
||||
|
||||
```
|
||||
e5e369a chore(bench): add client bench harness and Phase 2 handoff ← HEAD, pushed
|
||||
8e8483d fix(config): correct Critters and Companions spawn keys ← owner, unblocks boot
|
||||
611a1a5 docs: client-perf analysis, bench harness and work journal
|
||||
d80f85b feat(config): moderate spawn tuning (Alex's Mobs, Fowl Play, Critters)
|
||||
5919955 chore(analysis): archive finished reviews
|
||||
a01bc3b feat(perf): Default Options + 4 perf mods + Puzzles Lib 64 ← 217 mods
|
||||
aa1d273 ... (Phase 1 baseline was taken here)
|
||||
fc649b4 feat(pack): Paxi onion-fixes datapack (recipe collisions)
|
||||
```
|
||||
|
||||
## What has NOT happened yet
|
||||
|
||||
- Matrix blocks **B0–B14: zero runs**.
|
||||
- Any config value shipped in `a01bc3b` is still **unmeasured** (provisional mid-tier).
|
||||
- WATUT world-load crash unresolved.
|
||||
- S4 bench scene unbuilt; S5 tour only partially exercised.
|
||||
- Nothing has been reported back to the owner from Phase 2 data (B7 shader data, B12b
|
||||
trial verdicts are owed).
|
||||
@@ -0,0 +1,90 @@
|
||||
# Current state — exact position
|
||||
|
||||
As of capture (2026-10-14 ~23:45 EDT, HEAD `e5e369a` pushed).
|
||||
|
||||
## Multi-step plan position
|
||||
|
||||
The initiative's plan (from the journal + BENCH-MATRIX) is:
|
||||
**boot check → fix blockers → build bench world → confirm uncapped frametimes →
|
||||
run B0→B14 → write winners into build.py → ship → report to owner.**
|
||||
|
||||
| Step | Status |
|
||||
| --- | --- |
|
||||
| 1. Boot check, client | ✅ **essentially PASS** (2026-10-14/15 session; HANDOFF.md). 22 benign ERRORs. |
|
||||
| 1b. Boot check, dedicated server | ✅ **PASS** — booted from repo at 21:01, `Done (3.004s)`, no missing deps / mixin failures / `ftbteams` / fishermans-haven / YACL errors (see `notes-serverlog-triage.md`). |
|
||||
| 2. Fix boot blockers | ✅ CAC spawn-key NPE fixed by owner (`8e8483d`). The 20:34 crash report is this bug — **do not** confuse it with the WATUT crash. |
|
||||
| 3. WATUT intermittent world-load crash | ❌ **OPEN — first task.** Details below. |
|
||||
| 4. Confirm uncapped frametimes | ❌ Not verified after `vblank_mode=0` fix. |
|
||||
| 5. Bench world complete | ⚠️ **PARTIAL** — S1–S3 built & verified; **S4 airships not built**; S5 tour only partially exercised; S2 `/fill` motors may need manual re-placement (BENCH-MATRIX §2). |
|
||||
| 6. Run matrix B0→B14 | ❌ **Zero blocks run.** |
|
||||
| 7. Winners → `build.py` → ship → commit | ❌ Not started. |
|
||||
| 8. Report to owner (B7 shader data, B12b trial verdicts, B14) | ❌ Not started. |
|
||||
|
||||
## Open defect: WATUT world-load crash (blocking bench runs)
|
||||
|
||||
- Symptom: intermittent crash on world load —
|
||||
`corosus.watut.ShaderInstanceBlur.<init>` → `FileNotFoundException:
|
||||
minecraft:shaders/core/particle.json`, during "Rendering overlay".
|
||||
- First world load works, a later load crashed (2026-10-14/15 session).
|
||||
- **The 20:34 crash report in `crash-reports/` is NOT this** — it is the already-fixed
|
||||
CAC spawn-key NPE. No crash report exists for the WATUT crash yet.
|
||||
- Owner constraint: **remove no mods** → find a WATUT config/patch/reliable-retry, or
|
||||
reproduce and characterize it first.
|
||||
- It must be resolved (or made reliably recoverable) before trusting long bench runs.
|
||||
|
||||
## Live processes / environment left by the interrupted session
|
||||
|
||||
- **The dedicated server is still running**: java PID 1699563 in tmux session
|
||||
`onion-mc`, started 21:01:35 from the repo root (port 25565, RCON :25575).
|
||||
Idle boot-check run; zero players joined. **Never kill it; never touch `world/`.**
|
||||
- An RCON client from 127.0.0.1 connects/disconnects **every 30 s** (through #353 by
|
||||
23:57). The poller process was not identified (candidates: a keepalive loop or
|
||||
`tools/rcon.py` under some wrapper). Investigate before assuming it's yours to kill.
|
||||
- Uncommitted working-tree changes are **benign mod re-saves**, expected on boot:
|
||||
- `config/alexsmobs-common.toml`: 13-line tuned partial expanded to 913 full lines
|
||||
(all tuned values retained — e.g. `gazelleSpawnWeight = 20`).
|
||||
- `config/akashictome-common.toml`: re-serialized; curated whitelist **intact**,
|
||||
comments stripped, arrays collapsed.
|
||||
- Rule (`05-decisions-and-rules.md`): `git checkout -- config/` before staging;
|
||||
do not commit these.
|
||||
- `config/*.toml.bak` files + `config/spark/tmp-client/*.jfr.tmp` (spark's live
|
||||
background-profiler buffer, still growing while the server runs) = runtime junk.
|
||||
- `context.md` in the repo root was scout scratch from this capture session — safe to
|
||||
delete (content folded into this directory).
|
||||
|
||||
## Measurements so far (all of it)
|
||||
|
||||
Exactly **one** harness run, **not decision-grade** (vsync-locked):
|
||||
|
||||
```
|
||||
~/onion-bench/harness-results.csv:
|
||||
C1,s1,n=1889, p50=16.7ms p95=17.0ms p99=17.2ms peak=17.6ms avg_fps=59.8 1%low=57.5
|
||||
```
|
||||
|
||||
Supporting MangoHud CSVs in `~/onion-bench/mangologs/`:
|
||||
`java_2026-10-14_22-13-53.csv` (2353 rows), `java_2026-10-14_22-14-54.csv` (3762),
|
||||
`java_2026-10-14_22-21-46.csv` (178, truncated), plus two `glxgears` sanity runs.
|
||||
The vsync fix (`fps_limit=0`, vblank off in `~/.local/bin/mangohud-local`) has been
|
||||
applied but **not re-verified** — first task after WATUT: one C1/S1 run must show
|
||||
well above 60 fps.
|
||||
|
||||
## Bench instance state (ready to use)
|
||||
|
||||
`~/onion-bench/prism/instances/OnionBench/.minecraft`: provisioned, mods installed,
|
||||
`saves/onionbench` present (26 MB, from 21:54 worldgen), `config/defaultoptions/`
|
||||
seeded, `options.txt` carries the bench pins (`maxFps:260`, `enableVsync:false`,
|
||||
`renderDistance:8`, `simulationDistance:8`), `BENCH_VARIANT.txt` = `C1` (last applied).
|
||||
PreLaunchCommand empty by design. MaxMemAlloc 8192. Launched offline as user `Tester`
|
||||
via `~/onion-bench/launch-bench.sh` (DISPLAY=:1).
|
||||
|
||||
## Known open questions
|
||||
|
||||
1. WATUT crash root cause + fix path (top priority).
|
||||
2. Identity of the 30 s RCON poller.
|
||||
3. Whether uncommitted config re-saves should simply be checked out (recommended) —
|
||||
and whether AkashicTome stripping comments matters for future edits (edit the
|
||||
shipped source-of-truth, then expect the mod to re-serialize locally).
|
||||
4. S2 Create floor: `/fill`-placed kinetic network may need motors re-placed by hand
|
||||
(checklist in BENCH-MATRIX §2 step 2) — verify during S4 build.
|
||||
5. B5 (SSRD/Sable airship decay) is scoped to fleet P0/P1 — needs a documented proxy
|
||||
adaptation or a "deferred, fleet-only" note in results.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Remaining plan (with current status)
|
||||
|
||||
The authoritative run sheet is `analysis/client-perf/BENCH-MATRIX.md` — read it fully
|
||||
before benching. This file summarizes it, marks what's done, and adds proxy-machine
|
||||
notes. **Win** = median p99 improves by more than the machine's B0 noise spread AND
|
||||
1%-low does not worsen by more than the spread; ties keep upstream defaults.
|
||||
Floors (fleet): P0 1%-low ≥ 30, P1/P2 ≥ 60 — on this proxy, floors don't map; use
|
||||
relative wins only.
|
||||
|
||||
## Next actions, in order
|
||||
|
||||
1. **WATUT crash** (open): reproduce (repeated world loads in the OnionBench
|
||||
instance), capture a crash report, then fix via config/patch/retry. Owner says
|
||||
remove no mods. If unfixable, document a reliable workaround for bench runs.
|
||||
2. **Verify uncapped frametimes**: one `run_scene.py --variant C1 --scene s1` run;
|
||||
expect p99 well under 16.7 ms (vsync previously locked it). Do not proceed to B0
|
||||
until FPS is visibly uncapped.
|
||||
3. **Finish the bench world** (BENCH-MATRIX §2): build S4 — ONE Aeronautics ship
|
||||
(~5×7×15, planks+glass+1 propeller) hovering y≈170, copied via Create schematic to
|
||||
4 ships at z = 32/96/192/384; produce `bench-world-v1-s4-0/1/4.zip` copies.
|
||||
Verify S2 shafts rotate (re-place motors by hand if the `/fill` network didn't
|
||||
initialise) and S3 villager count. DH pregenerate per §2 step 5 (radius 256, tour,
|
||||
`generatorPlan=DISABLED` afterwards). Leave world at S1, save, zip as
|
||||
`bench-world-v1.zip` (record sha256).
|
||||
4. **Run the matrix** B0 → B1 → B2 → B3 → B4 → B6 → B7 → B8 → B9 → B10 → B11 →
|
||||
B12a → B12b → B13 → B14 (B5 deferred, fleet-only). Budget ≈25 h machine time.
|
||||
Apply variants with `apply_variant.py`; relaunch per config change; scenes in
|
||||
S1→S6 order per session; record with `--record`.
|
||||
5. **Ship**: write winners into `proposed-configs/build.py` (DEFAULT/DECK/HIGH layers),
|
||||
`build.py --ship`, `packwiz refresh`, commit (after `git checkout -- config/`).
|
||||
Ship `config/modernfix-mixins.properties` **only if B6 passes** (see the `SHIP`
|
||||
comment in `build.py`). New mod adds (B12b winners) ⇒ re-run B0 first.
|
||||
6. **Report to owner**: B7 shader-policy data, B12b trial verdicts, B14 spawn-tuning
|
||||
measurement, and any proxy-vs-fleet caveats.
|
||||
|
||||
## Matrix blocks (one line each; decision rules verbatim from BENCH-MATRIX §4)
|
||||
|
||||
| Block | Tests | Decision rule |
|
||||
| --- | --- | --- |
|
||||
| B0 | noise floor: C0 ×3 on S1 | defines each machine's win threshold; watch MemAvailable drift |
|
||||
| B1 | baseline: C0 on S1–S6 | reference numbers only (startup, atlas, heap summary, VRAM) |
|
||||
| B2 | composite: C1 / C1-deck / C1-high, all scenes | if C1 isn't a win on ≥2 machines, STOP and bisect via B3–B11 before shipping anything |
|
||||
| B3 | DH: radius ladder + SSAO/quality/threads | per tier: largest radius meeting the floor on S1+S5; SSAO-off on a win; threads stay 2 unless 4 wins S5 w/o losing S1 |
|
||||
| B4 | heap & GC: C1 × J0–J3, then heap 4/5/6 G | GC = best S5 1%-low; heap = smallest within winner's spread |
|
||||
| B5 | Sable/SSRD sub-levels + decay (**fleet P0/P1 only**) | SSRD = largest meeting floor with 4 ships; sable-noshade only on win + acceptable looks; decay beyond spread = report, no config fix |
|
||||
| B6 | ModernFix `dynamic_resources` | ship only if heap −300 MB or faster startup beyond spread AND zero missing textures AND 3 clean cold launches |
|
||||
| B7 | shaders ×4 profiles on S2/S1/S4 | **data for owner, no auto-decision**; verify Flywheel backends drop under shaders; watch Iris #3164 corruption |
|
||||
| B8 | entities: FA/Extensions, EMF LOD 20/8, LDL modes on S3 | smallest winning EMF LOD; cheapest winning LDL mode per tier |
|
||||
| B9 | Flywheel lightSmoothness flat/C1/smooth on S2 | smoothest value meeting the floor, per tier |
|
||||
| B10 | Sound Physics: spr-up/strict/nocap on S2s | adopt each winning change; listen for audio breakage |
|
||||
| B11 | vanilla/Sodium: threads 3, blend 0/2, mip 4 | adopt only wins (S5 + not losing S1; mip4 needs VRAM ≤ 5.5 GB on P1) |
|
||||
| B12a | added-mod ablation (BadOptimizations / Particle Core / Sodium Extra out) | keep unless removal wins or mod breaks something; Sodium Extra + RSO stay regardless |
|
||||
| B12b | trial mods (Gnetum, Better Block Entities, Flerovium, Super Resolution, Ixeris, Iris Flywheel Compat, Cull Leaves) | add only on win w/o breakage via `packwiz modrinth add`; **new mod set ⇒ re-run B0**; not More Culling (excludes 1.21.1) |
|
||||
| B13 | spark profile of worst B2 scene under C1 | flag any mod >5% of render-thread time that isn't Sodium/Iris/DH/Flywheel |
|
||||
| B14 | spawn tuning: defaults vs shipped, river/jungle/warm ocean ×10 min | keep if totals drop and every species still appears; back off if one effectively disappears (SPAWN-TUNING §B14) |
|
||||
|
||||
## Scenes and JVM sets (fixed inputs)
|
||||
|
||||
- **S1** vista (terrain + DH) · **S2** Create floor, 2304 kinetic BEs · **S2s** = S2 +
|
||||
24-sounds/tick storm · **S3** mob pen (30 villagers, allays w/ torches = moving LDL
|
||||
sources) · **S4** 0/1/4 Sable airship sub-levels · **S5** 24-teleport tour, 120 s,
|
||||
no warm-up discard · **S6** GUI/JEI overlay at S1.
|
||||
- **J0** phase0 G1 baseline · **J1** brucethemoose G1 · **J2** generational ZGC ·
|
||||
**J3** Shenandoah (plain; NOT `ShenandoahGCMode=iu` — aborts JDK 21). All parse-
|
||||
checked on OpenJDK 21.0.12. Default heap here: 8 G (instance), matrix suggests 6 G
|
||||
for P2 / 5 G for P0.
|
||||
|
||||
## Proxy-machine adaptations to document as you go
|
||||
|
||||
- B2's "≥2 machines" gate: run C1 vs C0 here; treat as single-machine evidence and
|
||||
mark fleet confirmation as pending.
|
||||
- B5 has no Sable-decay proxy path as specified — either adapt (4-ship scene exists
|
||||
once S4 is built) or record as deferred.
|
||||
- B4 heap ladder should use the instance's 8 G as the top rung.
|
||||
- Absolute floors (30/60 fps) are fleet values; use relative wins on this box.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Settled decisions, rules of the road, traps
|
||||
|
||||
## Owner decisions (2026-10-14) — settled, do not relitigate
|
||||
|
||||
1. **Delivery mechanism = Default Options** (+ enforced `config/modernfix-mixins.properties`
|
||||
**after B6 passes**). Done in `a01bc3b`.
|
||||
2. **Puzzles Lib 21.1.64** — done (bugfix only; no NeoForge perf change, LEVERS #14 corrected).
|
||||
3. **Shaders: opt-in** (packwiz downloads them; nothing enables them). B7 produces the
|
||||
policy data; owner decision still pending that data.
|
||||
4. **Spawn tuning allowed if not detrimental to gameplay** — done, moderate
|
||||
(`d80f85b`: alexsmobs/fowlplay/crittersandcompanions partial files). B14 measures it.
|
||||
5. **Remove no mods.** LEVERS §5 removal candidates are reference only.
|
||||
(This includes WATUT — so the WATUT crash must be fixed, not removed away.)
|
||||
6. **Add any helpful mods** — the five Add-verdict mods went in `a01bc3b` (Sodium Extra,
|
||||
Reese's Sodium Options, BadOptimizations, Particle Core, + Default Options).
|
||||
Trial mods are B12b's call; the agent may add trial winners.
|
||||
|
||||
## Rules of the road (iteration rules from the journal)
|
||||
|
||||
- Change tuned defaults **only** via `analysis/client-perf/proposed-configs/build.py`
|
||||
(DEFAULT / DECK / HIGH layers) → `build.py --ship` → `packwiz refresh` → commit.
|
||||
Never hand-edit `proposed-configs/out/*` or shipped files.
|
||||
- Shipped defaults are **copy-if-absent**: changes reach fresh installs only. Existing
|
||||
players follow PLAYER-TUNING §1 (delete 7 re-seeding config files; do NOT delete
|
||||
`options.txt`; set tier video values by hand).
|
||||
- **`git checkout -- config/` before staging** — the server's mods re-save partial
|
||||
spawn files in full on boot; committing those expansions is the known mistake.
|
||||
- `packwiz update` in bulk resets hand-set `side = "server"` on **petprotect,
|
||||
c2me-neoforge, noisium**. Single-mod updates are safe.
|
||||
- Any new mod: verify NeoForge 1.21.1 build + `neoforge.mods.toml` ranges against pins,
|
||||
check `side`, then **re-run B0** (new mod set invalidates the noise floor).
|
||||
- One recorded commit per matrix row; label all data as **proxy-machine** data.
|
||||
- Never kill the dedicated server (tmux `onion-mc`, repo root, port 25565) and never
|
||||
touch `world/`. `sp-world.tar.gz` (650 MB, untracked) — leave alone.
|
||||
- `.pi/journal.md` is the running log — append a session entry when you finish.
|
||||
|
||||
## Verified facts / traps (don't re-derive, don't re-trip)
|
||||
|
||||
- **packwiz `preserve`**: settable only by hand-editing `index.toml`; survives refresh
|
||||
and unrelated updates; silently dropped if the file is absent/ignored at refresh
|
||||
time; installer writes the file only if absent. Index files have no `side`, so
|
||||
shipped configs land on the server too (this repo IS the server).
|
||||
- **Default Options 21.1.8**: copy-if-absent for `config/defaultoptions/options.txt`
|
||||
(key_ lines stripped) and `config/defaultoptions/extra/**`; runs before NeoForge
|
||||
constructs mods — too late for `modernfix-mixins.properties` (hence B6 gating).
|
||||
- Observed defaults were generated from the **same jar versions as current pins**
|
||||
(DH 3.3.2, Sodium 0.8.13, EMF 3.3.9, Create 6.0.10/Flywheel 1.0.6, Sable 2.0.5,
|
||||
SPR 1.5.1). DH `numberOfThreads` true default = ceil(nproc/2) = 4 here (observed 2
|
||||
came from a 4-thread box).
|
||||
- Flywheel 1.0.x **disables both GPU backends when any shaderpack is active**
|
||||
(verified in `Backends.java`). No Iris Flywheel Compat in the pack (B12b trial).
|
||||
- SSRD 1.8.7 → `config/ssrd-common.toml` (COMMON; server reads it too; slider floor 16).
|
||||
LambDynamicLights 4.8.11 → `config/lambdynlights.toml` (lowercase modes).
|
||||
SPR merges partial `sound_rates.properties` over generated defaults.
|
||||
- **More Culling is unusable**: its NeoForge build declares `minecraft [1.21,1.21.1)`.
|
||||
- `-XX:ShenandoahGCMode=iu` aborts JDK 21 without UnlockExperimentalVMOptions — J3 uses
|
||||
plain `-XX:+UseShenandoahGC`.
|
||||
- Prism 11 `--offline` fails a fresh instance ("Required libraries are missing");
|
||||
launch online once, then offline works.
|
||||
- `xdotool windowactivate --sync` hangs under kwin — use `windowactivate` +
|
||||
`windowfocus`.
|
||||
- MangoHud needs `MANGOHUD_CONFIG` explicit + LD_PRELOAD exported — use the wrapper
|
||||
`~/.local/bin/mangohud-local`; system mangohud is not installed.
|
||||
- cdn.modrinth.com 501s Range requests — download whole jars. No `unzip`; use python
|
||||
zipfile. `lodestonelib` is the real slug (`lodestone` has no 1.21.1 build).
|
||||
- Duplicate `version_number` traps: tarotcards-remastered, room-for-two, too-many-bows
|
||||
reuse version strings across MC versions — pin exact filenames by hand.
|
||||
- Iris 1.8.14-beta.1 ↔ Sodium 0.8.13 mixin incompatibility reported on NeoForge
|
||||
(r5) — check when testing shaders (B7).
|
||||
|
||||
## Open owner-facing deliverables (owed from Phase 2)
|
||||
|
||||
- B7 shader-policy data → owner decision on default-off vs opt-in messaging.
|
||||
- B12b trial-mod verdicts (which trials earned a permanent add).
|
||||
- B14 spawn-tuning measurement (keep/back-off call).
|
||||
- Any B13 "mod >5% of render thread" findings.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Artifact map
|
||||
|
||||
Everything that exists because of this effort, with status. Paths relative to
|
||||
`/home/logan/onion-mc` unless marked `~/` (= `/home/logan`).
|
||||
|
||||
## Repo — analysis/ (packwizignored; committed to git)
|
||||
|
||||
| Path | What | Status |
|
||||
| --- | --- | --- |
|
||||
| `analysis/state/` | **This capture.** Uncommitted yet — commit alongside next work. | current |
|
||||
| `analysis/client-perf/HANDOFF.md` | Prior session's self-contained brief (2026-10-15). | committed `e5e369a` |
|
||||
| `analysis/client-perf/BENCH-MATRIX.md` | **The run sheet**: §0 boot check, §1 fixed inputs, §2 bench world recipe, §3 config IDs, §4 B0–B14 + decision rules, §5 CSV format. | committed |
|
||||
| `analysis/client-perf/LEVERS.md` | Ranked levers (meta M1–M3, tier 1–3), add/remove candidates, research corrections. §6 superseded by BENCH-MATRIX. | committed |
|
||||
| `analysis/client-perf/PLAYER-TUNING.md` | Tier table (Deck/Mid/High), §1 file-reset for existing players, heap/GC, shader/RP guidance, Deck env vars. Values provisional until Phase 2. | committed |
|
||||
| `analysis/client-perf/SPAWN-TUNING.md` | The shipped spawn tuning + B14 measurement plan. Header: "Unmeasured: bench block B14". | committed |
|
||||
| `analysis/client-perf/README.md` | Phase 1 index + method/caveats. Minor staleness: says B0–B13, matrix has B14. | committed |
|
||||
| `analysis/client-perf/notes-phase0-distill.md` | Distilled capture protocol from phase0 (uncapped, p99+1%low, warm-up discard, 3 runs, one variable; per-machine prep). | committed |
|
||||
| `analysis/client-perf/notes-serverlog-triage.md` | Server-log triage: no missing-dep/mixin/ftbteams/YACL findings; 8 benign recipe-book ERRORs. | committed |
|
||||
| `analysis/client-perf/proposed-configs/` | `build.py` (**single source of truth** for shipped defaults; BENCH variant layer; `--ship`), `apply_variant.py`, `README.md` (§1 M1 decision, §2 tiers, §3 key provenance), `templates/`, and `out/` (generated: tiers T-* and variants C0, C1, C1-deck, C1-high, dh-*, emf-*, fw-*, ldl-*, mf-*, opt-*, sable-*, sod-*, spr-*, ssrd-* + MANIFEST.md). | committed (out/ excluded) |
|
||||
| `analysis/client-perf/harness/` | `run_scene.py` (variant→launch→scene→p99 pipeline; verified once), `rcon.py`. | committed `e5e369a` |
|
||||
| `analysis/client-perf/bench/onion-bench/` | Bench datapack (pack_format 48): scene functions s1–s4 (+setups, storm, tour steps). S2/S3 exercised in game; S4 content unbuilt; full syntax never wholly verified. | committed |
|
||||
| `analysis/client-perf/data/` | `jarscan.json`, `jarscan-report.md` (static mixin/atlas/model counts), `observed-default-configs/` (real defaults from same-version jars; reference for build.py). | committed |
|
||||
| `analysis/client-perf/research/r1…r9-*.md` | Nine research briefs with confidence tags. | committed |
|
||||
| `analysis/client-perf/proposed-changes/puzzles-lib-21.1.64.patch` | Applied already (in `a01bc3b`); kept as record. | committed |
|
||||
| `analysis/audit/` | Static audit scripts: `fetch.py` (rebuild /tmp/onion/jars), `jarscan` inputs, `gen_fixes.py` (**generates the shipped Paxi onion-fixes datapack — edit this, not the JSON**), deps/collision/recipe chain tools. | committed |
|
||||
| `analysis/archive/` | Finished 2026-09 reviews (README maps old paths). | committed |
|
||||
| `analysis/FTB-XAERO-MAP-MIGRATION.md` | Pre-effort map migration doc. | committed |
|
||||
|
||||
## Repo — pack-facing (shipped to players)
|
||||
|
||||
| Path | What |
|
||||
| --- | --- |
|
||||
| `pack.toml`, `index.toml`, `packwiz.json`, `mods/*.pw.toml` (217) | packwiz manifest. |
|
||||
| `config/defaultoptions/` | Default Options seeds (8 provisional mid-tier files, T-mid equivalent). |
|
||||
| `config/alexsmobs-common.toml`, `config/fowlplay.json5`, `config/crittersandcompanions-common.json5` | Enforced partial spawn tuning (`d80f85b`). **Local copies currently re-saved-in-full by mods — do not commit.** |
|
||||
| `config/paxi/datapacks/onion-fixes/` | Recipe-collision fixes, generated by `analysis/audit/gen_fixes.py`. |
|
||||
| `config/akashictome-common.toml` | Curated guidebook whitelist. Local copy re-serialized by mod (whitelist intact). |
|
||||
| `phase0/P0-steamdeck.md`, `P1-windows-7700k-1060.md`, `P2-linux-6700k-1080ti.md` | Per-fleet-machine measurement plans. **Machines unreachable** — status: plan only. |
|
||||
| `tools/` | Map-migration/UUID/RCON utilities (pre-effort). `tools/rcon.py` is a candidate for the mystery 30 s poller. |
|
||||
| `.pi/journal.md` | The running work log — the richest source; read the last three sessions. |
|
||||
|
||||
## Outside the repo — bench machine
|
||||
|
||||
| Path | What | Status |
|
||||
| --- | --- | --- |
|
||||
| `~/onion-bench/prism/` | Bench Prism data dir (`-d`). Instance `OnionBench`: `.minecraft` provisioned, `saves/onionbench` (26 MB), Default Options seeded, bench pins in options.txt, `BENCH_VARIANT.txt = C1`. MaxMemAlloc 8192. | ready |
|
||||
| `~/onion-bench/server-bench/` | Temp NeoForge server that generated the bench world; `benchworld/` = world source (S1–S3 built; S4 missing). | ready, incomplete |
|
||||
| `~/onion-bench/mangologs/` | MangoHud frametime CSVs (2 glxgears sanity, 3 java runs incl. one truncated). | data |
|
||||
| `~/onion-bench/harness-results.csv` | The single C1/S1 result row (vsync-locked, non-decision-grade). | data |
|
||||
| `~/onion-bench/launch-*.sh` | Launch wrappers (DISPLAY=:1, XAUTHORITY, DRI_PRIME=1, offline `Tester`). | ready |
|
||||
| `~/.local/mangohud` + `~/.local/bin/mangohud-local` | User-local MangoHud 0.8.4 + wrapper (vblank off, fps_limit=0, CSVs to mangologs). | ready |
|
||||
| `~/onion-bench/*.out`, `mc-menu-*.png`, `/tmp/mangohud.pkg.tar.zst`, `/tmp/cac-*.json5` | Session debug scraps. | ignorable |
|
||||
| `/tmp/onion/jars` | Static-analysis jar cache. | **gone** (ephemeral; `analysis/audit/fetch.py` rebuilds — only needed for jarscan re-runs) |
|
||||
| tmux session `onion-mc` | The live dedicated server (java PID 1699563). | **running — leave alone** |
|
||||
|
||||
## Repo — runtime (never commit / never touch)
|
||||
|
||||
`world/`, `libraries/`, `logs/` (latest.log + debug.log = the 21:01 server run),
|
||||
`crash-reports/` (2026-10-14 crash = fixed CAC bug), `server.log`, `cache/`,
|
||||
`config/*.toml.bak`, `config/spark/tmp-client/*`, `sp-world.tar.gz`, `usercache.json`.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Onion MC — optimization effort: state capture
|
||||
|
||||
Captured **2026-10-14 ~23:45 EDT** (Manjaro bench machine), at git **HEAD `e5e369a`**
|
||||
(`chore(bench): add client bench harness and Phase 2 handoff`), branch `main`, pushed.
|
||||
Pack: **217 mods**, MC **1.21.1**, NeoForge **21.1.251**.
|
||||
|
||||
> **Update 2026-10-15 (b):** WATUT crash FIXED (`a84319b`, MidnightLib 1.10.0 — root
|
||||
> cause was a MidnightLib CME); uncapped frametimes VERIFIED; harness v2 (`bench.py`)
|
||||
> replaces `run_scene.py` (`e18bd3c`). See `.pi/journal.md` latest entry. Plan steps
|
||||
> 1–2 of `04-plan.md` are done; next is S4 build → B0.
|
||||
|
||||
## Where we are (one paragraph)
|
||||
|
||||
The client-performance initiative is in **Phase 2 (benchmarking)**, which has just
|
||||
begun and was interrupted mid-setup. Static analysis (Phase 1), the tuned-config
|
||||
generator, the owner's decisions, and the first wave of perf-mod adds are **done and
|
||||
shipped** (`a01bc3b`, `d80f85b`). The client **boots clean**; the dedicated server
|
||||
**boots clean** (a live idle instance from the boot check is still running in tmux —
|
||||
leave it alone). The bench harness works end-to-end but has produced exactly **one
|
||||
non-decision-grade measurement** (C1/S1, vsync-locked ~60 fps). The bench world has
|
||||
scenes S1–S3 built; **S4 (airships) is not built**. **Matrix blocks B0–B14 have not
|
||||
run at all.** The one known open defect is an **intermittent WATUT world-load crash**.
|
||||
Immediate next actions: resolve/work around WATUT → confirm uncapped frametimes →
|
||||
finish S4 → run B0→B14 → write winners into `build.py` → ship → report to owner.
|
||||
|
||||
## Reading order
|
||||
|
||||
| File | Contents |
|
||||
| --- | --- |
|
||||
| [`01-pack-and-environment.md`](01-pack-and-environment.md) | What the pack is, repo conventions, this machine's inventory, tool paths |
|
||||
| [`02-timeline.md`](02-timeline.md) | How the effort got here: phases, commits, decisions |
|
||||
| [`03-current-state.md`](03-current-state.md) | Exact current step, done/open/blocked, interrupted-session artifacts and evidence |
|
||||
| [`04-plan.md`](04-plan.md) | The remaining plan with per-step status, B0–B14 run sheet summary, ship procedure |
|
||||
| [`05-decisions-and-rules.md`](05-decisions-and-rules.md) | Settled owner decisions, rules of the road, traps and gotchas |
|
||||
| [`06-artifact-map.md`](06-artifact-map.md) | Every artifact, where it lives, what it is, current status |
|
||||
|
||||
## Authoritative sources this capture distills
|
||||
|
||||
- `.pi/journal.md` — the running work log (read the last three sessions first).
|
||||
- `analysis/client-perf/HANDOFF.md` — prior session's own handoff brief.
|
||||
- `analysis/client-perf/BENCH-MATRIX.md` — **the run sheet**; when this capture and
|
||||
the matrix disagree, the matrix wins.
|
||||
- `analysis/client-perf/LEVERS.md` — ranked levers with verification notes.
|
||||
- `phase0/P0|P1|P2-*.md` — per-fleet-machine measurement plans (machines unreachable).
|
||||
|
||||
## For the incoming agent
|
||||
|
||||
Start at `03-current-state.md`, then `04-plan.md`. Do the next actions in
|
||||
`04-plan.md` §"Next actions" **in order**. Before touching anything, read
|
||||
`05-decisions-and-rules.md` — several rules exist because a previous session already
|
||||
made the mistake once (config re-save commits, packwiz `side` resets, vsync-locked
|
||||
data). Append your session to `.pi/journal.md` when you finish.
|
||||
@@ -1,13 +0,0 @@
|
||||
# Onion MC spawn tuning (analysis/client-perf/SPAWN-TUNING.md). Partial file: NeoForge
|
||||
# fills every other key with its default on load. Halves only the common mobs that are
|
||||
# heavy by weight x hard-coded group size; rare mobs keep their defaults.
|
||||
[spawning]
|
||||
gazelleSpawnWeight = 20
|
||||
capuchinMonkeySpawnWeight = 14
|
||||
elephantSpawnWeight = 15
|
||||
straddlerSpawnWeight = 35
|
||||
rockyRollerSpawnWeight = 30
|
||||
underminerSpawnWeight = 25
|
||||
blobfishSpawnWeight = 15
|
||||
mudskipperSpawnWeight = 14
|
||||
caimanSpawnWeight = 15
|
||||
@@ -1,51 +0,0 @@
|
||||
// Onion MC spawn tuning (analysis/client-perf/SPAWN-TUNING.md). Partial file: YACL keeps
|
||||
// defaults for missing keys. Each listed rule list REPLACES that mob's defaults, so every
|
||||
// biome rule of a changed mob is listed. configVersion 2 skips the v1->v2 migration.
|
||||
{
|
||||
"configVersion": 2,
|
||||
"spawning": {
|
||||
"leaf_insect": [
|
||||
{"biome_spec": "#c:is_jungle", "weight": 8, "min": 1, "max": 1},
|
||||
{"biome_spec": "#c:is_forest", "weight": 4, "min": 1, "max": 1}
|
||||
],
|
||||
"sea_bunny": [
|
||||
{"biome_spec": "minecraft:ocean", "weight": 10, "min": 1, "max": 2},
|
||||
{"biome_spec": "minecraft:deep_ocean", "weight": 10, "min": 1, "max": 2},
|
||||
{"biome_spec": "minecraft:warm_ocean", "weight": 20, "min": 1, "max": 3},
|
||||
{"biome_spec": "minecraft:lukewarm_ocean", "weight": 10, "min": 1, "max": 3},
|
||||
{"biome_spec": "minecraft:deep_lukewarm_ocean", "weight": 10, "min": 1, "max": 3}
|
||||
],
|
||||
"dragonfly": [
|
||||
{"biome_spec": "#c:is_river", "weight": 4, "min": 1, "max": 1},
|
||||
{"biome_spec": "#c:is_swamp", "weight": 5, "min": 1, "max": 1},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 1}
|
||||
],
|
||||
"ladybug": [
|
||||
{"biome_spec": "#c:is_forest", "weight": 4, "min": 1, "max": 3},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 2},
|
||||
{"biome_spec": "#c:is_floral", "weight": 7, "min": 1, "max": 2}
|
||||
],
|
||||
"stag_beetle": [
|
||||
{"biome_spec": "#c:is_forest", "weight": 3, "min": 1, "max": 2},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 2}
|
||||
],
|
||||
"roly_poly": [
|
||||
{"biome_spec": "#c:is_forest", "weight": 3, "min": 1, "max": 3},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 2},
|
||||
{"biome_spec": "#c:is_swamp", "weight": 6, "min": 1, "max": 2}
|
||||
],
|
||||
"snail": [
|
||||
{"biome_spec": "#c:is_forest", "weight": 3, "min": 1, "max": 2},
|
||||
{"biome_spec": "#c:is_lush", "weight": 5, "min": 1, "max": 2},
|
||||
{"biome_spec": "#c:is_swamp", "weight": 6, "min": 1, "max": 2}
|
||||
],
|
||||
"stick_bug": [
|
||||
{"biome_spec": "#c:is_forest", "weight": 3, "min": 1, "max": 2},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 2}
|
||||
],
|
||||
"weevil": [
|
||||
{"biome_spec": "#c:is_forest", "weight": 3, "min": 1, "max": 3},
|
||||
{"biome_spec": "#c:is_lush", "weight": 6, "min": 1, "max": 2}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
// Onion MC spawn tuning (analysis/client-perf/SPAWN-TUNING.md). Partial file: YACL keeps
|
||||
// defaults for every missing key and re-saves. Targets the waterfowl/gull/sparrow flocks
|
||||
// behind Fowl Play #214; songbirds and raptors keep their defaults.
|
||||
{
|
||||
"duckSpawnWeight": 2,
|
||||
"duckMaxGroupSize": 4,
|
||||
"gooseSpawnWeight": 2,
|
||||
"gooseMaxGroupSize": 3,
|
||||
"gullSpawnWeight": 3,
|
||||
"gullMaxGroupSize": 5,
|
||||
"sparrowSpawnWeight": 12,
|
||||
"sparrowMaxGroupSize": 4
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
mixin.perf.dynamic_resources=true
|
||||
@@ -0,0 +1,980 @@
|
||||
_version = 4
|
||||
|
||||
[server]
|
||||
#
|
||||
# Defines the distance the player will receive updates around.
|
||||
realTimeUpdateDistanceRadiusInChunks = 256
|
||||
#
|
||||
# Prefix of the level keys sent to the clients.
|
||||
# If the mod is running behind a proxy, each backend should use a unique value.
|
||||
# If this value is empty, level key will be based on the server's seed hash.
|
||||
levelKeyPrefix = ""
|
||||
#
|
||||
# Defines the distance allowed to be synchronized around the player.
|
||||
# Should be the same or larger than maxGenerationRequestDistance in most cases.
|
||||
maxSyncOnLoadRequestDistance = 4096
|
||||
#
|
||||
# If true, clients will receive updated LODs when joining or loading new LODs.
|
||||
synchronizeOnLoad = true
|
||||
#
|
||||
# Custom server key used which can be used to always reuse the same LOD data folder,
|
||||
# for cases when the server doesn't have a static IP for some reason.
|
||||
# If this value is empty, the client itself decides which folder name to use.
|
||||
# Requires rejoining the server to apply after changing.
|
||||
serverKey = ""
|
||||
#
|
||||
# Defines the distance allowed to generate around the player.
|
||||
maxGenerationRequestDistance = 4096
|
||||
#
|
||||
# Makes the server send level keys for each world.
|
||||
# Disable this if you use alternative ways to send level keys.
|
||||
sendLevelKeys = true
|
||||
#
|
||||
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.
|
||||
# Autogenerated ID used to prevent multiple independent servers from accidentally
|
||||
# writing over each other's LODs when the same serverKey is set on both.
|
||||
#
|
||||
# How many LOD generation requests per second should a client send?
|
||||
# Also limits the number of client requests allowed to stay in the server's queue.
|
||||
generationRequestRateLimit = 20
|
||||
#
|
||||
# How many LOD sync requests per second should a client send?
|
||||
# Also limits the amount of player's requests allowed to stay in the server's queue.
|
||||
syncOnLoadRateLimit = 50
|
||||
#
|
||||
# If true, clients will receive real-time LOD updates for chunks outside the client's render distance.
|
||||
enableRealTimeUpdates = true
|
||||
#
|
||||
# Maximum global speed for uploading LODs to the clients, in KB/s.
|
||||
# Value of 0 disables the limit.
|
||||
globalBandwidthLimit = 0
|
||||
#
|
||||
# Maximum per-player speed for uploading LODs to the clients, in KB/s.
|
||||
# Value of 0 disables the limit.
|
||||
playerBandwidthLimit = 500
|
||||
#
|
||||
# Enables adaptive transfer speed based on client performance.
|
||||
# If true, DH will automatically adjust transfer rate to minimize connection lag.
|
||||
# If false, transfer speed will remain fixed.
|
||||
enableAdaptiveTransferSpeed = false
|
||||
|
||||
[common.lodBuilding]
|
||||
#
|
||||
# How should block data be compressed when creating LOD data?
|
||||
# This setting will only affect new or updated LOD data,
|
||||
# any data already generated when this setting is changed will be
|
||||
# unaffected until it is modified or re-loaded.
|
||||
#
|
||||
# MERGE_SAME_BLOCKS
|
||||
# Every block/biome change is recorded in the database.
|
||||
# This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
|
||||
# Expected Compression Ratio: 1.0
|
||||
#
|
||||
# VISUALLY_EQUAL
|
||||
# Only visible block/biome changes are recorded in the database.
|
||||
# Hidden blocks (IE ores) are ignored.
|
||||
# Expected Compression Ratio: 0.7
|
||||
worldCompression = "VISUALLY_EQUAL"
|
||||
#
|
||||
# Requires a level restart to apply.
|
||||
#
|
||||
# Setting this to a less strict mode increases DB read/write
|
||||
# speed at the cost of potential data loss/corruption if
|
||||
# the computer looses power
|
||||
# (unexpected program shutdown should be safe for all options).
|
||||
#
|
||||
# FULL
|
||||
# ACID - data will never be lost or corrupted
|
||||
# Slow
|
||||
# At max throughput: ~2k saves per sec, ~70% disk usage
|
||||
#
|
||||
# NORMAL
|
||||
# May not be durable - some data may be lost on power loss, but DB should be safe from corruption
|
||||
# Fast
|
||||
# At max throughput: ~14k saves per sec, ~40% disk usage
|
||||
#
|
||||
# OFF
|
||||
# Not consistent - database may be corrupted during power loss
|
||||
# Very Fast
|
||||
# At max throughput: ~16k saves per sec, ~6% disk usage
|
||||
databaseSyncMode = "NORMAL"
|
||||
dataCompression = "Z_STD_BLOCK"
|
||||
#
|
||||
# Enabling this will drastically speed up chunk processing time
|
||||
# if you are CPU limited.
|
||||
# Disabling it will drastically speed up chunk processing time
|
||||
# if you are disk speed/IO limited.
|
||||
#
|
||||
# Normally DH will attempt to skip creating LODs for chunks it's already seen
|
||||
# and that haven't changed.
|
||||
#
|
||||
# Disabling this option can also fix a rare bug where
|
||||
# modified chunks are skipped due to being too similar to
|
||||
# their saved hash.
|
||||
disableUnchangedChunkCheck = false
|
||||
|
||||
[common.multiThreading]
|
||||
#
|
||||
# How many threads should be used by Distant Horizons?
|
||||
numberOfThreads = 2
|
||||
#
|
||||
# A value between 1.0 and 0.0 that represents the percentage
|
||||
# of time each thread can run before going idle.
|
||||
#
|
||||
# This can be used to reduce CPU usage if the thread count
|
||||
# is already set to 1 for the given option, or more finely
|
||||
# tune CPU performance.
|
||||
threadRunTimeRatio = "1.0"
|
||||
#
|
||||
# What Java thread priority should DH's primary thread pools run with?
|
||||
#
|
||||
# You probably don't need to change this unless you are also
|
||||
# running C2ME and are seeing thread starvation in either C2ME or DH.
|
||||
threadPriority = 5
|
||||
|
||||
[common.logging]
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter.
|
||||
# This can be useful for debugging.
|
||||
logRendererEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the world generation process.
|
||||
# This can be useful for debugging.
|
||||
logWorldGenEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer OpenGL process.
|
||||
# This can be useful for debugging.
|
||||
logRendererGLEventToChat = "ERROR"
|
||||
#
|
||||
#
|
||||
globalChatMaxLevel = "ERROR"
|
||||
#
|
||||
# If enabled, the mod will log information about the world generation process.
|
||||
# This can be useful for debugging.
|
||||
logWorldGenChunkLoadEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer OpenGL process.
|
||||
# This can be useful for debugging.
|
||||
logRendererGLEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about network operations.
|
||||
# This can be useful for debugging.
|
||||
logNetworkEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, config changes sent by the server will be logged.
|
||||
logConnectionConfigChangesToFile = "WARN"
|
||||
#
|
||||
#
|
||||
globalFileMaxLevel = "INFO"
|
||||
|
||||
[common.logging.warning]
|
||||
#
|
||||
# If enabled, a message will be displayed in chat if explicit garbage collection
|
||||
# is disabled.
|
||||
# This is known to cause out-of-memory issues
|
||||
# and is better solved with a concurrent garbage collector.
|
||||
showExplicitGcDisabledWarning = false
|
||||
#
|
||||
# If enabled, a chat message will be displayed when DH has too many chunks
|
||||
# queued for updating.
|
||||
showSlowWorldGenSettingWarnings = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when DH has too many chunks
|
||||
# queued for updating.
|
||||
showUpdateQueueOverloadedChatWarning = false
|
||||
#
|
||||
# If enabled, a message will be logged if explicit garbage collection
|
||||
# is disabled.
|
||||
# This is known to cause out-of-memory issues
|
||||
# and is better solved with a concurrent garbage collector.
|
||||
logExplicitGcDisabledWarning = false
|
||||
#
|
||||
# If enabled, a chat message will be displayed if Java doesn't have enough
|
||||
# memory allocated to run DH well.
|
||||
showLowMemoryWarningOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when a replay is started
|
||||
# giving some basic information about how DH will function.
|
||||
showReplayWarningOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when a potentially problematic
|
||||
# mod is installed alongside DH.
|
||||
showModCompatibilityWarningsOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed if vanilla MC's
|
||||
# render distance is higher than the recommended amount.
|
||||
showHighVanillaRenderDistanceWarning = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed if DH detects
|
||||
# that any pooled objects have been garbage collected.
|
||||
showPoolInsufficientMemoryWarning = true
|
||||
#
|
||||
# If enabled, a message will be logged if the garbage
|
||||
# collector Java is currently using is known
|
||||
# to cause frame stuttering and/or other issues.
|
||||
logGarbageCollectorWarning = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed in chat if the garbage
|
||||
# collector Java is currently using is known
|
||||
# to cause frame stuttering and/or other issues.
|
||||
showGarbageCollectorWarning = true
|
||||
|
||||
[common.worldGenerator]
|
||||
#
|
||||
# How should distant generator progress be displayed?
|
||||
#
|
||||
# OVERLAY: may be the same as CHAT for some Minecraft versions
|
||||
# CHAT
|
||||
# LOG
|
||||
# DISABLED
|
||||
showGenerationProgress = "DISABLED"
|
||||
#
|
||||
# If the SURFACE_THEN_CHUNKS generator plan is
|
||||
# in use, this percentage can be defined to reduce how far the
|
||||
# second high-quality generation pass will go
|
||||
# as a percentage of your total render distance:
|
||||
# -1.0 = Infinity
|
||||
# 0.0 = 0%
|
||||
# 0.5 = 50%
|
||||
# 1.0 = 100%
|
||||
# 2.0 = 200%
|
||||
#
|
||||
# Does nothing on a dedicated server.
|
||||
surfaceRegenMaxDistancePercent = "-1.0"
|
||||
#
|
||||
# The max radius in chunks around the central point where world generation is allowed.
|
||||
# If this value is set to 0, generation bounds are disabled and the render distance will be used.
|
||||
#
|
||||
# This should only be set if you have a pre-generated world that has a very limited size.
|
||||
# Setting this on a normal MC world will prevent the world generator from filling
|
||||
# out your render distance.
|
||||
generationMaxChunkRadius = 0
|
||||
#
|
||||
# If the Minecraft server thread is taking too long
|
||||
# Distant Horizons will pause world gen to reduce server stuttering/lag.
|
||||
limitIfServerUnhealthy = true
|
||||
#
|
||||
# How often should the distant generator progress be displayed?
|
||||
generationProgressDisplayIntervalInSeconds = 2
|
||||
#
|
||||
# When logging generation progress also include the rate at which chunks
|
||||
# are being generated.
|
||||
# This can be useful for troubleshooting performance.
|
||||
generationProgressIncludeChunksPerSecond = true
|
||||
#
|
||||
# How detailed should LODs be generated outside the vanilla render distance?
|
||||
#
|
||||
# PRE_EXISTING_ONLY
|
||||
# Only create LOD data for already generated chunks.
|
||||
#
|
||||
# FEATURES
|
||||
# Generate everything except structures.
|
||||
# Note: This may cause world generator bugs or instability when paired with certain world generator mods.
|
||||
#
|
||||
# INTERNAL_SERVER
|
||||
# Ask the local server to generate/load each chunk.
|
||||
# This is the most compatible and will generate structures correctly,
|
||||
# but may cause server/simulation lag.
|
||||
# Note: unlike other modes this option DOES save generated chunks to
|
||||
# Minecraft's region files.
|
||||
chunkGeneratorMode = "FEATURES"
|
||||
#
|
||||
# Defines how LODs will be generated
|
||||
# outside the vanilla render distance.
|
||||
# In multiplayer, this may be overridden by the server.
|
||||
#
|
||||
# SURFACE_THEN_CHUNKS
|
||||
# The rough surface will be generated first
|
||||
# then chunks will be generated to fill in
|
||||
# missing features (ie trees and villages).
|
||||
#
|
||||
# Recommended for modded or vanilla worlds.
|
||||
#
|
||||
# SURFACE_ONLY
|
||||
# Only the rough surface will be generated.
|
||||
# Chunk features like trees and villages
|
||||
# won't be generated.
|
||||
#
|
||||
# Recommended for computers that don't have
|
||||
# enough CPU power or RAM space to handle
|
||||
# the chunk generator.
|
||||
#
|
||||
# CHUNKS_ONLY
|
||||
# Slower than either surface options,
|
||||
# but provides more accurate terrain.
|
||||
#
|
||||
# Recommended for custom worlds
|
||||
# where the surface generator would
|
||||
# generate incorrect terrain.
|
||||
#
|
||||
# DISABLED
|
||||
# No distant terrain will be generated.
|
||||
generatorPlan = "SURFACE_THEN_CHUNKS"
|
||||
#
|
||||
# If true and the player/camera is moving quickly
|
||||
# Distant Horizons will pause world gen to improve loading speed
|
||||
# and reduce CPU load.
|
||||
pauseIfMovingQuickly = true
|
||||
#
|
||||
# For how many seconds should instructions for disabling the distant generator progress be displayed?
|
||||
# Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.
|
||||
generationProgressDisableMessageDisplayTimeInSeconds = 20
|
||||
#
|
||||
# The center Z chunk position that the world gen max radius is centered around.
|
||||
generationCenterChunkZ = 0
|
||||
#
|
||||
# The center X chunk position that the world gen max radius is centered around.
|
||||
generationCenterChunkX = 0
|
||||
|
||||
[client]
|
||||
#
|
||||
# Should Distant Horizon's config button appear in Minecraft's options screen next to the fov slider?
|
||||
showDhOptionsButtonInMinecraftUi = true
|
||||
|
||||
[client.advanced.autoUpdater]
|
||||
#
|
||||
# If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build.
|
||||
# If [AUTO] is selected DH will update to new stable releases if the current jar is a stable jar
|
||||
# and will update to new nightly builds if the current jar is a nightly jar (IE the version number ends in '-dev').
|
||||
updateBranch = "AUTO"
|
||||
#
|
||||
# Automatically check for updates on game launch?
|
||||
enableAutoUpdater = true
|
||||
#
|
||||
# Should Distant Horizons silently, automatically download and install new versions?
|
||||
# This setting is force disabled on dedicated servers for stability reasons.
|
||||
enableSilentUpdates = false
|
||||
|
||||
[client.advanced.debugging]
|
||||
#
|
||||
# If enabled this will disable (most) vanilla Minecraft rendering.
|
||||
#
|
||||
# NOTE: Do not report any issues when this mode is on!
|
||||
# This setting is only for fun and debugging.
|
||||
# Mod compatibility is not guaranteed.
|
||||
lodOnlyMode = false
|
||||
#
|
||||
# What renderer is active?
|
||||
#
|
||||
# DEFAULT: Default lod renderer
|
||||
# DEBUG_TRIANGLE: Debug testing renderer
|
||||
# DISABLED: Disable rendering
|
||||
rendererMode = "DEFAULT"
|
||||
#
|
||||
# If true overlapping quads will be rendered as bright red for easy identification.
|
||||
# If false the quads will be rendered normally.
|
||||
showOverlappingQuadErrors = false
|
||||
#
|
||||
# If true OpenGL Buffer garbage collection will be logged
|
||||
# this also includes the number of live buffers.
|
||||
logBufferGarbageCollection = false
|
||||
#
|
||||
# Should specialized colors/rendering modes be used?
|
||||
#
|
||||
# OFF: LODs will be drawn with their normal colors.
|
||||
# SHOW_DETAIL: LODs' color will be based on their detail level.
|
||||
# SHOW_BLOCK_MATERIAL: LODs' color will be based on their material.
|
||||
# SHOW_OVERLAPPING_QUADS: LODs will be drawn with total white, but overlapping quads will be drawn with red.
|
||||
debugRenderingColors = "OFF"
|
||||
#
|
||||
# Stops vertex colors from being passed.
|
||||
# Useful for debugging shaders
|
||||
enableWhiteWorld = false
|
||||
#
|
||||
# If enabled the LODs will render as wireframe.
|
||||
renderWireframe = false
|
||||
#
|
||||
# If true several keys can be used to toggle debug states.
|
||||
# F6 - enable/disable LOD rendering
|
||||
# F7 - enable/disable LOD only rendering
|
||||
# F8 - cycle through the different debug rendering modes
|
||||
enableDebugKeybindings = false
|
||||
|
||||
[client.advanced.debugging.debugWireframe]
|
||||
#
|
||||
# Render LOD section status?
|
||||
showRenderSectionStatus = false
|
||||
#
|
||||
# Render queued network sync on load tasks?
|
||||
showNetworkSyncOnLoadQueue = false
|
||||
#
|
||||
# Render full data update/lock status?
|
||||
showFullDataUpdateStatus = false
|
||||
#
|
||||
# Render queued world gen tasks?
|
||||
showWorldGenQueue = false
|
||||
#
|
||||
# Render Quad Tree Rendering status?
|
||||
showQuadTreeRenderStatus = false
|
||||
#
|
||||
# If enabled, various wireframes for debugging internal functions will be drawn.
|
||||
#
|
||||
# NOTE: There WILL be performance hit!
|
||||
# Additionally, only stuff that's loaded after you enable this
|
||||
# will render their debug wireframes.
|
||||
enableRendering = false
|
||||
|
||||
[client.advanced.debugging.openGl]
|
||||
#
|
||||
# Defines how OpenGL errors are handled.
|
||||
# Requires rebooting Minecraft to change.
|
||||
# Will catch OpenGL errors thrown by other mods.
|
||||
overrideVanillaGLLogger = true
|
||||
#
|
||||
# Defines how OpenGL errors are handled.
|
||||
# May incorrectly catch OpenGL errors thrown by other mods.
|
||||
#
|
||||
# IGNORE: Do nothing.
|
||||
# LOG: write an error to the log.
|
||||
# LOG_THROW: write to the log and throw an exception.
|
||||
# Warning: this should only be enabled when debugging the LOD renderer
|
||||
# as it may break Minecraft's renderer when an exception is thrown.
|
||||
glErrorHandlingMode = "IGNORE"
|
||||
#
|
||||
# If true each Open GL error will only be logged once.
|
||||
# Enabling this may cause some error logs to be missed.
|
||||
# Does nothing if overrideVanillaGLLogger is set to false.
|
||||
#
|
||||
# Generally this can be kept as 'true' to prevent log spam.
|
||||
# However, Please set this to 'false' if a developer needs your log to debug a GL issue.
|
||||
onlyLogGlErrorsOnce = true
|
||||
|
||||
[client.advanced.debugging.exampleConfigScreen]
|
||||
shortTest = "69"
|
||||
mapTest = "{}"
|
||||
byteTest = "8"
|
||||
longTest = "42069"
|
||||
listTest = ["option 1", "option 2", "option 3"]
|
||||
boolTest = false
|
||||
doubleTest = "420.69"
|
||||
floatTest = "0.42069"
|
||||
linkableTest = 420
|
||||
intTest = 69420
|
||||
stringTest = "Test input box"
|
||||
|
||||
[client.advanced.debugging.f3Screen]
|
||||
#
|
||||
# Shows info about the render thread tasks.
|
||||
showRenderThreadTasks = false
|
||||
#
|
||||
# Shows how many chunks are queued for processing and the max count that can be queued.
|
||||
showQueuedChunkUpdateCount = true
|
||||
#
|
||||
# Shows the memory use and array counts for each DH object pool.
|
||||
showSeparatedObjectPools = false
|
||||
#
|
||||
# Shows the player's LOD position.
|
||||
showPlayerPos = true
|
||||
#
|
||||
# Shows the combined memory use and array counts for all DH pooled objects.
|
||||
showCombinedObjectPools = false
|
||||
#
|
||||
# Defines what internal detail level the player position will be shown as.
|
||||
# Internal detail level means: 6 = 1x1 block, 7 = 2x2 blocks, etc.
|
||||
playerPosSectionDetailLevel = 6
|
||||
#
|
||||
# Only show levels that DH is actively rendering.
|
||||
onlyShowRenderingLevels = true
|
||||
#
|
||||
# Shows info about each thread pool.
|
||||
showThreadPools = true
|
||||
#
|
||||
# Shows what levels are loaded and world gen/rendering info about those levels.
|
||||
showLevelStatus = true
|
||||
|
||||
[client.advanced.debugging.positionFinderDebugging]
|
||||
positionFinderEnable = false
|
||||
positionFinderMinBlockY = -64
|
||||
positionFinderZPos = 0
|
||||
positionFinderXPos = 0
|
||||
positionFinderMaxBlockY = 125
|
||||
positionFinderDetailLevel = 6
|
||||
positionFinderMarginPercent = "0.0"
|
||||
|
||||
[client.advanced.graphics]
|
||||
#
|
||||
# Enabling Anti-Aliasing smooths edges and reduces flickering when moving.
|
||||
enableAntiAliasing = true
|
||||
#
|
||||
# Enable Screen Space Ambient Occlusion
|
||||
enableSsao = false
|
||||
#
|
||||
# If true some vanilla graphics settings will be automatically changed
|
||||
# during DH setup to provide a better experience.
|
||||
#
|
||||
# IE disabling vanilla clouds (which render on top of DH LODs),
|
||||
# and chunk fading (DH already fades MC chunks)
|
||||
overrideVanillaGraphicsSettings = true
|
||||
|
||||
[client.advanced.graphics.culling]
|
||||
#
|
||||
# Determines how far from the camera Distant Horizons will start rendering.
|
||||
# Measured as a percentage of the vanilla render distance.
|
||||
#
|
||||
# -1 = auto, overdraw will change based on the vanilla render distance.
|
||||
#
|
||||
# Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,
|
||||
# but may cause holes in the world.
|
||||
# Holes are most likely to appear when flying through unloaded terrain.
|
||||
#
|
||||
# Increasing the vanilla render distance increases the effectiveness of this setting.
|
||||
overdrawPrevention = "-1.0"
|
||||
#
|
||||
# Identical to the other frustum culling option
|
||||
# only used when a shader mod is present using the DH API
|
||||
# and the shadow pass is being rendered.
|
||||
#
|
||||
# Disable this if shadows render incorrectly.
|
||||
disableShadowPassFrustumCulling = false
|
||||
#
|
||||
# A comma separated list of block resource locations that will be replaced by water
|
||||
# if they're visible on the water's surface.
|
||||
waterSubSurfaceBlockReplacementCsv = "minecraft:kelp,minecraft:tall_seagrass,minecraft:seagrass"
|
||||
#
|
||||
# A comma separated list of block resource locations that won't be rendered by DH.
|
||||
# Air is always included in this list.
|
||||
#
|
||||
# Note:
|
||||
# If you see gaps, or holes you may have to change
|
||||
# worldCompression to [MERGE_SAME_BLOCKS] and re-generate the LODs.
|
||||
ignoredRenderBlockCsv = "minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom"
|
||||
#
|
||||
# A comma separated list of block resource locations that will be removed
|
||||
# when on top of water.
|
||||
waterSurfaceBlockReplacementCsv = "minecraft:lily_pad"
|
||||
#
|
||||
# Defines what blocks should be rendered as LODs.
|
||||
#
|
||||
# NONE: Include all blocks in the LODs
|
||||
# NON_COLLIDING: Only render solid blocks in the LODs (tall grass, torches, etc. will be ignored)
|
||||
blocksToIgnore = "NON_COLLIDING"
|
||||
#
|
||||
# If true LODs outside the player's camera
|
||||
# aren't drawn, increasing GPU performance.
|
||||
#
|
||||
# If false all LODs are drawn, even those behind
|
||||
# the player's camera, decreasing GPU performance.
|
||||
#
|
||||
# Disable this if you see LODs disappearing at the corners of your vision.
|
||||
disableFrustumCulling = false
|
||||
#
|
||||
# If false all beacons near the camera won't be drawn to prevent vanilla overdraw.
|
||||
# If true all beacons will be rendered.
|
||||
#
|
||||
# Generally this should be left as true. It's main purpose is for debugging
|
||||
# beacon updating/rendering.
|
||||
disableBeaconDistanceCulling = true
|
||||
#
|
||||
# Should the blocks underneath avoided blocks gain the color of the avoided block?
|
||||
#
|
||||
# True: a red flower will tint the grass below it red.
|
||||
# False: skipped blocks will not change color of surface below them.
|
||||
tintWithAvoidedBlocks = true
|
||||
#
|
||||
# If enabled caves won't be rendered.
|
||||
#
|
||||
# Note: for some world types this can cause
|
||||
# overhangs or walls for floating objects.
|
||||
# Tweaking the caveCullingHeight, can resolve some
|
||||
# of those issues.
|
||||
enableCaveCulling = true
|
||||
#
|
||||
# At what Y value should cave culling start?
|
||||
# Lower this value if you get walls for areas with 0 light.
|
||||
caveCullingHeight = 60
|
||||
#
|
||||
# A comma separated list of block resource locations that shouldn't be rendered
|
||||
# if they are in a 0 sky light underground area.
|
||||
# Air is always included in this list.
|
||||
#
|
||||
# Defaults to an empty list since most cave blocks will be automatically ignored due to being:
|
||||
# transparent, non-solid, or liquids, but new blocks can be added here if needed.
|
||||
ignoredRenderCaveBlockCsv = ""
|
||||
#
|
||||
# If set to true the overdraw prevention radius will get closer
|
||||
# to the camera when flying/moving quickly.
|
||||
#
|
||||
# This helps reduce issues where Minecraft can't load or
|
||||
# generate chunks fast enough to keep up with DH.
|
||||
reduceOverdrawWithFastMovement = true
|
||||
|
||||
[client.advanced.graphics.texture]
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH will render their sides using the bottom texture.
|
||||
# Partial matches/incomplete resource locations will also match.
|
||||
#
|
||||
# Example: "minecraft:grass_block,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksDontUseSideTextureCsv = "grass_block,mycelium,nylium,dirt_path,grass"
|
||||
#
|
||||
# The highest detail level number that can render with block textures.
|
||||
# At higher detail levels each LOD covers multiple blocks,
|
||||
# which can essentially make textures smaller than a pixel and therefore invisible.
|
||||
#
|
||||
# Larger numbers texture more distant LODs
|
||||
# but increase memory usage.
|
||||
maxTexturedLodDetailLevel = 2
|
||||
#
|
||||
# If true nearby and zoomed-in LODs will render with their block's
|
||||
# actual texture instead of a single flat color.
|
||||
#
|
||||
# Only applies to high detail LODs where the texture is actually visible,
|
||||
# distant LODs always use flat colors.
|
||||
# Some shader packs nay not have an affect.
|
||||
enableTexturedLods = true
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH won't render textures on.
|
||||
# Partial matches/incomplete resource locations will also match.
|
||||
#
|
||||
# Example: "minecraft:grass_block,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksDontRenderTextureCsv = "minecraft:bamboo"
|
||||
#
|
||||
# A comma separated list of tag names
|
||||
# that DH will render their sides using the bottom texture.
|
||||
#
|
||||
# Example: "grass_blocks,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blockTagsDontUseSideTextureCsv = "grass_blocks,nylium"
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH will use rasterization to determine
|
||||
# the face textures.
|
||||
#
|
||||
# Can be used to fix issues with mods/blocks where
|
||||
# a side uses just part of a modeled block's texture.
|
||||
#
|
||||
# Example: "minecraft:beacon"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksAlwaysRasterizeTextureCsv = "minecraft:beacon"
|
||||
|
||||
[client.advanced.graphics.noiseTexture]
|
||||
#
|
||||
# Should a noise texture be applied to LODs?
|
||||
#
|
||||
# This is done to simulate textures and make the LODs appear more detailed.
|
||||
enableNoiseTexture = true
|
||||
#
|
||||
# Defines how far should the noise texture render before it fades away. (in blocks)
|
||||
# Set to 0 to disable noise from fading away
|
||||
noiseDropoff = 1024
|
||||
#
|
||||
# How many steps of noise should be applied to LODs?
|
||||
noiseSteps = 4
|
||||
#
|
||||
# How intense should the noise should be?
|
||||
noiseIntensity = "0.05"
|
||||
|
||||
[client.advanced.graphics.experimental]
|
||||
#
|
||||
# A comma separated list of dimension resource locations where DH won't render.
|
||||
#
|
||||
# Example: "minecraft:the_nether,minecraft:the_end"
|
||||
# For Minecraft 1.12.2 and older:
|
||||
# Open the F3 screen and copy the dimension_name:ID shown in yellow. Example: "overworld:0"
|
||||
#
|
||||
# Note:
|
||||
# Some DH settings will be disabled and/or changed to improve
|
||||
# visuals when DH rendering is disabled.
|
||||
ignoredDimensionCsv = ""
|
||||
#
|
||||
# Requires a restart to change.
|
||||
#
|
||||
# Options:
|
||||
# AUTO - changes based on the most likely API for that MC version
|
||||
# OPEN_GL - The Default for MC 1.21.11 and older (supports Iris shaders)
|
||||
# BLAZE_3D - The Default for MC 26.1.2 and newer (supports Vulkan)
|
||||
renderingEngine = "AUTO"
|
||||
#
|
||||
# This is the earth size ratio when applying the curvature shader effect.
|
||||
# Note: Enabling this feature may cause rendering bugs.
|
||||
#
|
||||
# 0 = flat/disabled
|
||||
# 1 = 1 to 1 (6,371,000 blocks)
|
||||
# 100 = 1 to 100 (63,710 blocks)
|
||||
# 10000 = 1 to 10000 (637.1 blocks)
|
||||
#
|
||||
# Note: Due to current limitations, the min value is [50]
|
||||
# and the max value is 5000. Any values outside this range
|
||||
# will be set to 0 (disabled).
|
||||
earthCurveRatio = 0
|
||||
|
||||
[client.advanced.graphics.genericRendering]
|
||||
#
|
||||
# A comma separated separated list of dimension resource locations where DH clouds will render.
|
||||
#
|
||||
# Example: "minecraft:overworld,minecraft:the_end"
|
||||
#
|
||||
# Changes require a world re-load.
|
||||
dimensionEnabledCloudRenderingCsv = "minecraft:overworld"
|
||||
#
|
||||
# If true LOD clouds will be rendered.
|
||||
enableCloudRendering = true
|
||||
#
|
||||
# If true LOD beacon beams will be rendered wider at extreme distances,
|
||||
# making them easier to see.
|
||||
# If false all LOD beacon beams will only ever be 1 block wide.
|
||||
expandDistantBeacons = true
|
||||
#
|
||||
# Sets the maximum height beacons will render up to.
|
||||
#
|
||||
# Requires a world re-load to take affect.
|
||||
beaconRenderHeight = 6000
|
||||
#
|
||||
# If true LOD beacon beams will be rendered.
|
||||
enableBeaconRendering = true
|
||||
#
|
||||
# If true non terrain objects will be rendered by DH.
|
||||
# i.e. beacon beams and clouds.
|
||||
enableGenericRendering = true
|
||||
#
|
||||
# False = DH will render a single layer of clouds, like vanilla Minecraft.
|
||||
# True = DH will render 3 layers of clouds at different heights.
|
||||
enableMultiLayerClouds = true
|
||||
|
||||
[client.advanced.graphics.quality]
|
||||
#
|
||||
# What is the maximum detail LODs can render at?
|
||||
# Higher settings will increase memory and GPU usage.
|
||||
#
|
||||
# CHUNK: render 1 LOD for each Chunk.
|
||||
# HALF_CHUNK: render 4 LODs for each Chunk.
|
||||
# FOUR_BLOCKS: render 16 LODs for each Chunk.
|
||||
# TWO_BLOCKS: render 64 LODs for each Chunk.
|
||||
# BLOCK: render 256 LODs for each Chunk (width of one block).
|
||||
#
|
||||
# Fastest: CHUNK
|
||||
# Fanciest: BLOCK
|
||||
maxHorizontalResolution = "BLOCK"
|
||||
#
|
||||
# If true LODs will fade away as you get closer to them.
|
||||
# If false LODs will cut off abruptly at a set distance from the camera.
|
||||
# This setting is affected by the vanilla overdraw prevention config.
|
||||
ditherDhFade = true
|
||||
#
|
||||
# If true DH will try to use the camera position when
|
||||
# determining LOD quality drop-off.
|
||||
# If false DH will use the player's position.
|
||||
#
|
||||
# Enabling helps free-cam mods render correctly.
|
||||
# Disabling helps multi-camera mods render correctly (ie Immersive Portals or camera mods).
|
||||
useCameraPositionForQualityDropOff = true
|
||||
#
|
||||
# How bright LOD colors are.
|
||||
#
|
||||
# 0 = black
|
||||
# 1 = normal
|
||||
# 2 = near white
|
||||
brightnessMultiplier = "1.0"
|
||||
#
|
||||
# How many detail levels zooming in can increase LOD quality by.
|
||||
# Higher numbers allow stronger zooms to render crisper terrain,
|
||||
# but will increase memory and GPU usage while zoomed in.
|
||||
#
|
||||
# A vanilla spyglass needs 4 detail levels to reach its full quality.
|
||||
maxZoomQualityIncrease = 4
|
||||
#
|
||||
# How should LODs be shaded?
|
||||
#
|
||||
# AUTO: Uses the same side shading as vanilla Minecraft blocks.
|
||||
# ENABLED: Simulates Minecraft's block shading for LODs.
|
||||
# Can be used to force LOD shading when using some shaders.
|
||||
# DISABLED: All LOD sides will be rendered with the same brightness.
|
||||
lodShading = "AUTO"
|
||||
#
|
||||
# How saturated LOD colors are.
|
||||
#
|
||||
# 0 = black and white
|
||||
# 1 = normal
|
||||
# 2 = very saturated
|
||||
saturationMultiplier = "1.0"
|
||||
#
|
||||
# This indicates how well LODs will represent
|
||||
# overhangs, caves, floating islands, etc.
|
||||
# Higher options will make the world more accurate, butwill increase memory and GPU usage.
|
||||
#
|
||||
# Lowest Quality: HEIGHT_MAP
|
||||
# Highest Quality: PIXEL_ART
|
||||
verticalQuality = "LOW"
|
||||
#
|
||||
# The radius of the mod's render distance. (measured in chunks)
|
||||
#
|
||||
# Note: this is a best effort number.
|
||||
# The actual render distance may be above or below this number
|
||||
# depending on your other graphic settings.
|
||||
lodChunkRenderDistanceRadius = 64
|
||||
#
|
||||
# How should the sides and bottom of grass block LODs render?
|
||||
#
|
||||
# AS_GRASS: all sides of dirt LOD's render using the top (green) color.
|
||||
# FADE_TO_DIRT: sides fade from grass to dirt.
|
||||
# AS_DIRT: sides render entirely as dirt.
|
||||
grassSideRendering = "FADE_TO_DIRT"
|
||||
#
|
||||
# This indicates how far apart drops in LOD quality are.
|
||||
#
|
||||
# Higher settings will increase the distance between drops
|
||||
# but will increase memory and GPU usage.
|
||||
horizontalQuality = "LOW"
|
||||
#
|
||||
# How should LOD transparency be handled.
|
||||
#
|
||||
# COMPLETE: LODs will render transparent.
|
||||
# DISABLED: LODs will be opaque.
|
||||
transparency = "DISABLED"
|
||||
#
|
||||
# This is the same as vanilla Biome Blending settings for Lod area.
|
||||
# Note: anything above '0' will slow down LOD loading time.
|
||||
#
|
||||
# '0' equals to Vanilla Biome Blending of '1x1' or 'OFF',
|
||||
# '1' equals to Vanilla Biome Blending of '3x3',
|
||||
# '2' equals to Vanilla Biome Blending of '5x5'...
|
||||
lodBiomeBlending = 1
|
||||
#
|
||||
# How should vanilla Minecraft fade into Distant Horizons LODs?
|
||||
#
|
||||
# NONE: Fastest, there will be a pronounced border between DH and MC rendering.
|
||||
# SINGLE_PASS: Fades after MC's transparent pass, opaque blocks underwater won't be faded.
|
||||
# DOUBLE_PASS: Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition.
|
||||
vanillaFadeMode = "SINGLE_PASS"
|
||||
#
|
||||
# If true LOD quality will increase when the camera is zoomed in,
|
||||
# IE when using a spyglass or zoom mod.
|
||||
#
|
||||
# Only LODs visible through the camera view are affected.
|
||||
#
|
||||
# When zoomed in LODs will load to the same detail level
|
||||
# they would have if you were close to them.
|
||||
increaseQualityWhenZoomedIn = true
|
||||
|
||||
[client.advanced.graphics.fog]
|
||||
#
|
||||
# Should Minecraft's fog render?
|
||||
# Note: Other mods may conflict with this setting.
|
||||
enableVanillaFog = false
|
||||
#
|
||||
# What is the maximum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
farFogMax = "1.0"
|
||||
#
|
||||
# Determines if fog is drawn on DH LODs.
|
||||
enableDhFog = true
|
||||
#
|
||||
# At what distance should the far fog start?
|
||||
#
|
||||
# 0.0: Fog starts at the player's position.
|
||||
# 1.0: Fog starts at the closest edge of the vanilla render distance.
|
||||
# 1.414: Fog starts at the corner of the vanilla render distance.
|
||||
farFogStart = "0.4"
|
||||
#
|
||||
# What is the minimum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
farFogMin = "0.0"
|
||||
#
|
||||
# What color should fog use?
|
||||
#
|
||||
# USE_WORLD_FOG_COLOR: Use the world's fog color.
|
||||
# USE_SKY_COLOR: Use the sky's color.
|
||||
colorMode = "USE_WORLD_FOG_COLOR"
|
||||
#
|
||||
# How should the fog thickness should be calculated?
|
||||
#
|
||||
# LINEAR: Linear based on distance (will ignore 'density')
|
||||
# EXPONENTIAL: 1/(e^(distance*density))
|
||||
# EXPONENTIAL_SQUARED: 1/(e^((distance*density)^2)
|
||||
farFogFalloff = "EXPONENTIAL_SQUARED"
|
||||
#
|
||||
# Used in conjunction with the Fog Falloff.
|
||||
farFogDensity = "2.5"
|
||||
#
|
||||
# Where should the far fog end?
|
||||
#
|
||||
# 0.0: Fog ends at player's position.
|
||||
# 1.0: Fog ends at the closest edge of the vanilla render distance.
|
||||
# 1.414: Fog ends at the corner of the vanilla render distance.
|
||||
farFogEnd = "1.0"
|
||||
|
||||
[client.advanced.graphics.fog.heightFog]
|
||||
#
|
||||
# Where should the height fog start?
|
||||
#
|
||||
# ABOVE_CAMERA: Height fog starts at the camera and goes towards the sky
|
||||
# BELOW_CAMERA: Height fog starts at the camera and goes towards the void
|
||||
# ABOVE_AND_BELOW_CAMERA: Height fog starts from the camera to goes towards both the sky and void
|
||||
# ABOVE_SET_HEIGHT: Height fog starts from a set height and goes towards the sky
|
||||
# BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards the void
|
||||
# ABOVE_AND_BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards both the sky and void
|
||||
heightFogDirection = "BELOW_SET_HEIGHT"
|
||||
#
|
||||
# What is the minimum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
heightFogMin = "0.0"
|
||||
#
|
||||
# If the height fog is calculated around a set height, what is that height position?
|
||||
heightFogBaseHeight = "80.0"
|
||||
#
|
||||
# What is the maximum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
heightFogMax = "1.0"
|
||||
#
|
||||
# How should the height fog thickness should be calculated?
|
||||
#
|
||||
# LINEAR: Linear based on height (will ignore 'density')
|
||||
# EXPONENTIAL: 1/(e^(height*density))
|
||||
# EXPONENTIAL_SQUARED: 1/(e^((height*density)^2)
|
||||
heightFogFalloff = "EXPONENTIAL_SQUARED"
|
||||
#
|
||||
# What is the height fog's density?
|
||||
heightFogDensity = "20.0"
|
||||
#
|
||||
# How should height effect the fog thickness?
|
||||
# Note: height fog is combined with the other fog settings.
|
||||
#
|
||||
# SPHERICAL: Fog is calculated based on camera distance.
|
||||
# CYLINDRICAL: Ignore height, fog is calculated based on horizontal distance.
|
||||
#
|
||||
# MAX: max(heightFog, farFog)
|
||||
# ADDITION: heightFog + farFog
|
||||
# MULTIPLY: heightFog * farFog
|
||||
# INVERSE_MULTIPLY: 1 - (1-heightFog) * (1-farFog)
|
||||
# LIMITED_ADDITION: farFog + max(farFog, heightFog)
|
||||
# MULTIPLY_ADDITION: farFog + farFog * heightFog
|
||||
# INVERSE_MULTIPLY_ADDITION: farFog + 1 - (1-heightFog) * (1-farFog)
|
||||
# AVERAGE: farFog*0.5 + heightFog*0.5
|
||||
heightFogMixMode = "SPHERICAL"
|
||||
#
|
||||
# Should the start of the height fog be offset?
|
||||
#
|
||||
# 0.0: Fog start with no offset.
|
||||
# 1.0: Fog start with offset of the entire world's height. (Includes depth)
|
||||
heightFogStart = "0.0"
|
||||
#
|
||||
# Should the end of the height fog be offset?
|
||||
#
|
||||
# 0.0: Fog end with no offset.
|
||||
# 1.0: Fog end with offset of the entire world's height. (Include depth)
|
||||
heightFogEnd = "0.6"
|
||||
|
||||
[client.advanced.multiplayer]
|
||||
#
|
||||
# How should multiplayer save folders should be named?
|
||||
#
|
||||
# NAME_ONLY: Example: "Minecraft Server"
|
||||
# IP_ONLY: Example: "192.168.1.40"
|
||||
# NAME_IP: Example: "Minecraft Server IP 192.168.1.40"
|
||||
# NAME_IP_PORT: Example: "Minecraft Server IP 192.168.1.40:25565"NAME_IP_PORT_MC_VERSION: Example: "Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5"
|
||||
serverFolderNameMode = "NAME_ONLY"
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"allowedCEM": "ALL",
|
||||
"logModelCreationData": false,
|
||||
"debugOnRightClick": false,
|
||||
"renderModeChoice": "NORMAL",
|
||||
"vanillaModelHologramRenderMode_2": "OFF",
|
||||
"modelExportMode": "NONE",
|
||||
"automaticModelExporting": false,
|
||||
"attemptPhysicsModPatch_2": "CUSTOM",
|
||||
"modelUpdateFrequency": "Slow",
|
||||
"entityRenderModeOverrides": {},
|
||||
"entityPhysicsModPatchOverrides": {},
|
||||
"entityVanillaHologramOverrides": {},
|
||||
"modelsNamesDisabled": [],
|
||||
"allowEBEModConfigModify": true,
|
||||
"animationLODDistance": 8,
|
||||
"retainDetailOnLowFps": false,
|
||||
"retainDetailOnLargerMobs": true,
|
||||
"animationFrameSkipDuringIrisShadowPass": true,
|
||||
"preventFirstPersonHandAnimating": false,
|
||||
"onlyClientPlayerModel": false,
|
||||
"doubleChestAnimFix": true,
|
||||
"enforceOptifineVariationRequiresDefaultModel": false,
|
||||
"enforceOptifineVariationRequiresDefaultModel_v2": false,
|
||||
"resetPlayerModelEachRender": true,
|
||||
"resetPlayerModelEachRender_v2": true,
|
||||
"onlyDebugRenderOnHover": false,
|
||||
"enforceOptifineSubFoldersVariantOnly": false,
|
||||
"enforceOptiFineAnimSyntaxLimits": true,
|
||||
"allowOptifineFallbackProperties": true,
|
||||
"showReloadErrorToast": true,
|
||||
"exportRotations": false,
|
||||
"asmMaths": true,
|
||||
"logASM": false,
|
||||
"parrotShoulderPositionAnimatesByDefault": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#Select the backend to use. Set to "DEFAULT" to let Flywheel decide.
|
||||
backend = "DEFAULT"
|
||||
#Enable or disable instance update limiting with distance.
|
||||
limitUpdates = true
|
||||
#The number of worker threads to use. Set to -1 to let Flywheel decide. Set to 0 to disable parallelism. Requires a game restart to take effect.
|
||||
# Default: -1
|
||||
# Range: -1 ~ 4
|
||||
workerThreads = -1
|
||||
|
||||
#Config options for Flywheel's built-in backends.
|
||||
[flw_backends]
|
||||
#How smooth Flywheel's shader-based lighting should be. May have a large performance impact.
|
||||
#Allowed Values: FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED
|
||||
lightSmoothness = "FLAT"
|
||||
@@ -0,0 +1,16 @@
|
||||
mode = "fastest"
|
||||
chunk_rebuild_scheduler = "culling"
|
||||
|
||||
[adaptive_ticking]
|
||||
slow = 5
|
||||
slower = 8
|
||||
background_sleep = true
|
||||
|
||||
[light_sources]
|
||||
entities = false
|
||||
self = true
|
||||
water_sensitive_check = true
|
||||
beam = true
|
||||
guardian_laser = true
|
||||
sonic_boom = true
|
||||
glowing_effect = true
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"quality": {
|
||||
"weather_quality": "FAST",
|
||||
"leaves_quality": "FAST",
|
||||
"enable_vignette": true,
|
||||
"use_closest_point_entity_sort": false
|
||||
},
|
||||
"advanced": {
|
||||
"enable_memory_tracing": false,
|
||||
"use_advanced_staging_buffers": true,
|
||||
"cpu_render_ahead_limit": 3
|
||||
},
|
||||
"performance": {
|
||||
"chunk_builder_threads": 0,
|
||||
"chunk_build_defer_mode": "ALWAYS",
|
||||
"animate_only_visible_textures": true,
|
||||
"use_entity_culling": true,
|
||||
"use_fog_occlusion": true,
|
||||
"use_block_face_culling": true,
|
||||
"use_no_error_g_l_context": true,
|
||||
"quad_splitting_mode": "SAFE"
|
||||
},
|
||||
"notifications": {
|
||||
"has_cleared_donation_button": false,
|
||||
"has_seen_donation_prompt": false
|
||||
},
|
||||
"debug": {
|
||||
"terrain_sorting_enabled": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
|
||||
create\:cogs=4
|
||||
create\:mixing=4
|
||||
create\:mixing_compounded_1=4
|
||||
create\:mechanical_press_activation=4
|
||||
create\:mechanical_press_activation_compounded_1=4
|
||||
create\:mechanical_press_activation_belt=4
|
||||
create\:mechanical_press_activation_belt_compounded_1=4
|
||||
create\:crushing_1=4
|
||||
create\:crushing_2=4
|
||||
create\:crushing_3=4
|
||||
create\:saw_activate_stone=4
|
||||
create\:saw_activate_wood=4
|
||||
create\:funnel_flap=4
|
||||
create\:funnel_flap_compounded_1=4
|
||||
create\:depot_slide=4
|
||||
create\:depot_plop=4
|
||||
create\:steam=4
|
||||
create\:sanding_short=4
|
||||
create\:sanding_long=4
|
||||
@@ -0,0 +1,99 @@
|
||||
# Enables/Disables all sound effects
|
||||
enabled=true
|
||||
# Affects how quiet a sound gets based on distance
|
||||
# Lower values mean distant sounds are louder
|
||||
# This setting requires you to be in singleplayer or having the mod installed on the server
|
||||
# 1.0 is the physically correct value
|
||||
attenuation_factor=1.0
|
||||
# The ray distance at which reverb starts
|
||||
# 0.0 disables reverb attenuation
|
||||
reverb_attenuation_distance=0.0
|
||||
# The volume of simulated reverberations
|
||||
reverb_gain=1.0
|
||||
# The brightness of reverberation
|
||||
# Higher values result in more high frequencies in reverberation
|
||||
# Lower values give a more muffled sound to the reverb
|
||||
reverb_brightness=1.0
|
||||
# The distance of reverb relative to the sound distance
|
||||
reverb_distance=1.5
|
||||
# The amount of sound that will be absorbed when traveling through blocks
|
||||
block_absorption=1.0
|
||||
# Higher values mean smaller objects won't be considered as occluding
|
||||
occlusion_variation=0.35
|
||||
# The default amount of sound reflectance energy for all blocks
|
||||
# Lower values result in more conservative reverb simulation with shorter reverb tails
|
||||
# Higher values result in more generous reverb simulation with higher reverb tails
|
||||
default_block_reflectivity=0.5
|
||||
# The default amount of occlusion for all blocks
|
||||
# Lower values will result in sounds being less muffled through walls
|
||||
# Higher values mean sounds will be not audible though thicker walls
|
||||
default_block_occlusion_factor=1.0
|
||||
# Minecraft won't allow sounds to play past a certain distance
|
||||
# This parameter is a multiplier for how far away a sound source is allowed to be in order for it to actually play
|
||||
# This setting only takes affect in singleplayer worlds and when installed on the server
|
||||
sound_distance_allowance=4.0
|
||||
# A value controlling the amount that air absorbs high frequencies with distance
|
||||
# A value of 1.0 is physically correct for air with normal humidity and temperature
|
||||
# Higher values mean air will absorb more high frequencies with distance
|
||||
# 0 disables this effect
|
||||
air_absorption=1.0
|
||||
# How much sound is filtered when the player is underwater
|
||||
# 0.0 means no filter
|
||||
# 1.0 means fully filtered
|
||||
underwater_filter=0.9
|
||||
# Whether sounds like cave, nether or underwater ambient sounds should have sound physics
|
||||
evaluate_ambient_sounds=false
|
||||
# The number of rays to trace to determine reverberation for each sound source
|
||||
# More rays provides more consistent tracing results but takes more time to calculate
|
||||
# Decrease this value if you experience lag spikes when sounds play
|
||||
environment_evaluation_ray_count=16
|
||||
# The number of rays bounces to trace to determine reverberation for each sound source
|
||||
# More bounces provides more echo and sound ducting but takes more time to calculate
|
||||
# Decrease this value if you experience lag spikes when sounds play
|
||||
environment_evaluation_ray_bounces=2
|
||||
# If sound hits a non-full-square side, block occlusion is multiplied by this
|
||||
non_full_block_occlusion_factor=0.25
|
||||
# The maximum amount of rays to determine occlusion
|
||||
# Directly correlates to the amount of blocks between walls that are considered
|
||||
max_occlusion_rays=16
|
||||
# The amount at which occlusion is capped
|
||||
max_occlusion=64.0
|
||||
# If enabled, the occlusion calculation only uses one path between the sound source and the listener instead of 9
|
||||
strict_occlusion=true
|
||||
# Whether to try calculating where the sound should come from based on reflections
|
||||
sound_direction_evaluation=true
|
||||
# Skip redirecting non-occluded sounds (the ones you can see directly)
|
||||
redirect_non_occluded_sounds=true
|
||||
# If music discs or other longer sounds should be frequently reevaluated
|
||||
update_moving_sounds=false
|
||||
# The interval in ticks that moving sounds are reevaluated
|
||||
# Lower values mean more frequent reevaluation but also more lag
|
||||
# This option only takes effect if update_moving_sounds is enabled
|
||||
sound_update_interval=5
|
||||
# The maximum distance a sound can be processed
|
||||
max_sound_processing_distance=512.0
|
||||
# Disable level clone and cache. This will fall back to original main thread access.
|
||||
# WARNING! Enabling this will cause instability and issues with other mods.
|
||||
unsafe_level_access=false
|
||||
# The radius of chunks to clone for level access
|
||||
level_clone_range=4
|
||||
# The maximum number of ticks to retain the cloned level in the cache
|
||||
level_clone_max_retain_ticks=20
|
||||
# The maximum distance a player can move from the cloned origin before invalidation
|
||||
level_clone_max_retain_block_distance=16
|
||||
# Enables debug logging
|
||||
debug_logging=false
|
||||
# Provides more information about occlusion in the logs
|
||||
occlusion_logging=false
|
||||
# Provides more information about the environment calculation in the logs
|
||||
environment_logging=false
|
||||
# Provides more information about how long computations take
|
||||
performance_logging=false
|
||||
# If enabled, the path of the sound will be rendered in game
|
||||
render_sound_bounces=false
|
||||
# If enabled, occlusion will be visualized in game
|
||||
render_occlusion=false
|
||||
# Enables/Disables sound effects for Simple Voice Chat audio
|
||||
simple_voice_chat_integration=true
|
||||
# Enables/Disables hearing your own echo with Simple Voice Chat
|
||||
simple_voice_chat_hear_self=false
|
||||
@@ -0,0 +1,12 @@
|
||||
#The render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 128
|
||||
# Range: 1 ~ 1000000
|
||||
physicsRenderDistance = 16
|
||||
#The minimum render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 16
|
||||
# Range: 1 ~ 1000000
|
||||
minPhysicsRenderDistance = 16
|
||||
#The maximum render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 4096
|
||||
# Range: 1 ~ 1000000
|
||||
maxPhysicsRenderDistance = 4096
|
||||
@@ -0,0 +1,11 @@
|
||||
version:3955
|
||||
renderDistance:8
|
||||
simulationDistance:8
|
||||
biomeBlendRadius:0
|
||||
mipmapLevels:2
|
||||
entityDistanceScaling:0.75
|
||||
particles:1
|
||||
entityShadows:false
|
||||
graphicsMode:1
|
||||
maxFps:120
|
||||
enableVsync:true
|
||||
@@ -0,0 +1,980 @@
|
||||
_version = 4
|
||||
|
||||
[server]
|
||||
#
|
||||
# Defines the distance the player will receive updates around.
|
||||
realTimeUpdateDistanceRadiusInChunks = 256
|
||||
#
|
||||
# Prefix of the level keys sent to the clients.
|
||||
# If the mod is running behind a proxy, each backend should use a unique value.
|
||||
# If this value is empty, level key will be based on the server's seed hash.
|
||||
levelKeyPrefix = ""
|
||||
#
|
||||
# Defines the distance allowed to be synchronized around the player.
|
||||
# Should be the same or larger than maxGenerationRequestDistance in most cases.
|
||||
maxSyncOnLoadRequestDistance = 4096
|
||||
#
|
||||
# If true, clients will receive updated LODs when joining or loading new LODs.
|
||||
synchronizeOnLoad = true
|
||||
#
|
||||
# Custom server key used which can be used to always reuse the same LOD data folder,
|
||||
# for cases when the server doesn't have a static IP for some reason.
|
||||
# If this value is empty, the client itself decides which folder name to use.
|
||||
# Requires rejoining the server to apply after changing.
|
||||
serverKey = ""
|
||||
#
|
||||
# Defines the distance allowed to generate around the player.
|
||||
maxGenerationRequestDistance = 4096
|
||||
#
|
||||
# Makes the server send level keys for each world.
|
||||
# Disable this if you use alternative ways to send level keys.
|
||||
sendLevelKeys = true
|
||||
#
|
||||
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.
|
||||
# Autogenerated ID used to prevent multiple independent servers from accidentally
|
||||
# writing over each other's LODs when the same serverKey is set on both.
|
||||
#
|
||||
# How many LOD generation requests per second should a client send?
|
||||
# Also limits the number of client requests allowed to stay in the server's queue.
|
||||
generationRequestRateLimit = 20
|
||||
#
|
||||
# How many LOD sync requests per second should a client send?
|
||||
# Also limits the amount of player's requests allowed to stay in the server's queue.
|
||||
syncOnLoadRateLimit = 50
|
||||
#
|
||||
# If true, clients will receive real-time LOD updates for chunks outside the client's render distance.
|
||||
enableRealTimeUpdates = true
|
||||
#
|
||||
# Maximum global speed for uploading LODs to the clients, in KB/s.
|
||||
# Value of 0 disables the limit.
|
||||
globalBandwidthLimit = 0
|
||||
#
|
||||
# Maximum per-player speed for uploading LODs to the clients, in KB/s.
|
||||
# Value of 0 disables the limit.
|
||||
playerBandwidthLimit = 500
|
||||
#
|
||||
# Enables adaptive transfer speed based on client performance.
|
||||
# If true, DH will automatically adjust transfer rate to minimize connection lag.
|
||||
# If false, transfer speed will remain fixed.
|
||||
enableAdaptiveTransferSpeed = false
|
||||
|
||||
[common.lodBuilding]
|
||||
#
|
||||
# How should block data be compressed when creating LOD data?
|
||||
# This setting will only affect new or updated LOD data,
|
||||
# any data already generated when this setting is changed will be
|
||||
# unaffected until it is modified or re-loaded.
|
||||
#
|
||||
# MERGE_SAME_BLOCKS
|
||||
# Every block/biome change is recorded in the database.
|
||||
# This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
|
||||
# Expected Compression Ratio: 1.0
|
||||
#
|
||||
# VISUALLY_EQUAL
|
||||
# Only visible block/biome changes are recorded in the database.
|
||||
# Hidden blocks (IE ores) are ignored.
|
||||
# Expected Compression Ratio: 0.7
|
||||
worldCompression = "VISUALLY_EQUAL"
|
||||
#
|
||||
# Requires a level restart to apply.
|
||||
#
|
||||
# Setting this to a less strict mode increases DB read/write
|
||||
# speed at the cost of potential data loss/corruption if
|
||||
# the computer looses power
|
||||
# (unexpected program shutdown should be safe for all options).
|
||||
#
|
||||
# FULL
|
||||
# ACID - data will never be lost or corrupted
|
||||
# Slow
|
||||
# At max throughput: ~2k saves per sec, ~70% disk usage
|
||||
#
|
||||
# NORMAL
|
||||
# May not be durable - some data may be lost on power loss, but DB should be safe from corruption
|
||||
# Fast
|
||||
# At max throughput: ~14k saves per sec, ~40% disk usage
|
||||
#
|
||||
# OFF
|
||||
# Not consistent - database may be corrupted during power loss
|
||||
# Very Fast
|
||||
# At max throughput: ~16k saves per sec, ~6% disk usage
|
||||
databaseSyncMode = "NORMAL"
|
||||
dataCompression = "Z_STD_BLOCK"
|
||||
#
|
||||
# Enabling this will drastically speed up chunk processing time
|
||||
# if you are CPU limited.
|
||||
# Disabling it will drastically speed up chunk processing time
|
||||
# if you are disk speed/IO limited.
|
||||
#
|
||||
# Normally DH will attempt to skip creating LODs for chunks it's already seen
|
||||
# and that haven't changed.
|
||||
#
|
||||
# Disabling this option can also fix a rare bug where
|
||||
# modified chunks are skipped due to being too similar to
|
||||
# their saved hash.
|
||||
disableUnchangedChunkCheck = false
|
||||
|
||||
[common.multiThreading]
|
||||
#
|
||||
# How many threads should be used by Distant Horizons?
|
||||
numberOfThreads = 2
|
||||
#
|
||||
# A value between 1.0 and 0.0 that represents the percentage
|
||||
# of time each thread can run before going idle.
|
||||
#
|
||||
# This can be used to reduce CPU usage if the thread count
|
||||
# is already set to 1 for the given option, or more finely
|
||||
# tune CPU performance.
|
||||
threadRunTimeRatio = "1.0"
|
||||
#
|
||||
# What Java thread priority should DH's primary thread pools run with?
|
||||
#
|
||||
# You probably don't need to change this unless you are also
|
||||
# running C2ME and are seeing thread starvation in either C2ME or DH.
|
||||
threadPriority = 5
|
||||
|
||||
[common.logging]
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter.
|
||||
# This can be useful for debugging.
|
||||
logRendererEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the world generation process.
|
||||
# This can be useful for debugging.
|
||||
logWorldGenEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer OpenGL process.
|
||||
# This can be useful for debugging.
|
||||
logRendererGLEventToChat = "ERROR"
|
||||
#
|
||||
#
|
||||
globalChatMaxLevel = "ERROR"
|
||||
#
|
||||
# If enabled, the mod will log information about the world generation process.
|
||||
# This can be useful for debugging.
|
||||
logWorldGenChunkLoadEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer OpenGL process.
|
||||
# This can be useful for debugging.
|
||||
logRendererGLEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about network operations.
|
||||
# This can be useful for debugging.
|
||||
logNetworkEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, config changes sent by the server will be logged.
|
||||
logConnectionConfigChangesToFile = "WARN"
|
||||
#
|
||||
#
|
||||
globalFileMaxLevel = "INFO"
|
||||
|
||||
[common.logging.warning]
|
||||
#
|
||||
# If enabled, a message will be displayed in chat if explicit garbage collection
|
||||
# is disabled.
|
||||
# This is known to cause out-of-memory issues
|
||||
# and is better solved with a concurrent garbage collector.
|
||||
showExplicitGcDisabledWarning = false
|
||||
#
|
||||
# If enabled, a chat message will be displayed when DH has too many chunks
|
||||
# queued for updating.
|
||||
showSlowWorldGenSettingWarnings = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when DH has too many chunks
|
||||
# queued for updating.
|
||||
showUpdateQueueOverloadedChatWarning = false
|
||||
#
|
||||
# If enabled, a message will be logged if explicit garbage collection
|
||||
# is disabled.
|
||||
# This is known to cause out-of-memory issues
|
||||
# and is better solved with a concurrent garbage collector.
|
||||
logExplicitGcDisabledWarning = false
|
||||
#
|
||||
# If enabled, a chat message will be displayed if Java doesn't have enough
|
||||
# memory allocated to run DH well.
|
||||
showLowMemoryWarningOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when a replay is started
|
||||
# giving some basic information about how DH will function.
|
||||
showReplayWarningOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when a potentially problematic
|
||||
# mod is installed alongside DH.
|
||||
showModCompatibilityWarningsOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed if vanilla MC's
|
||||
# render distance is higher than the recommended amount.
|
||||
showHighVanillaRenderDistanceWarning = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed if DH detects
|
||||
# that any pooled objects have been garbage collected.
|
||||
showPoolInsufficientMemoryWarning = true
|
||||
#
|
||||
# If enabled, a message will be logged if the garbage
|
||||
# collector Java is currently using is known
|
||||
# to cause frame stuttering and/or other issues.
|
||||
logGarbageCollectorWarning = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed in chat if the garbage
|
||||
# collector Java is currently using is known
|
||||
# to cause frame stuttering and/or other issues.
|
||||
showGarbageCollectorWarning = true
|
||||
|
||||
[common.worldGenerator]
|
||||
#
|
||||
# How should distant generator progress be displayed?
|
||||
#
|
||||
# OVERLAY: may be the same as CHAT for some Minecraft versions
|
||||
# CHAT
|
||||
# LOG
|
||||
# DISABLED
|
||||
showGenerationProgress = "DISABLED"
|
||||
#
|
||||
# If the SURFACE_THEN_CHUNKS generator plan is
|
||||
# in use, this percentage can be defined to reduce how far the
|
||||
# second high-quality generation pass will go
|
||||
# as a percentage of your total render distance:
|
||||
# -1.0 = Infinity
|
||||
# 0.0 = 0%
|
||||
# 0.5 = 50%
|
||||
# 1.0 = 100%
|
||||
# 2.0 = 200%
|
||||
#
|
||||
# Does nothing on a dedicated server.
|
||||
surfaceRegenMaxDistancePercent = "-1.0"
|
||||
#
|
||||
# The max radius in chunks around the central point where world generation is allowed.
|
||||
# If this value is set to 0, generation bounds are disabled and the render distance will be used.
|
||||
#
|
||||
# This should only be set if you have a pre-generated world that has a very limited size.
|
||||
# Setting this on a normal MC world will prevent the world generator from filling
|
||||
# out your render distance.
|
||||
generationMaxChunkRadius = 0
|
||||
#
|
||||
# If the Minecraft server thread is taking too long
|
||||
# Distant Horizons will pause world gen to reduce server stuttering/lag.
|
||||
limitIfServerUnhealthy = true
|
||||
#
|
||||
# How often should the distant generator progress be displayed?
|
||||
generationProgressDisplayIntervalInSeconds = 2
|
||||
#
|
||||
# When logging generation progress also include the rate at which chunks
|
||||
# are being generated.
|
||||
# This can be useful for troubleshooting performance.
|
||||
generationProgressIncludeChunksPerSecond = true
|
||||
#
|
||||
# How detailed should LODs be generated outside the vanilla render distance?
|
||||
#
|
||||
# PRE_EXISTING_ONLY
|
||||
# Only create LOD data for already generated chunks.
|
||||
#
|
||||
# FEATURES
|
||||
# Generate everything except structures.
|
||||
# Note: This may cause world generator bugs or instability when paired with certain world generator mods.
|
||||
#
|
||||
# INTERNAL_SERVER
|
||||
# Ask the local server to generate/load each chunk.
|
||||
# This is the most compatible and will generate structures correctly,
|
||||
# but may cause server/simulation lag.
|
||||
# Note: unlike other modes this option DOES save generated chunks to
|
||||
# Minecraft's region files.
|
||||
chunkGeneratorMode = "FEATURES"
|
||||
#
|
||||
# Defines how LODs will be generated
|
||||
# outside the vanilla render distance.
|
||||
# In multiplayer, this may be overridden by the server.
|
||||
#
|
||||
# SURFACE_THEN_CHUNKS
|
||||
# The rough surface will be generated first
|
||||
# then chunks will be generated to fill in
|
||||
# missing features (ie trees and villages).
|
||||
#
|
||||
# Recommended for modded or vanilla worlds.
|
||||
#
|
||||
# SURFACE_ONLY
|
||||
# Only the rough surface will be generated.
|
||||
# Chunk features like trees and villages
|
||||
# won't be generated.
|
||||
#
|
||||
# Recommended for computers that don't have
|
||||
# enough CPU power or RAM space to handle
|
||||
# the chunk generator.
|
||||
#
|
||||
# CHUNKS_ONLY
|
||||
# Slower than either surface options,
|
||||
# but provides more accurate terrain.
|
||||
#
|
||||
# Recommended for custom worlds
|
||||
# where the surface generator would
|
||||
# generate incorrect terrain.
|
||||
#
|
||||
# DISABLED
|
||||
# No distant terrain will be generated.
|
||||
generatorPlan = "SURFACE_THEN_CHUNKS"
|
||||
#
|
||||
# If true and the player/camera is moving quickly
|
||||
# Distant Horizons will pause world gen to improve loading speed
|
||||
# and reduce CPU load.
|
||||
pauseIfMovingQuickly = true
|
||||
#
|
||||
# For how many seconds should instructions for disabling the distant generator progress be displayed?
|
||||
# Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.
|
||||
generationProgressDisableMessageDisplayTimeInSeconds = 20
|
||||
#
|
||||
# The center Z chunk position that the world gen max radius is centered around.
|
||||
generationCenterChunkZ = 0
|
||||
#
|
||||
# The center X chunk position that the world gen max radius is centered around.
|
||||
generationCenterChunkX = 0
|
||||
|
||||
[client]
|
||||
#
|
||||
# Should Distant Horizon's config button appear in Minecraft's options screen next to the fov slider?
|
||||
showDhOptionsButtonInMinecraftUi = true
|
||||
|
||||
[client.advanced.autoUpdater]
|
||||
#
|
||||
# If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build.
|
||||
# If [AUTO] is selected DH will update to new stable releases if the current jar is a stable jar
|
||||
# and will update to new nightly builds if the current jar is a nightly jar (IE the version number ends in '-dev').
|
||||
updateBranch = "AUTO"
|
||||
#
|
||||
# Automatically check for updates on game launch?
|
||||
enableAutoUpdater = true
|
||||
#
|
||||
# Should Distant Horizons silently, automatically download and install new versions?
|
||||
# This setting is force disabled on dedicated servers for stability reasons.
|
||||
enableSilentUpdates = false
|
||||
|
||||
[client.advanced.debugging]
|
||||
#
|
||||
# If enabled this will disable (most) vanilla Minecraft rendering.
|
||||
#
|
||||
# NOTE: Do not report any issues when this mode is on!
|
||||
# This setting is only for fun and debugging.
|
||||
# Mod compatibility is not guaranteed.
|
||||
lodOnlyMode = false
|
||||
#
|
||||
# What renderer is active?
|
||||
#
|
||||
# DEFAULT: Default lod renderer
|
||||
# DEBUG_TRIANGLE: Debug testing renderer
|
||||
# DISABLED: Disable rendering
|
||||
rendererMode = "DEFAULT"
|
||||
#
|
||||
# If true overlapping quads will be rendered as bright red for easy identification.
|
||||
# If false the quads will be rendered normally.
|
||||
showOverlappingQuadErrors = false
|
||||
#
|
||||
# If true OpenGL Buffer garbage collection will be logged
|
||||
# this also includes the number of live buffers.
|
||||
logBufferGarbageCollection = false
|
||||
#
|
||||
# Should specialized colors/rendering modes be used?
|
||||
#
|
||||
# OFF: LODs will be drawn with their normal colors.
|
||||
# SHOW_DETAIL: LODs' color will be based on their detail level.
|
||||
# SHOW_BLOCK_MATERIAL: LODs' color will be based on their material.
|
||||
# SHOW_OVERLAPPING_QUADS: LODs will be drawn with total white, but overlapping quads will be drawn with red.
|
||||
debugRenderingColors = "OFF"
|
||||
#
|
||||
# Stops vertex colors from being passed.
|
||||
# Useful for debugging shaders
|
||||
enableWhiteWorld = false
|
||||
#
|
||||
# If enabled the LODs will render as wireframe.
|
||||
renderWireframe = false
|
||||
#
|
||||
# If true several keys can be used to toggle debug states.
|
||||
# F6 - enable/disable LOD rendering
|
||||
# F7 - enable/disable LOD only rendering
|
||||
# F8 - cycle through the different debug rendering modes
|
||||
enableDebugKeybindings = false
|
||||
|
||||
[client.advanced.debugging.debugWireframe]
|
||||
#
|
||||
# Render LOD section status?
|
||||
showRenderSectionStatus = false
|
||||
#
|
||||
# Render queued network sync on load tasks?
|
||||
showNetworkSyncOnLoadQueue = false
|
||||
#
|
||||
# Render full data update/lock status?
|
||||
showFullDataUpdateStatus = false
|
||||
#
|
||||
# Render queued world gen tasks?
|
||||
showWorldGenQueue = false
|
||||
#
|
||||
# Render Quad Tree Rendering status?
|
||||
showQuadTreeRenderStatus = false
|
||||
#
|
||||
# If enabled, various wireframes for debugging internal functions will be drawn.
|
||||
#
|
||||
# NOTE: There WILL be performance hit!
|
||||
# Additionally, only stuff that's loaded after you enable this
|
||||
# will render their debug wireframes.
|
||||
enableRendering = false
|
||||
|
||||
[client.advanced.debugging.openGl]
|
||||
#
|
||||
# Defines how OpenGL errors are handled.
|
||||
# Requires rebooting Minecraft to change.
|
||||
# Will catch OpenGL errors thrown by other mods.
|
||||
overrideVanillaGLLogger = true
|
||||
#
|
||||
# Defines how OpenGL errors are handled.
|
||||
# May incorrectly catch OpenGL errors thrown by other mods.
|
||||
#
|
||||
# IGNORE: Do nothing.
|
||||
# LOG: write an error to the log.
|
||||
# LOG_THROW: write to the log and throw an exception.
|
||||
# Warning: this should only be enabled when debugging the LOD renderer
|
||||
# as it may break Minecraft's renderer when an exception is thrown.
|
||||
glErrorHandlingMode = "IGNORE"
|
||||
#
|
||||
# If true each Open GL error will only be logged once.
|
||||
# Enabling this may cause some error logs to be missed.
|
||||
# Does nothing if overrideVanillaGLLogger is set to false.
|
||||
#
|
||||
# Generally this can be kept as 'true' to prevent log spam.
|
||||
# However, Please set this to 'false' if a developer needs your log to debug a GL issue.
|
||||
onlyLogGlErrorsOnce = true
|
||||
|
||||
[client.advanced.debugging.exampleConfigScreen]
|
||||
shortTest = "69"
|
||||
mapTest = "{}"
|
||||
byteTest = "8"
|
||||
longTest = "42069"
|
||||
listTest = ["option 1", "option 2", "option 3"]
|
||||
boolTest = false
|
||||
doubleTest = "420.69"
|
||||
floatTest = "0.42069"
|
||||
linkableTest = 420
|
||||
intTest = 69420
|
||||
stringTest = "Test input box"
|
||||
|
||||
[client.advanced.debugging.f3Screen]
|
||||
#
|
||||
# Shows info about the render thread tasks.
|
||||
showRenderThreadTasks = false
|
||||
#
|
||||
# Shows how many chunks are queued for processing and the max count that can be queued.
|
||||
showQueuedChunkUpdateCount = true
|
||||
#
|
||||
# Shows the memory use and array counts for each DH object pool.
|
||||
showSeparatedObjectPools = false
|
||||
#
|
||||
# Shows the player's LOD position.
|
||||
showPlayerPos = true
|
||||
#
|
||||
# Shows the combined memory use and array counts for all DH pooled objects.
|
||||
showCombinedObjectPools = false
|
||||
#
|
||||
# Defines what internal detail level the player position will be shown as.
|
||||
# Internal detail level means: 6 = 1x1 block, 7 = 2x2 blocks, etc.
|
||||
playerPosSectionDetailLevel = 6
|
||||
#
|
||||
# Only show levels that DH is actively rendering.
|
||||
onlyShowRenderingLevels = true
|
||||
#
|
||||
# Shows info about each thread pool.
|
||||
showThreadPools = true
|
||||
#
|
||||
# Shows what levels are loaded and world gen/rendering info about those levels.
|
||||
showLevelStatus = true
|
||||
|
||||
[client.advanced.debugging.positionFinderDebugging]
|
||||
positionFinderEnable = false
|
||||
positionFinderMinBlockY = -64
|
||||
positionFinderZPos = 0
|
||||
positionFinderXPos = 0
|
||||
positionFinderMaxBlockY = 125
|
||||
positionFinderDetailLevel = 6
|
||||
positionFinderMarginPercent = "0.0"
|
||||
|
||||
[client.advanced.graphics]
|
||||
#
|
||||
# Enabling Anti-Aliasing smooths edges and reduces flickering when moving.
|
||||
enableAntiAliasing = true
|
||||
#
|
||||
# Enable Screen Space Ambient Occlusion
|
||||
enableSsao = true
|
||||
#
|
||||
# If true some vanilla graphics settings will be automatically changed
|
||||
# during DH setup to provide a better experience.
|
||||
#
|
||||
# IE disabling vanilla clouds (which render on top of DH LODs),
|
||||
# and chunk fading (DH already fades MC chunks)
|
||||
overrideVanillaGraphicsSettings = true
|
||||
|
||||
[client.advanced.graphics.culling]
|
||||
#
|
||||
# Determines how far from the camera Distant Horizons will start rendering.
|
||||
# Measured as a percentage of the vanilla render distance.
|
||||
#
|
||||
# -1 = auto, overdraw will change based on the vanilla render distance.
|
||||
#
|
||||
# Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,
|
||||
# but may cause holes in the world.
|
||||
# Holes are most likely to appear when flying through unloaded terrain.
|
||||
#
|
||||
# Increasing the vanilla render distance increases the effectiveness of this setting.
|
||||
overdrawPrevention = "-1.0"
|
||||
#
|
||||
# Identical to the other frustum culling option
|
||||
# only used when a shader mod is present using the DH API
|
||||
# and the shadow pass is being rendered.
|
||||
#
|
||||
# Disable this if shadows render incorrectly.
|
||||
disableShadowPassFrustumCulling = false
|
||||
#
|
||||
# A comma separated list of block resource locations that will be replaced by water
|
||||
# if they're visible on the water's surface.
|
||||
waterSubSurfaceBlockReplacementCsv = "minecraft:kelp,minecraft:tall_seagrass,minecraft:seagrass"
|
||||
#
|
||||
# A comma separated list of block resource locations that won't be rendered by DH.
|
||||
# Air is always included in this list.
|
||||
#
|
||||
# Note:
|
||||
# If you see gaps, or holes you may have to change
|
||||
# worldCompression to [MERGE_SAME_BLOCKS] and re-generate the LODs.
|
||||
ignoredRenderBlockCsv = "minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom"
|
||||
#
|
||||
# A comma separated list of block resource locations that will be removed
|
||||
# when on top of water.
|
||||
waterSurfaceBlockReplacementCsv = "minecraft:lily_pad"
|
||||
#
|
||||
# Defines what blocks should be rendered as LODs.
|
||||
#
|
||||
# NONE: Include all blocks in the LODs
|
||||
# NON_COLLIDING: Only render solid blocks in the LODs (tall grass, torches, etc. will be ignored)
|
||||
blocksToIgnore = "NON_COLLIDING"
|
||||
#
|
||||
# If true LODs outside the player's camera
|
||||
# aren't drawn, increasing GPU performance.
|
||||
#
|
||||
# If false all LODs are drawn, even those behind
|
||||
# the player's camera, decreasing GPU performance.
|
||||
#
|
||||
# Disable this if you see LODs disappearing at the corners of your vision.
|
||||
disableFrustumCulling = false
|
||||
#
|
||||
# If false all beacons near the camera won't be drawn to prevent vanilla overdraw.
|
||||
# If true all beacons will be rendered.
|
||||
#
|
||||
# Generally this should be left as true. It's main purpose is for debugging
|
||||
# beacon updating/rendering.
|
||||
disableBeaconDistanceCulling = true
|
||||
#
|
||||
# Should the blocks underneath avoided blocks gain the color of the avoided block?
|
||||
#
|
||||
# True: a red flower will tint the grass below it red.
|
||||
# False: skipped blocks will not change color of surface below them.
|
||||
tintWithAvoidedBlocks = true
|
||||
#
|
||||
# If enabled caves won't be rendered.
|
||||
#
|
||||
# Note: for some world types this can cause
|
||||
# overhangs or walls for floating objects.
|
||||
# Tweaking the caveCullingHeight, can resolve some
|
||||
# of those issues.
|
||||
enableCaveCulling = true
|
||||
#
|
||||
# At what Y value should cave culling start?
|
||||
# Lower this value if you get walls for areas with 0 light.
|
||||
caveCullingHeight = 60
|
||||
#
|
||||
# A comma separated list of block resource locations that shouldn't be rendered
|
||||
# if they are in a 0 sky light underground area.
|
||||
# Air is always included in this list.
|
||||
#
|
||||
# Defaults to an empty list since most cave blocks will be automatically ignored due to being:
|
||||
# transparent, non-solid, or liquids, but new blocks can be added here if needed.
|
||||
ignoredRenderCaveBlockCsv = ""
|
||||
#
|
||||
# If set to true the overdraw prevention radius will get closer
|
||||
# to the camera when flying/moving quickly.
|
||||
#
|
||||
# This helps reduce issues where Minecraft can't load or
|
||||
# generate chunks fast enough to keep up with DH.
|
||||
reduceOverdrawWithFastMovement = true
|
||||
|
||||
[client.advanced.graphics.texture]
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH will render their sides using the bottom texture.
|
||||
# Partial matches/incomplete resource locations will also match.
|
||||
#
|
||||
# Example: "minecraft:grass_block,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksDontUseSideTextureCsv = "grass_block,mycelium,nylium,dirt_path,grass"
|
||||
#
|
||||
# The highest detail level number that can render with block textures.
|
||||
# At higher detail levels each LOD covers multiple blocks,
|
||||
# which can essentially make textures smaller than a pixel and therefore invisible.
|
||||
#
|
||||
# Larger numbers texture more distant LODs
|
||||
# but increase memory usage.
|
||||
maxTexturedLodDetailLevel = 2
|
||||
#
|
||||
# If true nearby and zoomed-in LODs will render with their block's
|
||||
# actual texture instead of a single flat color.
|
||||
#
|
||||
# Only applies to high detail LODs where the texture is actually visible,
|
||||
# distant LODs always use flat colors.
|
||||
# Some shader packs nay not have an affect.
|
||||
enableTexturedLods = true
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH won't render textures on.
|
||||
# Partial matches/incomplete resource locations will also match.
|
||||
#
|
||||
# Example: "minecraft:grass_block,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksDontRenderTextureCsv = "minecraft:bamboo"
|
||||
#
|
||||
# A comma separated list of tag names
|
||||
# that DH will render their sides using the bottom texture.
|
||||
#
|
||||
# Example: "grass_blocks,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blockTagsDontUseSideTextureCsv = "grass_blocks,nylium"
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH will use rasterization to determine
|
||||
# the face textures.
|
||||
#
|
||||
# Can be used to fix issues with mods/blocks where
|
||||
# a side uses just part of a modeled block's texture.
|
||||
#
|
||||
# Example: "minecraft:beacon"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksAlwaysRasterizeTextureCsv = "minecraft:beacon"
|
||||
|
||||
[client.advanced.graphics.noiseTexture]
|
||||
#
|
||||
# Should a noise texture be applied to LODs?
|
||||
#
|
||||
# This is done to simulate textures and make the LODs appear more detailed.
|
||||
enableNoiseTexture = true
|
||||
#
|
||||
# Defines how far should the noise texture render before it fades away. (in blocks)
|
||||
# Set to 0 to disable noise from fading away
|
||||
noiseDropoff = 1024
|
||||
#
|
||||
# How many steps of noise should be applied to LODs?
|
||||
noiseSteps = 4
|
||||
#
|
||||
# How intense should the noise should be?
|
||||
noiseIntensity = "0.05"
|
||||
|
||||
[client.advanced.graphics.experimental]
|
||||
#
|
||||
# A comma separated list of dimension resource locations where DH won't render.
|
||||
#
|
||||
# Example: "minecraft:the_nether,minecraft:the_end"
|
||||
# For Minecraft 1.12.2 and older:
|
||||
# Open the F3 screen and copy the dimension_name:ID shown in yellow. Example: "overworld:0"
|
||||
#
|
||||
# Note:
|
||||
# Some DH settings will be disabled and/or changed to improve
|
||||
# visuals when DH rendering is disabled.
|
||||
ignoredDimensionCsv = ""
|
||||
#
|
||||
# Requires a restart to change.
|
||||
#
|
||||
# Options:
|
||||
# AUTO - changes based on the most likely API for that MC version
|
||||
# OPEN_GL - The Default for MC 1.21.11 and older (supports Iris shaders)
|
||||
# BLAZE_3D - The Default for MC 26.1.2 and newer (supports Vulkan)
|
||||
renderingEngine = "AUTO"
|
||||
#
|
||||
# This is the earth size ratio when applying the curvature shader effect.
|
||||
# Note: Enabling this feature may cause rendering bugs.
|
||||
#
|
||||
# 0 = flat/disabled
|
||||
# 1 = 1 to 1 (6,371,000 blocks)
|
||||
# 100 = 1 to 100 (63,710 blocks)
|
||||
# 10000 = 1 to 10000 (637.1 blocks)
|
||||
#
|
||||
# Note: Due to current limitations, the min value is [50]
|
||||
# and the max value is 5000. Any values outside this range
|
||||
# will be set to 0 (disabled).
|
||||
earthCurveRatio = 0
|
||||
|
||||
[client.advanced.graphics.genericRendering]
|
||||
#
|
||||
# A comma separated separated list of dimension resource locations where DH clouds will render.
|
||||
#
|
||||
# Example: "minecraft:overworld,minecraft:the_end"
|
||||
#
|
||||
# Changes require a world re-load.
|
||||
dimensionEnabledCloudRenderingCsv = "minecraft:overworld"
|
||||
#
|
||||
# If true LOD clouds will be rendered.
|
||||
enableCloudRendering = true
|
||||
#
|
||||
# If true LOD beacon beams will be rendered wider at extreme distances,
|
||||
# making them easier to see.
|
||||
# If false all LOD beacon beams will only ever be 1 block wide.
|
||||
expandDistantBeacons = true
|
||||
#
|
||||
# Sets the maximum height beacons will render up to.
|
||||
#
|
||||
# Requires a world re-load to take affect.
|
||||
beaconRenderHeight = 6000
|
||||
#
|
||||
# If true LOD beacon beams will be rendered.
|
||||
enableBeaconRendering = true
|
||||
#
|
||||
# If true non terrain objects will be rendered by DH.
|
||||
# i.e. beacon beams and clouds.
|
||||
enableGenericRendering = true
|
||||
#
|
||||
# False = DH will render a single layer of clouds, like vanilla Minecraft.
|
||||
# True = DH will render 3 layers of clouds at different heights.
|
||||
enableMultiLayerClouds = true
|
||||
|
||||
[client.advanced.graphics.quality]
|
||||
#
|
||||
# What is the maximum detail LODs can render at?
|
||||
# Higher settings will increase memory and GPU usage.
|
||||
#
|
||||
# CHUNK: render 1 LOD for each Chunk.
|
||||
# HALF_CHUNK: render 4 LODs for each Chunk.
|
||||
# FOUR_BLOCKS: render 16 LODs for each Chunk.
|
||||
# TWO_BLOCKS: render 64 LODs for each Chunk.
|
||||
# BLOCK: render 256 LODs for each Chunk (width of one block).
|
||||
#
|
||||
# Fastest: CHUNK
|
||||
# Fanciest: BLOCK
|
||||
maxHorizontalResolution = "BLOCK"
|
||||
#
|
||||
# If true LODs will fade away as you get closer to them.
|
||||
# If false LODs will cut off abruptly at a set distance from the camera.
|
||||
# This setting is affected by the vanilla overdraw prevention config.
|
||||
ditherDhFade = true
|
||||
#
|
||||
# If true DH will try to use the camera position when
|
||||
# determining LOD quality drop-off.
|
||||
# If false DH will use the player's position.
|
||||
#
|
||||
# Enabling helps free-cam mods render correctly.
|
||||
# Disabling helps multi-camera mods render correctly (ie Immersive Portals or camera mods).
|
||||
useCameraPositionForQualityDropOff = true
|
||||
#
|
||||
# How bright LOD colors are.
|
||||
#
|
||||
# 0 = black
|
||||
# 1 = normal
|
||||
# 2 = near white
|
||||
brightnessMultiplier = "1.0"
|
||||
#
|
||||
# How many detail levels zooming in can increase LOD quality by.
|
||||
# Higher numbers allow stronger zooms to render crisper terrain,
|
||||
# but will increase memory and GPU usage while zoomed in.
|
||||
#
|
||||
# A vanilla spyglass needs 4 detail levels to reach its full quality.
|
||||
maxZoomQualityIncrease = 4
|
||||
#
|
||||
# How should LODs be shaded?
|
||||
#
|
||||
# AUTO: Uses the same side shading as vanilla Minecraft blocks.
|
||||
# ENABLED: Simulates Minecraft's block shading for LODs.
|
||||
# Can be used to force LOD shading when using some shaders.
|
||||
# DISABLED: All LOD sides will be rendered with the same brightness.
|
||||
lodShading = "AUTO"
|
||||
#
|
||||
# How saturated LOD colors are.
|
||||
#
|
||||
# 0 = black and white
|
||||
# 1 = normal
|
||||
# 2 = very saturated
|
||||
saturationMultiplier = "1.0"
|
||||
#
|
||||
# This indicates how well LODs will represent
|
||||
# overhangs, caves, floating islands, etc.
|
||||
# Higher options will make the world more accurate, butwill increase memory and GPU usage.
|
||||
#
|
||||
# Lowest Quality: HEIGHT_MAP
|
||||
# Highest Quality: PIXEL_ART
|
||||
verticalQuality = "MEDIUM"
|
||||
#
|
||||
# The radius of the mod's render distance. (measured in chunks)
|
||||
#
|
||||
# Note: this is a best effort number.
|
||||
# The actual render distance may be above or below this number
|
||||
# depending on your other graphic settings.
|
||||
lodChunkRenderDistanceRadius = 128
|
||||
#
|
||||
# How should the sides and bottom of grass block LODs render?
|
||||
#
|
||||
# AS_GRASS: all sides of dirt LOD's render using the top (green) color.
|
||||
# FADE_TO_DIRT: sides fade from grass to dirt.
|
||||
# AS_DIRT: sides render entirely as dirt.
|
||||
grassSideRendering = "FADE_TO_DIRT"
|
||||
#
|
||||
# This indicates how far apart drops in LOD quality are.
|
||||
#
|
||||
# Higher settings will increase the distance between drops
|
||||
# but will increase memory and GPU usage.
|
||||
horizontalQuality = "MEDIUM"
|
||||
#
|
||||
# How should LOD transparency be handled.
|
||||
#
|
||||
# COMPLETE: LODs will render transparent.
|
||||
# DISABLED: LODs will be opaque.
|
||||
transparency = "COMPLETE"
|
||||
#
|
||||
# This is the same as vanilla Biome Blending settings for Lod area.
|
||||
# Note: anything above '0' will slow down LOD loading time.
|
||||
#
|
||||
# '0' equals to Vanilla Biome Blending of '1x1' or 'OFF',
|
||||
# '1' equals to Vanilla Biome Blending of '3x3',
|
||||
# '2' equals to Vanilla Biome Blending of '5x5'...
|
||||
lodBiomeBlending = 3
|
||||
#
|
||||
# How should vanilla Minecraft fade into Distant Horizons LODs?
|
||||
#
|
||||
# NONE: Fastest, there will be a pronounced border between DH and MC rendering.
|
||||
# SINGLE_PASS: Fades after MC's transparent pass, opaque blocks underwater won't be faded.
|
||||
# DOUBLE_PASS: Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition.
|
||||
vanillaFadeMode = "DOUBLE_PASS"
|
||||
#
|
||||
# If true LOD quality will increase when the camera is zoomed in,
|
||||
# IE when using a spyglass or zoom mod.
|
||||
#
|
||||
# Only LODs visible through the camera view are affected.
|
||||
#
|
||||
# When zoomed in LODs will load to the same detail level
|
||||
# they would have if you were close to them.
|
||||
increaseQualityWhenZoomedIn = true
|
||||
|
||||
[client.advanced.graphics.fog]
|
||||
#
|
||||
# Should Minecraft's fog render?
|
||||
# Note: Other mods may conflict with this setting.
|
||||
enableVanillaFog = false
|
||||
#
|
||||
# What is the maximum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
farFogMax = "1.0"
|
||||
#
|
||||
# Determines if fog is drawn on DH LODs.
|
||||
enableDhFog = true
|
||||
#
|
||||
# At what distance should the far fog start?
|
||||
#
|
||||
# 0.0: Fog starts at the player's position.
|
||||
# 1.0: Fog starts at the closest edge of the vanilla render distance.
|
||||
# 1.414: Fog starts at the corner of the vanilla render distance.
|
||||
farFogStart = "0.4"
|
||||
#
|
||||
# What is the minimum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
farFogMin = "0.0"
|
||||
#
|
||||
# What color should fog use?
|
||||
#
|
||||
# USE_WORLD_FOG_COLOR: Use the world's fog color.
|
||||
# USE_SKY_COLOR: Use the sky's color.
|
||||
colorMode = "USE_WORLD_FOG_COLOR"
|
||||
#
|
||||
# How should the fog thickness should be calculated?
|
||||
#
|
||||
# LINEAR: Linear based on distance (will ignore 'density')
|
||||
# EXPONENTIAL: 1/(e^(distance*density))
|
||||
# EXPONENTIAL_SQUARED: 1/(e^((distance*density)^2)
|
||||
farFogFalloff = "EXPONENTIAL_SQUARED"
|
||||
#
|
||||
# Used in conjunction with the Fog Falloff.
|
||||
farFogDensity = "2.5"
|
||||
#
|
||||
# Where should the far fog end?
|
||||
#
|
||||
# 0.0: Fog ends at player's position.
|
||||
# 1.0: Fog ends at the closest edge of the vanilla render distance.
|
||||
# 1.414: Fog ends at the corner of the vanilla render distance.
|
||||
farFogEnd = "1.0"
|
||||
|
||||
[client.advanced.graphics.fog.heightFog]
|
||||
#
|
||||
# Where should the height fog start?
|
||||
#
|
||||
# ABOVE_CAMERA: Height fog starts at the camera and goes towards the sky
|
||||
# BELOW_CAMERA: Height fog starts at the camera and goes towards the void
|
||||
# ABOVE_AND_BELOW_CAMERA: Height fog starts from the camera to goes towards both the sky and void
|
||||
# ABOVE_SET_HEIGHT: Height fog starts from a set height and goes towards the sky
|
||||
# BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards the void
|
||||
# ABOVE_AND_BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards both the sky and void
|
||||
heightFogDirection = "BELOW_SET_HEIGHT"
|
||||
#
|
||||
# What is the minimum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
heightFogMin = "0.0"
|
||||
#
|
||||
# If the height fog is calculated around a set height, what is that height position?
|
||||
heightFogBaseHeight = "80.0"
|
||||
#
|
||||
# What is the maximum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
heightFogMax = "1.0"
|
||||
#
|
||||
# How should the height fog thickness should be calculated?
|
||||
#
|
||||
# LINEAR: Linear based on height (will ignore 'density')
|
||||
# EXPONENTIAL: 1/(e^(height*density))
|
||||
# EXPONENTIAL_SQUARED: 1/(e^((height*density)^2)
|
||||
heightFogFalloff = "EXPONENTIAL_SQUARED"
|
||||
#
|
||||
# What is the height fog's density?
|
||||
heightFogDensity = "20.0"
|
||||
#
|
||||
# How should height effect the fog thickness?
|
||||
# Note: height fog is combined with the other fog settings.
|
||||
#
|
||||
# SPHERICAL: Fog is calculated based on camera distance.
|
||||
# CYLINDRICAL: Ignore height, fog is calculated based on horizontal distance.
|
||||
#
|
||||
# MAX: max(heightFog, farFog)
|
||||
# ADDITION: heightFog + farFog
|
||||
# MULTIPLY: heightFog * farFog
|
||||
# INVERSE_MULTIPLY: 1 - (1-heightFog) * (1-farFog)
|
||||
# LIMITED_ADDITION: farFog + max(farFog, heightFog)
|
||||
# MULTIPLY_ADDITION: farFog + farFog * heightFog
|
||||
# INVERSE_MULTIPLY_ADDITION: farFog + 1 - (1-heightFog) * (1-farFog)
|
||||
# AVERAGE: farFog*0.5 + heightFog*0.5
|
||||
heightFogMixMode = "SPHERICAL"
|
||||
#
|
||||
# Should the start of the height fog be offset?
|
||||
#
|
||||
# 0.0: Fog start with no offset.
|
||||
# 1.0: Fog start with offset of the entire world's height. (Includes depth)
|
||||
heightFogStart = "0.0"
|
||||
#
|
||||
# Should the end of the height fog be offset?
|
||||
#
|
||||
# 0.0: Fog end with no offset.
|
||||
# 1.0: Fog end with offset of the entire world's height. (Include depth)
|
||||
heightFogEnd = "0.6"
|
||||
|
||||
[client.advanced.multiplayer]
|
||||
#
|
||||
# How should multiplayer save folders should be named?
|
||||
#
|
||||
# NAME_ONLY: Example: "Minecraft Server"
|
||||
# IP_ONLY: Example: "192.168.1.40"
|
||||
# NAME_IP: Example: "Minecraft Server IP 192.168.1.40"
|
||||
# NAME_IP_PORT: Example: "Minecraft Server IP 192.168.1.40:25565"NAME_IP_PORT_MC_VERSION: Example: "Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5"
|
||||
serverFolderNameMode = "NAME_ONLY"
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"allowedCEM": "ALL",
|
||||
"logModelCreationData": false,
|
||||
"debugOnRightClick": false,
|
||||
"renderModeChoice": "NORMAL",
|
||||
"vanillaModelHologramRenderMode_2": "OFF",
|
||||
"modelExportMode": "NONE",
|
||||
"automaticModelExporting": false,
|
||||
"attemptPhysicsModPatch_2": "CUSTOM",
|
||||
"modelUpdateFrequency": "Slow",
|
||||
"entityRenderModeOverrides": {},
|
||||
"entityPhysicsModPatchOverrides": {},
|
||||
"entityVanillaHologramOverrides": {},
|
||||
"modelsNamesDisabled": [],
|
||||
"allowEBEModConfigModify": true,
|
||||
"animationLODDistance": 12,
|
||||
"retainDetailOnLowFps": true,
|
||||
"retainDetailOnLargerMobs": true,
|
||||
"animationFrameSkipDuringIrisShadowPass": true,
|
||||
"preventFirstPersonHandAnimating": false,
|
||||
"onlyClientPlayerModel": false,
|
||||
"doubleChestAnimFix": true,
|
||||
"enforceOptifineVariationRequiresDefaultModel": false,
|
||||
"enforceOptifineVariationRequiresDefaultModel_v2": false,
|
||||
"resetPlayerModelEachRender": true,
|
||||
"resetPlayerModelEachRender_v2": true,
|
||||
"onlyDebugRenderOnHover": false,
|
||||
"enforceOptifineSubFoldersVariantOnly": false,
|
||||
"enforceOptiFineAnimSyntaxLimits": true,
|
||||
"allowOptifineFallbackProperties": true,
|
||||
"showReloadErrorToast": true,
|
||||
"exportRotations": false,
|
||||
"asmMaths": true,
|
||||
"logASM": false,
|
||||
"parrotShoulderPositionAnimatesByDefault": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#Select the backend to use. Set to "DEFAULT" to let Flywheel decide.
|
||||
backend = "DEFAULT"
|
||||
#Enable or disable instance update limiting with distance.
|
||||
limitUpdates = true
|
||||
#The number of worker threads to use. Set to -1 to let Flywheel decide. Set to 0 to disable parallelism. Requires a game restart to take effect.
|
||||
# Default: -1
|
||||
# Range: -1 ~ 4
|
||||
workerThreads = -1
|
||||
|
||||
#Config options for Flywheel's built-in backends.
|
||||
[flw_backends]
|
||||
#How smooth Flywheel's shader-based lighting should be. May have a large performance impact.
|
||||
#Allowed Values: FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED
|
||||
lightSmoothness = "SMOOTH"
|
||||
@@ -0,0 +1,16 @@
|
||||
mode = "fast"
|
||||
chunk_rebuild_scheduler = "culling"
|
||||
|
||||
[adaptive_ticking]
|
||||
slow = 5
|
||||
slower = 8
|
||||
background_sleep = true
|
||||
|
||||
[light_sources]
|
||||
entities = false
|
||||
self = true
|
||||
water_sensitive_check = true
|
||||
beam = true
|
||||
guardian_laser = true
|
||||
sonic_boom = true
|
||||
glowing_effect = true
|
||||
@@ -0,0 +1,20 @@
|
||||
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
|
||||
create\:cogs=4
|
||||
create\:mixing=4
|
||||
create\:mixing_compounded_1=4
|
||||
create\:mechanical_press_activation=4
|
||||
create\:mechanical_press_activation_compounded_1=4
|
||||
create\:mechanical_press_activation_belt=4
|
||||
create\:mechanical_press_activation_belt_compounded_1=4
|
||||
create\:crushing_1=4
|
||||
create\:crushing_2=4
|
||||
create\:crushing_3=4
|
||||
create\:saw_activate_stone=4
|
||||
create\:saw_activate_wood=4
|
||||
create\:funnel_flap=4
|
||||
create\:funnel_flap_compounded_1=4
|
||||
create\:depot_slide=4
|
||||
create\:depot_plop=4
|
||||
create\:steam=4
|
||||
create\:sanding_short=4
|
||||
create\:sanding_long=4
|
||||
@@ -0,0 +1,99 @@
|
||||
# Enables/Disables all sound effects
|
||||
enabled=true
|
||||
# Affects how quiet a sound gets based on distance
|
||||
# Lower values mean distant sounds are louder
|
||||
# This setting requires you to be in singleplayer or having the mod installed on the server
|
||||
# 1.0 is the physically correct value
|
||||
attenuation_factor=1.0
|
||||
# The ray distance at which reverb starts
|
||||
# 0.0 disables reverb attenuation
|
||||
reverb_attenuation_distance=0.0
|
||||
# The volume of simulated reverberations
|
||||
reverb_gain=1.0
|
||||
# The brightness of reverberation
|
||||
# Higher values result in more high frequencies in reverberation
|
||||
# Lower values give a more muffled sound to the reverb
|
||||
reverb_brightness=1.0
|
||||
# The distance of reverb relative to the sound distance
|
||||
reverb_distance=1.5
|
||||
# The amount of sound that will be absorbed when traveling through blocks
|
||||
block_absorption=1.0
|
||||
# Higher values mean smaller objects won't be considered as occluding
|
||||
occlusion_variation=0.35
|
||||
# The default amount of sound reflectance energy for all blocks
|
||||
# Lower values result in more conservative reverb simulation with shorter reverb tails
|
||||
# Higher values result in more generous reverb simulation with higher reverb tails
|
||||
default_block_reflectivity=0.5
|
||||
# The default amount of occlusion for all blocks
|
||||
# Lower values will result in sounds being less muffled through walls
|
||||
# Higher values mean sounds will be not audible though thicker walls
|
||||
default_block_occlusion_factor=1.0
|
||||
# Minecraft won't allow sounds to play past a certain distance
|
||||
# This parameter is a multiplier for how far away a sound source is allowed to be in order for it to actually play
|
||||
# This setting only takes affect in singleplayer worlds and when installed on the server
|
||||
sound_distance_allowance=4.0
|
||||
# A value controlling the amount that air absorbs high frequencies with distance
|
||||
# A value of 1.0 is physically correct for air with normal humidity and temperature
|
||||
# Higher values mean air will absorb more high frequencies with distance
|
||||
# 0 disables this effect
|
||||
air_absorption=1.0
|
||||
# How much sound is filtered when the player is underwater
|
||||
# 0.0 means no filter
|
||||
# 1.0 means fully filtered
|
||||
underwater_filter=0.9
|
||||
# Whether sounds like cave, nether or underwater ambient sounds should have sound physics
|
||||
evaluate_ambient_sounds=false
|
||||
# The number of rays to trace to determine reverberation for each sound source
|
||||
# More rays provides more consistent tracing results but takes more time to calculate
|
||||
# Decrease this value if you experience lag spikes when sounds play
|
||||
environment_evaluation_ray_count=16
|
||||
# The number of rays bounces to trace to determine reverberation for each sound source
|
||||
# More bounces provides more echo and sound ducting but takes more time to calculate
|
||||
# Decrease this value if you experience lag spikes when sounds play
|
||||
environment_evaluation_ray_bounces=2
|
||||
# If sound hits a non-full-square side, block occlusion is multiplied by this
|
||||
non_full_block_occlusion_factor=0.25
|
||||
# The maximum amount of rays to determine occlusion
|
||||
# Directly correlates to the amount of blocks between walls that are considered
|
||||
max_occlusion_rays=16
|
||||
# The amount at which occlusion is capped
|
||||
max_occlusion=64.0
|
||||
# If enabled, the occlusion calculation only uses one path between the sound source and the listener instead of 9
|
||||
strict_occlusion=true
|
||||
# Whether to try calculating where the sound should come from based on reflections
|
||||
sound_direction_evaluation=true
|
||||
# Skip redirecting non-occluded sounds (the ones you can see directly)
|
||||
redirect_non_occluded_sounds=true
|
||||
# If music discs or other longer sounds should be frequently reevaluated
|
||||
update_moving_sounds=false
|
||||
# The interval in ticks that moving sounds are reevaluated
|
||||
# Lower values mean more frequent reevaluation but also more lag
|
||||
# This option only takes effect if update_moving_sounds is enabled
|
||||
sound_update_interval=5
|
||||
# The maximum distance a sound can be processed
|
||||
max_sound_processing_distance=512.0
|
||||
# Disable level clone and cache. This will fall back to original main thread access.
|
||||
# WARNING! Enabling this will cause instability and issues with other mods.
|
||||
unsafe_level_access=false
|
||||
# The radius of chunks to clone for level access
|
||||
level_clone_range=4
|
||||
# The maximum number of ticks to retain the cloned level in the cache
|
||||
level_clone_max_retain_ticks=20
|
||||
# The maximum distance a player can move from the cloned origin before invalidation
|
||||
level_clone_max_retain_block_distance=16
|
||||
# Enables debug logging
|
||||
debug_logging=false
|
||||
# Provides more information about occlusion in the logs
|
||||
occlusion_logging=false
|
||||
# Provides more information about the environment calculation in the logs
|
||||
environment_logging=false
|
||||
# Provides more information about how long computations take
|
||||
performance_logging=false
|
||||
# If enabled, the path of the sound will be rendered in game
|
||||
render_sound_bounces=false
|
||||
# If enabled, occlusion will be visualized in game
|
||||
render_occlusion=false
|
||||
# Enables/Disables sound effects for Simple Voice Chat audio
|
||||
simple_voice_chat_integration=true
|
||||
# Enables/Disables hearing your own echo with Simple Voice Chat
|
||||
simple_voice_chat_hear_self=false
|
||||
@@ -0,0 +1,12 @@
|
||||
#The render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 128
|
||||
# Range: 1 ~ 1000000
|
||||
physicsRenderDistance = 32
|
||||
#The minimum render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 16
|
||||
# Range: 1 ~ 1000000
|
||||
minPhysicsRenderDistance = 16
|
||||
#The maximum render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 4096
|
||||
# Range: 1 ~ 1000000
|
||||
maxPhysicsRenderDistance = 4096
|
||||
@@ -0,0 +1,11 @@
|
||||
version:3955
|
||||
renderDistance:8
|
||||
simulationDistance:8
|
||||
biomeBlendRadius:2
|
||||
mipmapLevels:4
|
||||
entityDistanceScaling:1.0
|
||||
particles:0
|
||||
entityShadows:true
|
||||
graphicsMode:1
|
||||
maxFps:120
|
||||
enableVsync:true
|
||||
@@ -0,0 +1,980 @@
|
||||
_version = 4
|
||||
|
||||
[server]
|
||||
#
|
||||
# Defines the distance the player will receive updates around.
|
||||
realTimeUpdateDistanceRadiusInChunks = 256
|
||||
#
|
||||
# Prefix of the level keys sent to the clients.
|
||||
# If the mod is running behind a proxy, each backend should use a unique value.
|
||||
# If this value is empty, level key will be based on the server's seed hash.
|
||||
levelKeyPrefix = ""
|
||||
#
|
||||
# Defines the distance allowed to be synchronized around the player.
|
||||
# Should be the same or larger than maxGenerationRequestDistance in most cases.
|
||||
maxSyncOnLoadRequestDistance = 4096
|
||||
#
|
||||
# If true, clients will receive updated LODs when joining or loading new LODs.
|
||||
synchronizeOnLoad = true
|
||||
#
|
||||
# Custom server key used which can be used to always reuse the same LOD data folder,
|
||||
# for cases when the server doesn't have a static IP for some reason.
|
||||
# If this value is empty, the client itself decides which folder name to use.
|
||||
# Requires rejoining the server to apply after changing.
|
||||
serverKey = ""
|
||||
#
|
||||
# Defines the distance allowed to generate around the player.
|
||||
maxGenerationRequestDistance = 4096
|
||||
#
|
||||
# Makes the server send level keys for each world.
|
||||
# Disable this if you use alternative ways to send level keys.
|
||||
sendLevelKeys = true
|
||||
#
|
||||
# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU'RE DOING.
|
||||
# Autogenerated ID used to prevent multiple independent servers from accidentally
|
||||
# writing over each other's LODs when the same serverKey is set on both.
|
||||
#
|
||||
# How many LOD generation requests per second should a client send?
|
||||
# Also limits the number of client requests allowed to stay in the server's queue.
|
||||
generationRequestRateLimit = 20
|
||||
#
|
||||
# How many LOD sync requests per second should a client send?
|
||||
# Also limits the amount of player's requests allowed to stay in the server's queue.
|
||||
syncOnLoadRateLimit = 50
|
||||
#
|
||||
# If true, clients will receive real-time LOD updates for chunks outside the client's render distance.
|
||||
enableRealTimeUpdates = true
|
||||
#
|
||||
# Maximum global speed for uploading LODs to the clients, in KB/s.
|
||||
# Value of 0 disables the limit.
|
||||
globalBandwidthLimit = 0
|
||||
#
|
||||
# Maximum per-player speed for uploading LODs to the clients, in KB/s.
|
||||
# Value of 0 disables the limit.
|
||||
playerBandwidthLimit = 500
|
||||
#
|
||||
# Enables adaptive transfer speed based on client performance.
|
||||
# If true, DH will automatically adjust transfer rate to minimize connection lag.
|
||||
# If false, transfer speed will remain fixed.
|
||||
enableAdaptiveTransferSpeed = false
|
||||
|
||||
[common.lodBuilding]
|
||||
#
|
||||
# How should block data be compressed when creating LOD data?
|
||||
# This setting will only affect new or updated LOD data,
|
||||
# any data already generated when this setting is changed will be
|
||||
# unaffected until it is modified or re-loaded.
|
||||
#
|
||||
# MERGE_SAME_BLOCKS
|
||||
# Every block/biome change is recorded in the database.
|
||||
# This is what DH 2.0 and 2.0.1 all used by default and will store a lot of data.
|
||||
# Expected Compression Ratio: 1.0
|
||||
#
|
||||
# VISUALLY_EQUAL
|
||||
# Only visible block/biome changes are recorded in the database.
|
||||
# Hidden blocks (IE ores) are ignored.
|
||||
# Expected Compression Ratio: 0.7
|
||||
worldCompression = "VISUALLY_EQUAL"
|
||||
#
|
||||
# Requires a level restart to apply.
|
||||
#
|
||||
# Setting this to a less strict mode increases DB read/write
|
||||
# speed at the cost of potential data loss/corruption if
|
||||
# the computer looses power
|
||||
# (unexpected program shutdown should be safe for all options).
|
||||
#
|
||||
# FULL
|
||||
# ACID - data will never be lost or corrupted
|
||||
# Slow
|
||||
# At max throughput: ~2k saves per sec, ~70% disk usage
|
||||
#
|
||||
# NORMAL
|
||||
# May not be durable - some data may be lost on power loss, but DB should be safe from corruption
|
||||
# Fast
|
||||
# At max throughput: ~14k saves per sec, ~40% disk usage
|
||||
#
|
||||
# OFF
|
||||
# Not consistent - database may be corrupted during power loss
|
||||
# Very Fast
|
||||
# At max throughput: ~16k saves per sec, ~6% disk usage
|
||||
databaseSyncMode = "NORMAL"
|
||||
dataCompression = "Z_STD_BLOCK"
|
||||
#
|
||||
# Enabling this will drastically speed up chunk processing time
|
||||
# if you are CPU limited.
|
||||
# Disabling it will drastically speed up chunk processing time
|
||||
# if you are disk speed/IO limited.
|
||||
#
|
||||
# Normally DH will attempt to skip creating LODs for chunks it's already seen
|
||||
# and that haven't changed.
|
||||
#
|
||||
# Disabling this option can also fix a rare bug where
|
||||
# modified chunks are skipped due to being too similar to
|
||||
# their saved hash.
|
||||
disableUnchangedChunkCheck = false
|
||||
|
||||
[common.multiThreading]
|
||||
#
|
||||
# How many threads should be used by Distant Horizons?
|
||||
numberOfThreads = 2
|
||||
#
|
||||
# A value between 1.0 and 0.0 that represents the percentage
|
||||
# of time each thread can run before going idle.
|
||||
#
|
||||
# This can be used to reduce CPU usage if the thread count
|
||||
# is already set to 1 for the given option, or more finely
|
||||
# tune CPU performance.
|
||||
threadRunTimeRatio = "1.0"
|
||||
#
|
||||
# What Java thread priority should DH's primary thread pools run with?
|
||||
#
|
||||
# You probably don't need to change this unless you are also
|
||||
# running C2ME and are seeing thread starvation in either C2ME or DH.
|
||||
threadPriority = 5
|
||||
|
||||
[common.logging]
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer setup, cleanup, and any issues it may encounter.
|
||||
# This can be useful for debugging.
|
||||
logRendererEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the world generation process.
|
||||
# This can be useful for debugging.
|
||||
logWorldGenEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer OpenGL process.
|
||||
# This can be useful for debugging.
|
||||
logRendererGLEventToChat = "ERROR"
|
||||
#
|
||||
#
|
||||
globalChatMaxLevel = "ERROR"
|
||||
#
|
||||
# If enabled, the mod will log information about the world generation process.
|
||||
# This can be useful for debugging.
|
||||
logWorldGenChunkLoadEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about the renderer OpenGL process.
|
||||
# This can be useful for debugging.
|
||||
logRendererGLEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, the mod will log information about network operations.
|
||||
# This can be useful for debugging.
|
||||
logNetworkEventToFile = "INFO"
|
||||
#
|
||||
# If enabled, config changes sent by the server will be logged.
|
||||
logConnectionConfigChangesToFile = "WARN"
|
||||
#
|
||||
#
|
||||
globalFileMaxLevel = "INFO"
|
||||
|
||||
[common.logging.warning]
|
||||
#
|
||||
# If enabled, a message will be displayed in chat if explicit garbage collection
|
||||
# is disabled.
|
||||
# This is known to cause out-of-memory issues
|
||||
# and is better solved with a concurrent garbage collector.
|
||||
showExplicitGcDisabledWarning = false
|
||||
#
|
||||
# If enabled, a chat message will be displayed when DH has too many chunks
|
||||
# queued for updating.
|
||||
showSlowWorldGenSettingWarnings = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when DH has too many chunks
|
||||
# queued for updating.
|
||||
showUpdateQueueOverloadedChatWarning = false
|
||||
#
|
||||
# If enabled, a message will be logged if explicit garbage collection
|
||||
# is disabled.
|
||||
# This is known to cause out-of-memory issues
|
||||
# and is better solved with a concurrent garbage collector.
|
||||
logExplicitGcDisabledWarning = false
|
||||
#
|
||||
# If enabled, a chat message will be displayed if Java doesn't have enough
|
||||
# memory allocated to run DH well.
|
||||
showLowMemoryWarningOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when a replay is started
|
||||
# giving some basic information about how DH will function.
|
||||
showReplayWarningOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed when a potentially problematic
|
||||
# mod is installed alongside DH.
|
||||
showModCompatibilityWarningsOnStartup = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed if vanilla MC's
|
||||
# render distance is higher than the recommended amount.
|
||||
showHighVanillaRenderDistanceWarning = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed if DH detects
|
||||
# that any pooled objects have been garbage collected.
|
||||
showPoolInsufficientMemoryWarning = true
|
||||
#
|
||||
# If enabled, a message will be logged if the garbage
|
||||
# collector Java is currently using is known
|
||||
# to cause frame stuttering and/or other issues.
|
||||
logGarbageCollectorWarning = true
|
||||
#
|
||||
# If enabled, a chat message will be displayed in chat if the garbage
|
||||
# collector Java is currently using is known
|
||||
# to cause frame stuttering and/or other issues.
|
||||
showGarbageCollectorWarning = true
|
||||
|
||||
[common.worldGenerator]
|
||||
#
|
||||
# How should distant generator progress be displayed?
|
||||
#
|
||||
# OVERLAY: may be the same as CHAT for some Minecraft versions
|
||||
# CHAT
|
||||
# LOG
|
||||
# DISABLED
|
||||
showGenerationProgress = "DISABLED"
|
||||
#
|
||||
# If the SURFACE_THEN_CHUNKS generator plan is
|
||||
# in use, this percentage can be defined to reduce how far the
|
||||
# second high-quality generation pass will go
|
||||
# as a percentage of your total render distance:
|
||||
# -1.0 = Infinity
|
||||
# 0.0 = 0%
|
||||
# 0.5 = 50%
|
||||
# 1.0 = 100%
|
||||
# 2.0 = 200%
|
||||
#
|
||||
# Does nothing on a dedicated server.
|
||||
surfaceRegenMaxDistancePercent = "-1.0"
|
||||
#
|
||||
# The max radius in chunks around the central point where world generation is allowed.
|
||||
# If this value is set to 0, generation bounds are disabled and the render distance will be used.
|
||||
#
|
||||
# This should only be set if you have a pre-generated world that has a very limited size.
|
||||
# Setting this on a normal MC world will prevent the world generator from filling
|
||||
# out your render distance.
|
||||
generationMaxChunkRadius = 0
|
||||
#
|
||||
# If the Minecraft server thread is taking too long
|
||||
# Distant Horizons will pause world gen to reduce server stuttering/lag.
|
||||
limitIfServerUnhealthy = true
|
||||
#
|
||||
# How often should the distant generator progress be displayed?
|
||||
generationProgressDisplayIntervalInSeconds = 2
|
||||
#
|
||||
# When logging generation progress also include the rate at which chunks
|
||||
# are being generated.
|
||||
# This can be useful for troubleshooting performance.
|
||||
generationProgressIncludeChunksPerSecond = true
|
||||
#
|
||||
# How detailed should LODs be generated outside the vanilla render distance?
|
||||
#
|
||||
# PRE_EXISTING_ONLY
|
||||
# Only create LOD data for already generated chunks.
|
||||
#
|
||||
# FEATURES
|
||||
# Generate everything except structures.
|
||||
# Note: This may cause world generator bugs or instability when paired with certain world generator mods.
|
||||
#
|
||||
# INTERNAL_SERVER
|
||||
# Ask the local server to generate/load each chunk.
|
||||
# This is the most compatible and will generate structures correctly,
|
||||
# but may cause server/simulation lag.
|
||||
# Note: unlike other modes this option DOES save generated chunks to
|
||||
# Minecraft's region files.
|
||||
chunkGeneratorMode = "FEATURES"
|
||||
#
|
||||
# Defines how LODs will be generated
|
||||
# outside the vanilla render distance.
|
||||
# In multiplayer, this may be overridden by the server.
|
||||
#
|
||||
# SURFACE_THEN_CHUNKS
|
||||
# The rough surface will be generated first
|
||||
# then chunks will be generated to fill in
|
||||
# missing features (ie trees and villages).
|
||||
#
|
||||
# Recommended for modded or vanilla worlds.
|
||||
#
|
||||
# SURFACE_ONLY
|
||||
# Only the rough surface will be generated.
|
||||
# Chunk features like trees and villages
|
||||
# won't be generated.
|
||||
#
|
||||
# Recommended for computers that don't have
|
||||
# enough CPU power or RAM space to handle
|
||||
# the chunk generator.
|
||||
#
|
||||
# CHUNKS_ONLY
|
||||
# Slower than either surface options,
|
||||
# but provides more accurate terrain.
|
||||
#
|
||||
# Recommended for custom worlds
|
||||
# where the surface generator would
|
||||
# generate incorrect terrain.
|
||||
#
|
||||
# DISABLED
|
||||
# No distant terrain will be generated.
|
||||
generatorPlan = "SURFACE_THEN_CHUNKS"
|
||||
#
|
||||
# If true and the player/camera is moving quickly
|
||||
# Distant Horizons will pause world gen to improve loading speed
|
||||
# and reduce CPU load.
|
||||
pauseIfMovingQuickly = true
|
||||
#
|
||||
# For how many seconds should instructions for disabling the distant generator progress be displayed?
|
||||
# Setting this to 0 hides the instructional message so the world gen progress is shown immediately when it starts.
|
||||
generationProgressDisableMessageDisplayTimeInSeconds = 20
|
||||
#
|
||||
# The center Z chunk position that the world gen max radius is centered around.
|
||||
generationCenterChunkZ = 0
|
||||
#
|
||||
# The center X chunk position that the world gen max radius is centered around.
|
||||
generationCenterChunkX = 0
|
||||
|
||||
[client]
|
||||
#
|
||||
# Should Distant Horizon's config button appear in Minecraft's options screen next to the fov slider?
|
||||
showDhOptionsButtonInMinecraftUi = true
|
||||
|
||||
[client.advanced.autoUpdater]
|
||||
#
|
||||
# If DH should use the nightly (provided by Gitlab), or stable (provided by Modrinth) build.
|
||||
# If [AUTO] is selected DH will update to new stable releases if the current jar is a stable jar
|
||||
# and will update to new nightly builds if the current jar is a nightly jar (IE the version number ends in '-dev').
|
||||
updateBranch = "AUTO"
|
||||
#
|
||||
# Automatically check for updates on game launch?
|
||||
enableAutoUpdater = true
|
||||
#
|
||||
# Should Distant Horizons silently, automatically download and install new versions?
|
||||
# This setting is force disabled on dedicated servers for stability reasons.
|
||||
enableSilentUpdates = false
|
||||
|
||||
[client.advanced.debugging]
|
||||
#
|
||||
# If enabled this will disable (most) vanilla Minecraft rendering.
|
||||
#
|
||||
# NOTE: Do not report any issues when this mode is on!
|
||||
# This setting is only for fun and debugging.
|
||||
# Mod compatibility is not guaranteed.
|
||||
lodOnlyMode = false
|
||||
#
|
||||
# What renderer is active?
|
||||
#
|
||||
# DEFAULT: Default lod renderer
|
||||
# DEBUG_TRIANGLE: Debug testing renderer
|
||||
# DISABLED: Disable rendering
|
||||
rendererMode = "DEFAULT"
|
||||
#
|
||||
# If true overlapping quads will be rendered as bright red for easy identification.
|
||||
# If false the quads will be rendered normally.
|
||||
showOverlappingQuadErrors = false
|
||||
#
|
||||
# If true OpenGL Buffer garbage collection will be logged
|
||||
# this also includes the number of live buffers.
|
||||
logBufferGarbageCollection = false
|
||||
#
|
||||
# Should specialized colors/rendering modes be used?
|
||||
#
|
||||
# OFF: LODs will be drawn with their normal colors.
|
||||
# SHOW_DETAIL: LODs' color will be based on their detail level.
|
||||
# SHOW_BLOCK_MATERIAL: LODs' color will be based on their material.
|
||||
# SHOW_OVERLAPPING_QUADS: LODs will be drawn with total white, but overlapping quads will be drawn with red.
|
||||
debugRenderingColors = "OFF"
|
||||
#
|
||||
# Stops vertex colors from being passed.
|
||||
# Useful for debugging shaders
|
||||
enableWhiteWorld = false
|
||||
#
|
||||
# If enabled the LODs will render as wireframe.
|
||||
renderWireframe = false
|
||||
#
|
||||
# If true several keys can be used to toggle debug states.
|
||||
# F6 - enable/disable LOD rendering
|
||||
# F7 - enable/disable LOD only rendering
|
||||
# F8 - cycle through the different debug rendering modes
|
||||
enableDebugKeybindings = false
|
||||
|
||||
[client.advanced.debugging.debugWireframe]
|
||||
#
|
||||
# Render LOD section status?
|
||||
showRenderSectionStatus = false
|
||||
#
|
||||
# Render queued network sync on load tasks?
|
||||
showNetworkSyncOnLoadQueue = false
|
||||
#
|
||||
# Render full data update/lock status?
|
||||
showFullDataUpdateStatus = false
|
||||
#
|
||||
# Render queued world gen tasks?
|
||||
showWorldGenQueue = false
|
||||
#
|
||||
# Render Quad Tree Rendering status?
|
||||
showQuadTreeRenderStatus = false
|
||||
#
|
||||
# If enabled, various wireframes for debugging internal functions will be drawn.
|
||||
#
|
||||
# NOTE: There WILL be performance hit!
|
||||
# Additionally, only stuff that's loaded after you enable this
|
||||
# will render their debug wireframes.
|
||||
enableRendering = false
|
||||
|
||||
[client.advanced.debugging.openGl]
|
||||
#
|
||||
# Defines how OpenGL errors are handled.
|
||||
# Requires rebooting Minecraft to change.
|
||||
# Will catch OpenGL errors thrown by other mods.
|
||||
overrideVanillaGLLogger = true
|
||||
#
|
||||
# Defines how OpenGL errors are handled.
|
||||
# May incorrectly catch OpenGL errors thrown by other mods.
|
||||
#
|
||||
# IGNORE: Do nothing.
|
||||
# LOG: write an error to the log.
|
||||
# LOG_THROW: write to the log and throw an exception.
|
||||
# Warning: this should only be enabled when debugging the LOD renderer
|
||||
# as it may break Minecraft's renderer when an exception is thrown.
|
||||
glErrorHandlingMode = "IGNORE"
|
||||
#
|
||||
# If true each Open GL error will only be logged once.
|
||||
# Enabling this may cause some error logs to be missed.
|
||||
# Does nothing if overrideVanillaGLLogger is set to false.
|
||||
#
|
||||
# Generally this can be kept as 'true' to prevent log spam.
|
||||
# However, Please set this to 'false' if a developer needs your log to debug a GL issue.
|
||||
onlyLogGlErrorsOnce = true
|
||||
|
||||
[client.advanced.debugging.exampleConfigScreen]
|
||||
shortTest = "69"
|
||||
mapTest = "{}"
|
||||
byteTest = "8"
|
||||
longTest = "42069"
|
||||
listTest = ["option 1", "option 2", "option 3"]
|
||||
boolTest = false
|
||||
doubleTest = "420.69"
|
||||
floatTest = "0.42069"
|
||||
linkableTest = 420
|
||||
intTest = 69420
|
||||
stringTest = "Test input box"
|
||||
|
||||
[client.advanced.debugging.f3Screen]
|
||||
#
|
||||
# Shows info about the render thread tasks.
|
||||
showRenderThreadTasks = false
|
||||
#
|
||||
# Shows how many chunks are queued for processing and the max count that can be queued.
|
||||
showQueuedChunkUpdateCount = true
|
||||
#
|
||||
# Shows the memory use and array counts for each DH object pool.
|
||||
showSeparatedObjectPools = false
|
||||
#
|
||||
# Shows the player's LOD position.
|
||||
showPlayerPos = true
|
||||
#
|
||||
# Shows the combined memory use and array counts for all DH pooled objects.
|
||||
showCombinedObjectPools = false
|
||||
#
|
||||
# Defines what internal detail level the player position will be shown as.
|
||||
# Internal detail level means: 6 = 1x1 block, 7 = 2x2 blocks, etc.
|
||||
playerPosSectionDetailLevel = 6
|
||||
#
|
||||
# Only show levels that DH is actively rendering.
|
||||
onlyShowRenderingLevels = true
|
||||
#
|
||||
# Shows info about each thread pool.
|
||||
showThreadPools = true
|
||||
#
|
||||
# Shows what levels are loaded and world gen/rendering info about those levels.
|
||||
showLevelStatus = true
|
||||
|
||||
[client.advanced.debugging.positionFinderDebugging]
|
||||
positionFinderEnable = false
|
||||
positionFinderMinBlockY = -64
|
||||
positionFinderZPos = 0
|
||||
positionFinderXPos = 0
|
||||
positionFinderMaxBlockY = 125
|
||||
positionFinderDetailLevel = 6
|
||||
positionFinderMarginPercent = "0.0"
|
||||
|
||||
[client.advanced.graphics]
|
||||
#
|
||||
# Enabling Anti-Aliasing smooths edges and reduces flickering when moving.
|
||||
enableAntiAliasing = true
|
||||
#
|
||||
# Enable Screen Space Ambient Occlusion
|
||||
enableSsao = false
|
||||
#
|
||||
# If true some vanilla graphics settings will be automatically changed
|
||||
# during DH setup to provide a better experience.
|
||||
#
|
||||
# IE disabling vanilla clouds (which render on top of DH LODs),
|
||||
# and chunk fading (DH already fades MC chunks)
|
||||
overrideVanillaGraphicsSettings = true
|
||||
|
||||
[client.advanced.graphics.culling]
|
||||
#
|
||||
# Determines how far from the camera Distant Horizons will start rendering.
|
||||
# Measured as a percentage of the vanilla render distance.
|
||||
#
|
||||
# -1 = auto, overdraw will change based on the vanilla render distance.
|
||||
#
|
||||
# Higher values will prevent LODs from rendering behind vanilla blocks at a higher distance,
|
||||
# but may cause holes in the world.
|
||||
# Holes are most likely to appear when flying through unloaded terrain.
|
||||
#
|
||||
# Increasing the vanilla render distance increases the effectiveness of this setting.
|
||||
overdrawPrevention = "-1.0"
|
||||
#
|
||||
# Identical to the other frustum culling option
|
||||
# only used when a shader mod is present using the DH API
|
||||
# and the shadow pass is being rendered.
|
||||
#
|
||||
# Disable this if shadows render incorrectly.
|
||||
disableShadowPassFrustumCulling = false
|
||||
#
|
||||
# A comma separated list of block resource locations that will be replaced by water
|
||||
# if they're visible on the water's surface.
|
||||
waterSubSurfaceBlockReplacementCsv = "minecraft:kelp,minecraft:tall_seagrass,minecraft:seagrass"
|
||||
#
|
||||
# A comma separated list of block resource locations that won't be rendered by DH.
|
||||
# Air is always included in this list.
|
||||
#
|
||||
# Note:
|
||||
# If you see gaps, or holes you may have to change
|
||||
# worldCompression to [MERGE_SAME_BLOCKS] and re-generate the LODs.
|
||||
ignoredRenderBlockCsv = "minecraft:barrier,minecraft:structure_void,minecraft:light,minecraft:tripwire,minecraft:brown_mushroom"
|
||||
#
|
||||
# A comma separated list of block resource locations that will be removed
|
||||
# when on top of water.
|
||||
waterSurfaceBlockReplacementCsv = "minecraft:lily_pad"
|
||||
#
|
||||
# Defines what blocks should be rendered as LODs.
|
||||
#
|
||||
# NONE: Include all blocks in the LODs
|
||||
# NON_COLLIDING: Only render solid blocks in the LODs (tall grass, torches, etc. will be ignored)
|
||||
blocksToIgnore = "NON_COLLIDING"
|
||||
#
|
||||
# If true LODs outside the player's camera
|
||||
# aren't drawn, increasing GPU performance.
|
||||
#
|
||||
# If false all LODs are drawn, even those behind
|
||||
# the player's camera, decreasing GPU performance.
|
||||
#
|
||||
# Disable this if you see LODs disappearing at the corners of your vision.
|
||||
disableFrustumCulling = false
|
||||
#
|
||||
# If false all beacons near the camera won't be drawn to prevent vanilla overdraw.
|
||||
# If true all beacons will be rendered.
|
||||
#
|
||||
# Generally this should be left as true. It's main purpose is for debugging
|
||||
# beacon updating/rendering.
|
||||
disableBeaconDistanceCulling = true
|
||||
#
|
||||
# Should the blocks underneath avoided blocks gain the color of the avoided block?
|
||||
#
|
||||
# True: a red flower will tint the grass below it red.
|
||||
# False: skipped blocks will not change color of surface below them.
|
||||
tintWithAvoidedBlocks = true
|
||||
#
|
||||
# If enabled caves won't be rendered.
|
||||
#
|
||||
# Note: for some world types this can cause
|
||||
# overhangs or walls for floating objects.
|
||||
# Tweaking the caveCullingHeight, can resolve some
|
||||
# of those issues.
|
||||
enableCaveCulling = true
|
||||
#
|
||||
# At what Y value should cave culling start?
|
||||
# Lower this value if you get walls for areas with 0 light.
|
||||
caveCullingHeight = 60
|
||||
#
|
||||
# A comma separated list of block resource locations that shouldn't be rendered
|
||||
# if they are in a 0 sky light underground area.
|
||||
# Air is always included in this list.
|
||||
#
|
||||
# Defaults to an empty list since most cave blocks will be automatically ignored due to being:
|
||||
# transparent, non-solid, or liquids, but new blocks can be added here if needed.
|
||||
ignoredRenderCaveBlockCsv = ""
|
||||
#
|
||||
# If set to true the overdraw prevention radius will get closer
|
||||
# to the camera when flying/moving quickly.
|
||||
#
|
||||
# This helps reduce issues where Minecraft can't load or
|
||||
# generate chunks fast enough to keep up with DH.
|
||||
reduceOverdrawWithFastMovement = true
|
||||
|
||||
[client.advanced.graphics.texture]
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH will render their sides using the bottom texture.
|
||||
# Partial matches/incomplete resource locations will also match.
|
||||
#
|
||||
# Example: "minecraft:grass_block,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksDontUseSideTextureCsv = "grass_block,mycelium,nylium,dirt_path,grass"
|
||||
#
|
||||
# The highest detail level number that can render with block textures.
|
||||
# At higher detail levels each LOD covers multiple blocks,
|
||||
# which can essentially make textures smaller than a pixel and therefore invisible.
|
||||
#
|
||||
# Larger numbers texture more distant LODs
|
||||
# but increase memory usage.
|
||||
maxTexturedLodDetailLevel = 2
|
||||
#
|
||||
# If true nearby and zoomed-in LODs will render with their block's
|
||||
# actual texture instead of a single flat color.
|
||||
#
|
||||
# Only applies to high detail LODs where the texture is actually visible,
|
||||
# distant LODs always use flat colors.
|
||||
# Some shader packs nay not have an affect.
|
||||
enableTexturedLods = true
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH won't render textures on.
|
||||
# Partial matches/incomplete resource locations will also match.
|
||||
#
|
||||
# Example: "minecraft:grass_block,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksDontRenderTextureCsv = "minecraft:bamboo"
|
||||
#
|
||||
# A comma separated list of tag names
|
||||
# that DH will render their sides using the bottom texture.
|
||||
#
|
||||
# Example: "grass_blocks,nylium"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blockTagsDontUseSideTextureCsv = "grass_blocks,nylium"
|
||||
#
|
||||
# A comma separated list of block resource locations
|
||||
# that DH will use rasterization to determine
|
||||
# the face textures.
|
||||
#
|
||||
# Can be used to fix issues with mods/blocks where
|
||||
# a side uses just part of a modeled block's texture.
|
||||
#
|
||||
# Example: "minecraft:beacon"
|
||||
#
|
||||
# Changes require a restart.
|
||||
blocksAlwaysRasterizeTextureCsv = "minecraft:beacon"
|
||||
|
||||
[client.advanced.graphics.noiseTexture]
|
||||
#
|
||||
# Should a noise texture be applied to LODs?
|
||||
#
|
||||
# This is done to simulate textures and make the LODs appear more detailed.
|
||||
enableNoiseTexture = true
|
||||
#
|
||||
# Defines how far should the noise texture render before it fades away. (in blocks)
|
||||
# Set to 0 to disable noise from fading away
|
||||
noiseDropoff = 1024
|
||||
#
|
||||
# How many steps of noise should be applied to LODs?
|
||||
noiseSteps = 4
|
||||
#
|
||||
# How intense should the noise should be?
|
||||
noiseIntensity = "0.05"
|
||||
|
||||
[client.advanced.graphics.experimental]
|
||||
#
|
||||
# A comma separated list of dimension resource locations where DH won't render.
|
||||
#
|
||||
# Example: "minecraft:the_nether,minecraft:the_end"
|
||||
# For Minecraft 1.12.2 and older:
|
||||
# Open the F3 screen and copy the dimension_name:ID shown in yellow. Example: "overworld:0"
|
||||
#
|
||||
# Note:
|
||||
# Some DH settings will be disabled and/or changed to improve
|
||||
# visuals when DH rendering is disabled.
|
||||
ignoredDimensionCsv = ""
|
||||
#
|
||||
# Requires a restart to change.
|
||||
#
|
||||
# Options:
|
||||
# AUTO - changes based on the most likely API for that MC version
|
||||
# OPEN_GL - The Default for MC 1.21.11 and older (supports Iris shaders)
|
||||
# BLAZE_3D - The Default for MC 26.1.2 and newer (supports Vulkan)
|
||||
renderingEngine = "AUTO"
|
||||
#
|
||||
# This is the earth size ratio when applying the curvature shader effect.
|
||||
# Note: Enabling this feature may cause rendering bugs.
|
||||
#
|
||||
# 0 = flat/disabled
|
||||
# 1 = 1 to 1 (6,371,000 blocks)
|
||||
# 100 = 1 to 100 (63,710 blocks)
|
||||
# 10000 = 1 to 10000 (637.1 blocks)
|
||||
#
|
||||
# Note: Due to current limitations, the min value is [50]
|
||||
# and the max value is 5000. Any values outside this range
|
||||
# will be set to 0 (disabled).
|
||||
earthCurveRatio = 0
|
||||
|
||||
[client.advanced.graphics.genericRendering]
|
||||
#
|
||||
# A comma separated separated list of dimension resource locations where DH clouds will render.
|
||||
#
|
||||
# Example: "minecraft:overworld,minecraft:the_end"
|
||||
#
|
||||
# Changes require a world re-load.
|
||||
dimensionEnabledCloudRenderingCsv = "minecraft:overworld"
|
||||
#
|
||||
# If true LOD clouds will be rendered.
|
||||
enableCloudRendering = true
|
||||
#
|
||||
# If true LOD beacon beams will be rendered wider at extreme distances,
|
||||
# making them easier to see.
|
||||
# If false all LOD beacon beams will only ever be 1 block wide.
|
||||
expandDistantBeacons = true
|
||||
#
|
||||
# Sets the maximum height beacons will render up to.
|
||||
#
|
||||
# Requires a world re-load to take affect.
|
||||
beaconRenderHeight = 6000
|
||||
#
|
||||
# If true LOD beacon beams will be rendered.
|
||||
enableBeaconRendering = true
|
||||
#
|
||||
# If true non terrain objects will be rendered by DH.
|
||||
# i.e. beacon beams and clouds.
|
||||
enableGenericRendering = true
|
||||
#
|
||||
# False = DH will render a single layer of clouds, like vanilla Minecraft.
|
||||
# True = DH will render 3 layers of clouds at different heights.
|
||||
enableMultiLayerClouds = true
|
||||
|
||||
[client.advanced.graphics.quality]
|
||||
#
|
||||
# What is the maximum detail LODs can render at?
|
||||
# Higher settings will increase memory and GPU usage.
|
||||
#
|
||||
# CHUNK: render 1 LOD for each Chunk.
|
||||
# HALF_CHUNK: render 4 LODs for each Chunk.
|
||||
# FOUR_BLOCKS: render 16 LODs for each Chunk.
|
||||
# TWO_BLOCKS: render 64 LODs for each Chunk.
|
||||
# BLOCK: render 256 LODs for each Chunk (width of one block).
|
||||
#
|
||||
# Fastest: CHUNK
|
||||
# Fanciest: BLOCK
|
||||
maxHorizontalResolution = "BLOCK"
|
||||
#
|
||||
# If true LODs will fade away as you get closer to them.
|
||||
# If false LODs will cut off abruptly at a set distance from the camera.
|
||||
# This setting is affected by the vanilla overdraw prevention config.
|
||||
ditherDhFade = true
|
||||
#
|
||||
# If true DH will try to use the camera position when
|
||||
# determining LOD quality drop-off.
|
||||
# If false DH will use the player's position.
|
||||
#
|
||||
# Enabling helps free-cam mods render correctly.
|
||||
# Disabling helps multi-camera mods render correctly (ie Immersive Portals or camera mods).
|
||||
useCameraPositionForQualityDropOff = true
|
||||
#
|
||||
# How bright LOD colors are.
|
||||
#
|
||||
# 0 = black
|
||||
# 1 = normal
|
||||
# 2 = near white
|
||||
brightnessMultiplier = "1.0"
|
||||
#
|
||||
# How many detail levels zooming in can increase LOD quality by.
|
||||
# Higher numbers allow stronger zooms to render crisper terrain,
|
||||
# but will increase memory and GPU usage while zoomed in.
|
||||
#
|
||||
# A vanilla spyglass needs 4 detail levels to reach its full quality.
|
||||
maxZoomQualityIncrease = 4
|
||||
#
|
||||
# How should LODs be shaded?
|
||||
#
|
||||
# AUTO: Uses the same side shading as vanilla Minecraft blocks.
|
||||
# ENABLED: Simulates Minecraft's block shading for LODs.
|
||||
# Can be used to force LOD shading when using some shaders.
|
||||
# DISABLED: All LOD sides will be rendered with the same brightness.
|
||||
lodShading = "AUTO"
|
||||
#
|
||||
# How saturated LOD colors are.
|
||||
#
|
||||
# 0 = black and white
|
||||
# 1 = normal
|
||||
# 2 = very saturated
|
||||
saturationMultiplier = "1.0"
|
||||
#
|
||||
# This indicates how well LODs will represent
|
||||
# overhangs, caves, floating islands, etc.
|
||||
# Higher options will make the world more accurate, butwill increase memory and GPU usage.
|
||||
#
|
||||
# Lowest Quality: HEIGHT_MAP
|
||||
# Highest Quality: PIXEL_ART
|
||||
verticalQuality = "MEDIUM"
|
||||
#
|
||||
# The radius of the mod's render distance. (measured in chunks)
|
||||
#
|
||||
# Note: this is a best effort number.
|
||||
# The actual render distance may be above or below this number
|
||||
# depending on your other graphic settings.
|
||||
lodChunkRenderDistanceRadius = 128
|
||||
#
|
||||
# How should the sides and bottom of grass block LODs render?
|
||||
#
|
||||
# AS_GRASS: all sides of dirt LOD's render using the top (green) color.
|
||||
# FADE_TO_DIRT: sides fade from grass to dirt.
|
||||
# AS_DIRT: sides render entirely as dirt.
|
||||
grassSideRendering = "FADE_TO_DIRT"
|
||||
#
|
||||
# This indicates how far apart drops in LOD quality are.
|
||||
#
|
||||
# Higher settings will increase the distance between drops
|
||||
# but will increase memory and GPU usage.
|
||||
horizontalQuality = "MEDIUM"
|
||||
#
|
||||
# How should LOD transparency be handled.
|
||||
#
|
||||
# COMPLETE: LODs will render transparent.
|
||||
# DISABLED: LODs will be opaque.
|
||||
transparency = "COMPLETE"
|
||||
#
|
||||
# This is the same as vanilla Biome Blending settings for Lod area.
|
||||
# Note: anything above '0' will slow down LOD loading time.
|
||||
#
|
||||
# '0' equals to Vanilla Biome Blending of '1x1' or 'OFF',
|
||||
# '1' equals to Vanilla Biome Blending of '3x3',
|
||||
# '2' equals to Vanilla Biome Blending of '5x5'...
|
||||
lodBiomeBlending = 3
|
||||
#
|
||||
# How should vanilla Minecraft fade into Distant Horizons LODs?
|
||||
#
|
||||
# NONE: Fastest, there will be a pronounced border between DH and MC rendering.
|
||||
# SINGLE_PASS: Fades after MC's transparent pass, opaque blocks underwater won't be faded.
|
||||
# DOUBLE_PASS: Slowest, fades after both MC's opaque and transparent passes, provides the smoothest transition.
|
||||
vanillaFadeMode = "DOUBLE_PASS"
|
||||
#
|
||||
# If true LOD quality will increase when the camera is zoomed in,
|
||||
# IE when using a spyglass or zoom mod.
|
||||
#
|
||||
# Only LODs visible through the camera view are affected.
|
||||
#
|
||||
# When zoomed in LODs will load to the same detail level
|
||||
# they would have if you were close to them.
|
||||
increaseQualityWhenZoomedIn = true
|
||||
|
||||
[client.advanced.graphics.fog]
|
||||
#
|
||||
# Should Minecraft's fog render?
|
||||
# Note: Other mods may conflict with this setting.
|
||||
enableVanillaFog = false
|
||||
#
|
||||
# What is the maximum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
farFogMax = "1.0"
|
||||
#
|
||||
# Determines if fog is drawn on DH LODs.
|
||||
enableDhFog = true
|
||||
#
|
||||
# At what distance should the far fog start?
|
||||
#
|
||||
# 0.0: Fog starts at the player's position.
|
||||
# 1.0: Fog starts at the closest edge of the vanilla render distance.
|
||||
# 1.414: Fog starts at the corner of the vanilla render distance.
|
||||
farFogStart = "0.4"
|
||||
#
|
||||
# What is the minimum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
farFogMin = "0.0"
|
||||
#
|
||||
# What color should fog use?
|
||||
#
|
||||
# USE_WORLD_FOG_COLOR: Use the world's fog color.
|
||||
# USE_SKY_COLOR: Use the sky's color.
|
||||
colorMode = "USE_WORLD_FOG_COLOR"
|
||||
#
|
||||
# How should the fog thickness should be calculated?
|
||||
#
|
||||
# LINEAR: Linear based on distance (will ignore 'density')
|
||||
# EXPONENTIAL: 1/(e^(distance*density))
|
||||
# EXPONENTIAL_SQUARED: 1/(e^((distance*density)^2)
|
||||
farFogFalloff = "EXPONENTIAL_SQUARED"
|
||||
#
|
||||
# Used in conjunction with the Fog Falloff.
|
||||
farFogDensity = "2.5"
|
||||
#
|
||||
# Where should the far fog end?
|
||||
#
|
||||
# 0.0: Fog ends at player's position.
|
||||
# 1.0: Fog ends at the closest edge of the vanilla render distance.
|
||||
# 1.414: Fog ends at the corner of the vanilla render distance.
|
||||
farFogEnd = "1.0"
|
||||
|
||||
[client.advanced.graphics.fog.heightFog]
|
||||
#
|
||||
# Where should the height fog start?
|
||||
#
|
||||
# ABOVE_CAMERA: Height fog starts at the camera and goes towards the sky
|
||||
# BELOW_CAMERA: Height fog starts at the camera and goes towards the void
|
||||
# ABOVE_AND_BELOW_CAMERA: Height fog starts from the camera to goes towards both the sky and void
|
||||
# ABOVE_SET_HEIGHT: Height fog starts from a set height and goes towards the sky
|
||||
# BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards the void
|
||||
# ABOVE_AND_BELOW_SET_HEIGHT: Height fog starts from a set height and goes towards both the sky and void
|
||||
heightFogDirection = "BELOW_SET_HEIGHT"
|
||||
#
|
||||
# What is the minimum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
heightFogMin = "0.0"
|
||||
#
|
||||
# If the height fog is calculated around a set height, what is that height position?
|
||||
heightFogBaseHeight = "80.0"
|
||||
#
|
||||
# What is the maximum fog thickness?
|
||||
#
|
||||
# 0.0: No fog.
|
||||
# 1.0: Fully opaque fog.
|
||||
heightFogMax = "1.0"
|
||||
#
|
||||
# How should the height fog thickness should be calculated?
|
||||
#
|
||||
# LINEAR: Linear based on height (will ignore 'density')
|
||||
# EXPONENTIAL: 1/(e^(height*density))
|
||||
# EXPONENTIAL_SQUARED: 1/(e^((height*density)^2)
|
||||
heightFogFalloff = "EXPONENTIAL_SQUARED"
|
||||
#
|
||||
# What is the height fog's density?
|
||||
heightFogDensity = "20.0"
|
||||
#
|
||||
# How should height effect the fog thickness?
|
||||
# Note: height fog is combined with the other fog settings.
|
||||
#
|
||||
# SPHERICAL: Fog is calculated based on camera distance.
|
||||
# CYLINDRICAL: Ignore height, fog is calculated based on horizontal distance.
|
||||
#
|
||||
# MAX: max(heightFog, farFog)
|
||||
# ADDITION: heightFog + farFog
|
||||
# MULTIPLY: heightFog * farFog
|
||||
# INVERSE_MULTIPLY: 1 - (1-heightFog) * (1-farFog)
|
||||
# LIMITED_ADDITION: farFog + max(farFog, heightFog)
|
||||
# MULTIPLY_ADDITION: farFog + farFog * heightFog
|
||||
# INVERSE_MULTIPLY_ADDITION: farFog + 1 - (1-heightFog) * (1-farFog)
|
||||
# AVERAGE: farFog*0.5 + heightFog*0.5
|
||||
heightFogMixMode = "SPHERICAL"
|
||||
#
|
||||
# Should the start of the height fog be offset?
|
||||
#
|
||||
# 0.0: Fog start with no offset.
|
||||
# 1.0: Fog start with offset of the entire world's height. (Includes depth)
|
||||
heightFogStart = "0.0"
|
||||
#
|
||||
# Should the end of the height fog be offset?
|
||||
#
|
||||
# 0.0: Fog end with no offset.
|
||||
# 1.0: Fog end with offset of the entire world's height. (Include depth)
|
||||
heightFogEnd = "0.6"
|
||||
|
||||
[client.advanced.multiplayer]
|
||||
#
|
||||
# How should multiplayer save folders should be named?
|
||||
#
|
||||
# NAME_ONLY: Example: "Minecraft Server"
|
||||
# IP_ONLY: Example: "192.168.1.40"
|
||||
# NAME_IP: Example: "Minecraft Server IP 192.168.1.40"
|
||||
# NAME_IP_PORT: Example: "Minecraft Server IP 192.168.1.40:25565"NAME_IP_PORT_MC_VERSION: Example: "Minecraft Server IP 192.168.1.40:25565 GameVersion 1.16.5"
|
||||
serverFolderNameMode = "NAME_ONLY"
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"allowedCEM": "ALL",
|
||||
"logModelCreationData": false,
|
||||
"debugOnRightClick": false,
|
||||
"renderModeChoice": "NORMAL",
|
||||
"vanillaModelHologramRenderMode_2": "OFF",
|
||||
"modelExportMode": "NONE",
|
||||
"automaticModelExporting": false,
|
||||
"attemptPhysicsModPatch_2": "CUSTOM",
|
||||
"modelUpdateFrequency": "Slow",
|
||||
"entityRenderModeOverrides": {},
|
||||
"entityPhysicsModPatchOverrides": {},
|
||||
"entityVanillaHologramOverrides": {},
|
||||
"modelsNamesDisabled": [],
|
||||
"allowEBEModConfigModify": true,
|
||||
"animationLODDistance": 12,
|
||||
"retainDetailOnLowFps": true,
|
||||
"retainDetailOnLargerMobs": true,
|
||||
"animationFrameSkipDuringIrisShadowPass": true,
|
||||
"preventFirstPersonHandAnimating": false,
|
||||
"onlyClientPlayerModel": false,
|
||||
"doubleChestAnimFix": true,
|
||||
"enforceOptifineVariationRequiresDefaultModel": false,
|
||||
"enforceOptifineVariationRequiresDefaultModel_v2": false,
|
||||
"resetPlayerModelEachRender": true,
|
||||
"resetPlayerModelEachRender_v2": true,
|
||||
"onlyDebugRenderOnHover": false,
|
||||
"enforceOptifineSubFoldersVariantOnly": false,
|
||||
"enforceOptiFineAnimSyntaxLimits": true,
|
||||
"allowOptifineFallbackProperties": true,
|
||||
"showReloadErrorToast": true,
|
||||
"exportRotations": false,
|
||||
"asmMaths": true,
|
||||
"logASM": false,
|
||||
"parrotShoulderPositionAnimatesByDefault": true
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#Select the backend to use. Set to "DEFAULT" to let Flywheel decide.
|
||||
backend = "DEFAULT"
|
||||
#Enable or disable instance update limiting with distance.
|
||||
limitUpdates = true
|
||||
#The number of worker threads to use. Set to -1 to let Flywheel decide. Set to 0 to disable parallelism. Requires a game restart to take effect.
|
||||
# Default: -1
|
||||
# Range: -1 ~ 4
|
||||
workerThreads = -1
|
||||
|
||||
#Config options for Flywheel's built-in backends.
|
||||
[flw_backends]
|
||||
#How smooth Flywheel's shader-based lighting should be. May have a large performance impact.
|
||||
#Allowed Values: FLAT, TRI_LINEAR, SMOOTH, SMOOTH_INNER_FACE_CORRECTED
|
||||
lightSmoothness = "TRI_LINEAR"
|
||||
@@ -0,0 +1,16 @@
|
||||
mode = "fast"
|
||||
chunk_rebuild_scheduler = "culling"
|
||||
|
||||
[adaptive_ticking]
|
||||
slow = 5
|
||||
slower = 8
|
||||
background_sleep = true
|
||||
|
||||
[light_sources]
|
||||
entities = true
|
||||
self = true
|
||||
water_sensitive_check = true
|
||||
beam = true
|
||||
guardian_laser = true
|
||||
sonic_boom = true
|
||||
glowing_effect = true
|
||||
@@ -0,0 +1,20 @@
|
||||
# Partial file: SPR merges these over its generated per-sound defaults (-1 = unlimited).
|
||||
create\:cogs=4
|
||||
create\:mixing=4
|
||||
create\:mixing_compounded_1=4
|
||||
create\:mechanical_press_activation=4
|
||||
create\:mechanical_press_activation_compounded_1=4
|
||||
create\:mechanical_press_activation_belt=4
|
||||
create\:mechanical_press_activation_belt_compounded_1=4
|
||||
create\:crushing_1=4
|
||||
create\:crushing_2=4
|
||||
create\:crushing_3=4
|
||||
create\:saw_activate_stone=4
|
||||
create\:saw_activate_wood=4
|
||||
create\:funnel_flap=4
|
||||
create\:funnel_flap_compounded_1=4
|
||||
create\:depot_slide=4
|
||||
create\:depot_plop=4
|
||||
create\:steam=4
|
||||
create\:sanding_short=4
|
||||
create\:sanding_long=4
|
||||
@@ -0,0 +1,99 @@
|
||||
# Enables/Disables all sound effects
|
||||
enabled=true
|
||||
# Affects how quiet a sound gets based on distance
|
||||
# Lower values mean distant sounds are louder
|
||||
# This setting requires you to be in singleplayer or having the mod installed on the server
|
||||
# 1.0 is the physically correct value
|
||||
attenuation_factor=1.0
|
||||
# The ray distance at which reverb starts
|
||||
# 0.0 disables reverb attenuation
|
||||
reverb_attenuation_distance=0.0
|
||||
# The volume of simulated reverberations
|
||||
reverb_gain=1.0
|
||||
# The brightness of reverberation
|
||||
# Higher values result in more high frequencies in reverberation
|
||||
# Lower values give a more muffled sound to the reverb
|
||||
reverb_brightness=1.0
|
||||
# The distance of reverb relative to the sound distance
|
||||
reverb_distance=1.5
|
||||
# The amount of sound that will be absorbed when traveling through blocks
|
||||
block_absorption=1.0
|
||||
# Higher values mean smaller objects won't be considered as occluding
|
||||
occlusion_variation=0.35
|
||||
# The default amount of sound reflectance energy for all blocks
|
||||
# Lower values result in more conservative reverb simulation with shorter reverb tails
|
||||
# Higher values result in more generous reverb simulation with higher reverb tails
|
||||
default_block_reflectivity=0.5
|
||||
# The default amount of occlusion for all blocks
|
||||
# Lower values will result in sounds being less muffled through walls
|
||||
# Higher values mean sounds will be not audible though thicker walls
|
||||
default_block_occlusion_factor=1.0
|
||||
# Minecraft won't allow sounds to play past a certain distance
|
||||
# This parameter is a multiplier for how far away a sound source is allowed to be in order for it to actually play
|
||||
# This setting only takes affect in singleplayer worlds and when installed on the server
|
||||
sound_distance_allowance=4.0
|
||||
# A value controlling the amount that air absorbs high frequencies with distance
|
||||
# A value of 1.0 is physically correct for air with normal humidity and temperature
|
||||
# Higher values mean air will absorb more high frequencies with distance
|
||||
# 0 disables this effect
|
||||
air_absorption=1.0
|
||||
# How much sound is filtered when the player is underwater
|
||||
# 0.0 means no filter
|
||||
# 1.0 means fully filtered
|
||||
underwater_filter=0.9
|
||||
# Whether sounds like cave, nether or underwater ambient sounds should have sound physics
|
||||
evaluate_ambient_sounds=false
|
||||
# The number of rays to trace to determine reverberation for each sound source
|
||||
# More rays provides more consistent tracing results but takes more time to calculate
|
||||
# Decrease this value if you experience lag spikes when sounds play
|
||||
environment_evaluation_ray_count=16
|
||||
# The number of rays bounces to trace to determine reverberation for each sound source
|
||||
# More bounces provides more echo and sound ducting but takes more time to calculate
|
||||
# Decrease this value if you experience lag spikes when sounds play
|
||||
environment_evaluation_ray_bounces=2
|
||||
# If sound hits a non-full-square side, block occlusion is multiplied by this
|
||||
non_full_block_occlusion_factor=0.25
|
||||
# The maximum amount of rays to determine occlusion
|
||||
# Directly correlates to the amount of blocks between walls that are considered
|
||||
max_occlusion_rays=16
|
||||
# The amount at which occlusion is capped
|
||||
max_occlusion=64.0
|
||||
# If enabled, the occlusion calculation only uses one path between the sound source and the listener instead of 9
|
||||
strict_occlusion=false
|
||||
# Whether to try calculating where the sound should come from based on reflections
|
||||
sound_direction_evaluation=true
|
||||
# Skip redirecting non-occluded sounds (the ones you can see directly)
|
||||
redirect_non_occluded_sounds=true
|
||||
# If music discs or other longer sounds should be frequently reevaluated
|
||||
update_moving_sounds=false
|
||||
# The interval in ticks that moving sounds are reevaluated
|
||||
# Lower values mean more frequent reevaluation but also more lag
|
||||
# This option only takes effect if update_moving_sounds is enabled
|
||||
sound_update_interval=5
|
||||
# The maximum distance a sound can be processed
|
||||
max_sound_processing_distance=512.0
|
||||
# Disable level clone and cache. This will fall back to original main thread access.
|
||||
# WARNING! Enabling this will cause instability and issues with other mods.
|
||||
unsafe_level_access=false
|
||||
# The radius of chunks to clone for level access
|
||||
level_clone_range=4
|
||||
# The maximum number of ticks to retain the cloned level in the cache
|
||||
level_clone_max_retain_ticks=20
|
||||
# The maximum distance a player can move from the cloned origin before invalidation
|
||||
level_clone_max_retain_block_distance=16
|
||||
# Enables debug logging
|
||||
debug_logging=false
|
||||
# Provides more information about occlusion in the logs
|
||||
occlusion_logging=false
|
||||
# Provides more information about the environment calculation in the logs
|
||||
environment_logging=false
|
||||
# Provides more information about how long computations take
|
||||
performance_logging=false
|
||||
# If enabled, the path of the sound will be rendered in game
|
||||
render_sound_bounces=false
|
||||
# If enabled, occlusion will be visualized in game
|
||||
render_occlusion=false
|
||||
# Enables/Disables sound effects for Simple Voice Chat audio
|
||||
simple_voice_chat_integration=true
|
||||
# Enables/Disables hearing your own echo with Simple Voice Chat
|
||||
simple_voice_chat_hear_self=false
|
||||
@@ -0,0 +1,12 @@
|
||||
#The render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 128
|
||||
# Range: 1 ~ 1000000
|
||||
physicsRenderDistance = 16
|
||||
#The minimum render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 16
|
||||
# Range: 1 ~ 1000000
|
||||
minPhysicsRenderDistance = 16
|
||||
#The maximum render distance for physics objects (SubLevels) in chunks.
|
||||
# Default: 4096
|
||||
# Range: 1 ~ 1000000
|
||||
maxPhysicsRenderDistance = 4096
|
||||
@@ -0,0 +1,11 @@
|
||||
version:3955
|
||||
renderDistance:8
|
||||
simulationDistance:8
|
||||
biomeBlendRadius:1
|
||||
mipmapLevels:2
|
||||
entityDistanceScaling:0.75
|
||||
particles:0
|
||||
entityShadows:true
|
||||
graphicsMode:1
|
||||
maxFps:120
|
||||
enableVsync:true
|
||||
+124
-10
@@ -4,14 +4,6 @@ hash-format = "sha256"
|
||||
file = "config/akashictome-common.toml"
|
||||
hash = "4e479c86ddfec0c5e28f07034f033a10abae404309f38ccc4bfc15da9de2dfa2"
|
||||
|
||||
[[files]]
|
||||
file = "config/alexsmobs-common.toml"
|
||||
hash = "bc47ee67032ceab8b2b408819ce9b268812f37da8f13b9b7efeae0deed607c4b"
|
||||
|
||||
[[files]]
|
||||
file = "config/crittersandcompanions-common.json5"
|
||||
hash = "1ac5a9b392b3aa980abe86b37aa8a9853e91c02cd18f8a1bceea0d08fe3f10c4"
|
||||
|
||||
[[files]]
|
||||
file = "config/defaultoptions/extra/config/DistantHorizons.toml"
|
||||
hash = "8949bf3c7a43df7a7ae384213db0985425db3559219c29cb63eaef45c7962dcc"
|
||||
@@ -45,8 +37,108 @@ file = "config/defaultoptions/options.txt"
|
||||
hash = "62034e0589087dab02d1a15192ad313a57f907d5ee9ebbf8853151f7831851a1"
|
||||
|
||||
[[files]]
|
||||
file = "config/fowlplay.json5"
|
||||
hash = "09ad767a358310e9d38e632ba0e670b0afd1554c162eadb6928d106df17b66c3"
|
||||
file = "config/modernfix-mixins.properties"
|
||||
hash = "ca92e91a2d856d42e4a5b1ba1900fbed0e942bcbeddd75c0195a2375ade75695"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/DistantHorizons.toml"
|
||||
hash = "46910495d79b540beed8137765fff4e14be3fd1860a5cadc73b38a6fbc0f3984"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/entity_model_features.json"
|
||||
hash = "89a8e21f43f512d78bfbc3ffb97b6d65cedc732a65ee565aed46499aa79fd57d"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/flywheel-client.toml"
|
||||
hash = "e10963bf43711e1d326e076a5b5baa5654b73d1a3878da6aa11c373b47c9e3ac"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/lambdynlights.toml"
|
||||
hash = "53a0c285438775ce29b8d44180feba1fd17b496eb0bbe5cb18684f2af1fdd3c9"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/sodium-options.json"
|
||||
hash = "c8c04fb322fc59c73e446f665522c3f0cdaafe6143b94b5895bb07f267489235"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/sound_physics_remastered/sound_rates.properties"
|
||||
hash = "c4676fcc924cc4bacc187d5364831aac1be170b93a2dc8e11119bff0235353e9"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/sound_physics_remastered/soundphysics.properties"
|
||||
hash = "912706e845015c30604db7ad0020a4ca95cdf4a94f7cad9eb8a166eab83f8279"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/config/ssrd-common.toml"
|
||||
hash = "9b75828543ed2b8cc5804eabecdc28262297a90d6f0e8e771d64ff779a69e3c9"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/deck/options.txt"
|
||||
hash = "c021331ed6ac5150025ccf8f61fc96793fdf2d2950617a85b68b977f7510ee47"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/DistantHorizons.toml"
|
||||
hash = "b4070cb07cf2fb9c7b14a2c49bc6b45b6aeb26d650fe8de80e4f9cbe67cb3ff5"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/entity_model_features.json"
|
||||
hash = "7650058e21d1eaafc14cc1879bc20c2b510eb9f3095914322b8b08fc31025437"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/flywheel-client.toml"
|
||||
hash = "8171fd8500c7bbd52df9fe4f8ea1b7abdc6a24868a9e7e54fa908b4725382bde"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/lambdynlights.toml"
|
||||
hash = "0406650cc9d47011360284e0019f237ba3c94fc3067dc2bb4171f9a5fbea8367"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/sound_physics_remastered/sound_rates.properties"
|
||||
hash = "c4676fcc924cc4bacc187d5364831aac1be170b93a2dc8e11119bff0235353e9"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/sound_physics_remastered/soundphysics.properties"
|
||||
hash = "912706e845015c30604db7ad0020a4ca95cdf4a94f7cad9eb8a166eab83f8279"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/config/ssrd-common.toml"
|
||||
hash = "9b744451175d440c4f2567d1fdd9799c2dc434012f5b1edb33cc7550193c4cef"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/high/options.txt"
|
||||
hash = "22900375cf1d9a608d9d19c8bdbd5bd497d47f66a11a57182de576e09497fd7d"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/DistantHorizons.toml"
|
||||
hash = "8949bf3c7a43df7a7ae384213db0985425db3559219c29cb63eaef45c7962dcc"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/entity_model_features.json"
|
||||
hash = "7650058e21d1eaafc14cc1879bc20c2b510eb9f3095914322b8b08fc31025437"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/flywheel-client.toml"
|
||||
hash = "517e16b3dc86f15b5dccd9ff4f70f767b73ebc2166f6ea2891565417d930772a"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/lambdynlights.toml"
|
||||
hash = "3876c8b0c49cc8c2a7f500bc5d77429035e4edb25e16d27434e1dd4d546ae510"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/sound_physics_remastered/sound_rates.properties"
|
||||
hash = "c4676fcc924cc4bacc187d5364831aac1be170b93a2dc8e11119bff0235353e9"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/sound_physics_remastered/soundphysics.properties"
|
||||
hash = "d93803ffc12f4b55f9668dc2b9a4823d733b77f02dfee0879d6507f262ad34c6"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/config/ssrd-common.toml"
|
||||
hash = "9b75828543ed2b8cc5804eabecdc28262297a90d6f0e8e771d64ff779a69e3c9"
|
||||
|
||||
[[files]]
|
||||
file = "config/onion-perf/mid/options.txt"
|
||||
hash = "62034e0589087dab02d1a15192ad313a57f907d5ee9ebbf8853151f7831851a1"
|
||||
|
||||
[[files]]
|
||||
file = "config/paxi/datapacks/onion-fixes/data/adorablehamsterpets/recipe/cucumber_crate.json"
|
||||
@@ -729,6 +821,11 @@ file = "mods/inventory-particles.pw.toml"
|
||||
hash = "7ff791a8a6074a5f580a0a1899de69cff45ccf6db0a99d83494d57ff1a4cee43"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris-flw-compat.pw.toml"
|
||||
hash = "ed85b2ed42beb6523223eef6f0ae397ed29f8059b2bf5aa08d8598de04865614"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/iris-veil-compat.pw.toml"
|
||||
hash = "69f9051c61c05546a2bf008654149a101814842bc2bf384df5c0ee0d64301ccd"
|
||||
@@ -844,6 +941,11 @@ file = "mods/mezzconfig.pw.toml"
|
||||
hash = "efad386063cb79013d7522fcc5b5879c5c3cd556e83bd41a56980632827b45b2"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/midnightlib.pw.toml"
|
||||
hash = "f7198ef16f4eefbe80ffd1d8ac7c2d323da4e68faf916a8b4b9c3e40f0913b80"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "mods/miners-delight.pw.toml"
|
||||
hash = "13481133d139f134bd3407983e29f86df835ff66bacb013d9d43e14f155241f4"
|
||||
@@ -1309,6 +1411,18 @@ file = "mods/zume.pw.toml"
|
||||
hash = "165466fe8e528309d95875396f2a5e13c0bab7bcb0c1134d1fd5f3a935d488f5"
|
||||
metafile = true
|
||||
|
||||
[[files]]
|
||||
file = "onion-perf-setup.bat"
|
||||
hash = "8e8e613429e5a2243829fa023c1dcc36b874ce42176bfa55dfa3c8a73122ccfd"
|
||||
|
||||
[[files]]
|
||||
file = "onion-perf-setup.ps1"
|
||||
hash = "86e12ff56f13229c3b0159eb2ea0cba403f35c6221be67037ea8481d370fa30d"
|
||||
|
||||
[[files]]
|
||||
file = "onion-perf-setup.sh"
|
||||
hash = "ecd5dfd5e135db6e9a98df65a7176cc2a9b3e14d7a140a3dcce90aae64d06519"
|
||||
|
||||
[[files]]
|
||||
file = "resourcepacks/better-leaves.pw.toml"
|
||||
hash = "c26f8da8b6f013fb7e917e29fc6ae783eb379c539da12d9d61196bc762fd26e1"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
name = "Iris & Oculus Flywheel Compat"
|
||||
filename = "iris-flywheel-compat-NeoForge-2.4.0.jar"
|
||||
side = "client"
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/ndHYMY2K/versions/6ZQz5Ow2/iris-flywheel-compat-NeoForge-2.4.0.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "192e5f00f2c146d2920867d7cd691ecec7ddd2acfb95880cc59084e8949225db92ecfb9221ba7223fc924ec43f2ca861115f83f9dc3f79e14a2f2a8db5287070"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "ndHYMY2K"
|
||||
version = "6ZQz5Ow2"
|
||||
@@ -0,0 +1,13 @@
|
||||
name = "MidnightLib"
|
||||
filename = "midnightlib-neoforge-1.10.0+1.21.1.jar"
|
||||
side = "both"
|
||||
|
||||
[download]
|
||||
url = "https://cdn.modrinth.com/data/codAaoxh/versions/vcVnfiCM/midnightlib-neoforge-1.10.0%2B1.21.1.jar"
|
||||
hash-format = "sha512"
|
||||
hash = "9586940ec144452577455b61fd7c9f7ee123044a47255f9425be1bfcd4e842d5902b66c7f5546540281cad8c12e30d38fc29e8fb3aa3cb592ed13ff430316e68"
|
||||
|
||||
[update]
|
||||
[update.modrinth]
|
||||
mod-id = "codAaoxh"
|
||||
version = "vcVnfiCM"
|
||||
@@ -0,0 +1,7 @@
|
||||
@echo off
|
||||
rem Onion MC: apply a performance tier (deck / mid / high) to this instance.
|
||||
rem Double-click it in the instance folder, or run: onion-perf-setup.bat high --yes
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0onion-perf-setup.ps1" %*
|
||||
set ONION_RC=%ERRORLEVEL%
|
||||
pause
|
||||
exit /b %ONION_RC%
|
||||
@@ -0,0 +1,183 @@
|
||||
# Onion MC: apply a performance tier's tuned settings to this instance.
|
||||
#
|
||||
# onion-perf-setup.bat [deck|mid|high] [--yes] (or run this .ps1 directly)
|
||||
#
|
||||
# Lives in the instance's game folder (the one that holds config\ and options.txt)
|
||||
# after a pack update. Close Minecraft first. Every file it changes is backed up to
|
||||
# onion-perf-backup\<date-time>\ first. options.txt is merged key by key: only the
|
||||
# tier's video keys change; keybinds, audio, resource packs etc. are left alone.
|
||||
# No network access. Windows PowerShell 5.1 compatible.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Fail([string]$msg) {
|
||||
[Console]::Error.WriteLine("error: $msg")
|
||||
exit 1
|
||||
}
|
||||
|
||||
$Game = $PSScriptRoot
|
||||
if (-not $Game) { $Game = Split-Path -Parent $MyInvocation.MyCommand.Path }
|
||||
|
||||
$Tier = ''
|
||||
$Yes = $false
|
||||
foreach ($a in $args) {
|
||||
$s = ([string]$a).ToLowerInvariant()
|
||||
if (@('deck', 'mid', 'high') -contains $s) {
|
||||
if ($Tier) { Fail 'more than one tier given' }
|
||||
$Tier = $s
|
||||
} elseif (@('-y', '--yes', '-yes', '/y') -contains $s) {
|
||||
$Yes = $true
|
||||
} else {
|
||||
Fail "unknown argument '$a' (use: deck | mid | high, optionally --yes)"
|
||||
}
|
||||
}
|
||||
|
||||
$Perf = Join-Path $Game 'config\onion-perf'
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Perf 'mid') -PathType Container)) {
|
||||
Fail "$Perf not found. Update the modpack first (it ships these settings), and run this script from the instance's game folder."
|
||||
}
|
||||
|
||||
$Interactive = [Environment]::UserInteractive -and -not [Console]::IsInputRedirected
|
||||
|
||||
if (-not $Tier) {
|
||||
if ($Interactive) {
|
||||
Write-Host 'Which tier fits this machine?'
|
||||
Write-Host ' deck Steam Deck, handheld, laptop/integrated graphics'
|
||||
Write-Host ' mid GTX 1060 / RX 580 class (~6 GB VRAM), 4-6 core CPU [pack default]'
|
||||
Write-Host ' high 8 GB+ VRAM graphics card (GTX 1080 Ti, RTX 3060 and up)'
|
||||
$Tier = (Read-Host 'Tier [mid]').Trim().ToLowerInvariant()
|
||||
if (-not $Tier) { $Tier = 'mid' }
|
||||
if (@('deck', 'mid', 'high') -notcontains $Tier) { Fail "unknown tier '$Tier'" }
|
||||
} else {
|
||||
$Tier = 'mid'
|
||||
}
|
||||
}
|
||||
$Src = Join-Path $Perf $Tier
|
||||
if (-not (Test-Path -LiteralPath $Src -PathType Container)) { Fail "$Src not found. Update the modpack first." }
|
||||
|
||||
if (-not $Yes) {
|
||||
if (-not $Interactive) { Fail 'not running interactively: close Minecraft, then re-run with --yes' }
|
||||
$ans = Read-Host 'Is Minecraft closed? [y/N]'
|
||||
if (@('y', 'yes') -notcontains $ans.Trim().ToLowerInvariant()) {
|
||||
Write-Host 'Close Minecraft and run this again.'
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$Stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$Backup = Join-Path $Game (Join-Path 'onion-perf-backup' $Stamp)
|
||||
$n = 1
|
||||
while (Test-Path -LiteralPath $Backup) {
|
||||
$Backup = Join-Path $Game (Join-Path 'onion-perf-backup' "$Stamp-$n")
|
||||
$n++
|
||||
}
|
||||
# .NET file calls, not New-Item/Copy-Item: instance folders often contain '[' (Prism
|
||||
# names the game dir after the instance) and -Path/Copy-Item -Destination glob those.
|
||||
[IO.Directory]::CreateDirectory($Backup) | Out-Null
|
||||
|
||||
function Save-Backup([string]$rel) {
|
||||
$from = Join-Path $Game $rel
|
||||
if (-not (Test-Path -LiteralPath $from -PathType Leaf)) { return }
|
||||
$to = Join-Path $Backup $rel
|
||||
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($to)) | Out-Null
|
||||
[IO.File]::Copy($from, $to, $true)
|
||||
}
|
||||
|
||||
function Test-SameFile([string]$a, [string]$b) {
|
||||
if (-not (Test-Path -LiteralPath $b -PathType Leaf)) { return $false }
|
||||
$x = [IO.File]::ReadAllBytes($a)
|
||||
$y = [IO.File]::ReadAllBytes($b)
|
||||
if ($x.Length -ne $y.Length) { return $false }
|
||||
return [Convert]::ToBase64String($x) -ceq [Convert]::ToBase64String($y)
|
||||
}
|
||||
|
||||
$replaced = 0
|
||||
$unchanged = 0
|
||||
Write-Host "Applying tier '$Tier' to $Game"
|
||||
$srcConfig = Join-Path $Src 'config'
|
||||
$files = @()
|
||||
if (Test-Path -LiteralPath $srcConfig) {
|
||||
$srcConfig = (Get-Item -LiteralPath $srcConfig).FullName.TrimEnd('\', '/')
|
||||
$files = @(Get-ChildItem -LiteralPath $srcConfig -Recurse -File | Sort-Object FullName)
|
||||
}
|
||||
foreach ($f in $files) {
|
||||
$rel = 'config' + $f.FullName.Substring($srcConfig.Length)
|
||||
$dst = Join-Path $Game $rel
|
||||
if (Test-SameFile $f.FullName $dst) { $unchanged++; continue }
|
||||
Save-Backup $rel
|
||||
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($dst)) | Out-Null
|
||||
[IO.File]::Copy($f.FullName, $dst, $true)
|
||||
Write-Host " replaced $($rel -replace '\\', '/')"
|
||||
$replaced++
|
||||
}
|
||||
|
||||
$Opt = Join-Path $Game 'options.txt'
|
||||
$SrcOpt = Join-Path $Src 'options.txt'
|
||||
$Utf8 = New-Object Text.UTF8Encoding $false
|
||||
if (-not (Test-Path -LiteralPath $SrcOpt -PathType Leaf)) {
|
||||
Write-Host ' (tier has no options.txt)'
|
||||
} elseif (-not (Test-Path -LiteralPath $Opt -PathType Leaf)) {
|
||||
Write-Host ' options.txt not found: skipped (Default Options seeds it on first launch)'
|
||||
} else {
|
||||
$skip = @('version', 'maxFps', 'enableVsync')
|
||||
$want = @{}
|
||||
$order = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($line in [IO.File]::ReadAllLines($SrcOpt, $Utf8)) {
|
||||
$i = $line.IndexOf(':')
|
||||
if ($i -lt 0) { continue }
|
||||
$k = $line.Substring(0, $i)
|
||||
if ($skip -ccontains $k) { continue }
|
||||
if (-not $want.ContainsKey($k)) { $order.Add($k) }
|
||||
$want[$k] = $line.Substring($i + 1)
|
||||
}
|
||||
$raw = [IO.File]::ReadAllText($Opt, $Utf8)
|
||||
$nl = "`n"
|
||||
if ($raw.Contains("`r`n")) { $nl = "`r`n" }
|
||||
$lines = $raw -split "`r?`n"
|
||||
$trailing = $raw.EndsWith("`n")
|
||||
if ($trailing) { $lines = $lines[0..($lines.Length - 2)] }
|
||||
if ($raw.Length -eq 0) { $lines = @() }
|
||||
$out = New-Object System.Collections.Generic.List[string]
|
||||
$done = @{}
|
||||
$changes = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($line in $lines) {
|
||||
$i = $line.IndexOf(':')
|
||||
if ($i -gt 0) {
|
||||
$k = $line.Substring(0, $i)
|
||||
if ($want.ContainsKey($k) -and -not $done.ContainsKey($k)) {
|
||||
$v = $line.Substring($i + 1)
|
||||
if ($v -cne $want[$k]) { $changes.Add(" ${k}: $v -> $($want[$k])") }
|
||||
$out.Add("${k}:$($want[$k])")
|
||||
$done[$k] = $true
|
||||
continue
|
||||
}
|
||||
}
|
||||
$out.Add($line)
|
||||
}
|
||||
foreach ($k in $order) {
|
||||
if (-not $done.ContainsKey($k)) {
|
||||
$changes.Add(" ${k}: (unset) -> $($want[$k])")
|
||||
$out.Add("${k}:$($want[$k])")
|
||||
}
|
||||
}
|
||||
if ($changes.Count -gt 0) {
|
||||
Save-Backup 'options.txt'
|
||||
# WriteAllText (not WriteAllLines): same UTF-8-without-BOM bytes, but joins with
|
||||
# the file's own line endings - WriteAllLines always uses Environment.NewLine.
|
||||
$text = [string]::Join($nl, $out.ToArray()) + $nl
|
||||
[IO.File]::WriteAllText($Opt, $text, $Utf8)
|
||||
Write-Host ' options.txt keys changed:'
|
||||
foreach ($c in $changes) { Write-Host $c }
|
||||
} else {
|
||||
Write-Host ' options.txt: already matches the tier'
|
||||
}
|
||||
}
|
||||
|
||||
if (@(Get-ChildItem -LiteralPath $Backup -Force).Count -eq 0) {
|
||||
Remove-Item -LiteralPath $Backup -Force
|
||||
$Backup = '(nothing changed, no backup needed)'
|
||||
}
|
||||
Write-Host "Done: tier '$Tier'; $replaced file(s) replaced, $unchanged already up to date."
|
||||
Write-Host "Backup: $Backup"
|
||||
Write-Host 'Also set Java memory in your launcher (5 GB is enough): see PLAYER-TUNING.md section 2.'
|
||||
exit 0
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/bin/sh
|
||||
# Onion MC: apply a performance tier's tuned settings to this instance.
|
||||
#
|
||||
# sh onion-perf-setup.sh [deck|mid|high] [--yes]
|
||||
#
|
||||
# Run it from (or point it at) the instance's game folder - the one that holds
|
||||
# config/ and options.txt; this script lives there after a pack update.
|
||||
# Close Minecraft first. Every file it changes is backed up to
|
||||
# onion-perf-backup/<date-time>/ first. options.txt is merged key by key: only the
|
||||
# tier's video keys change; keybinds, audio, resource packs etc. are left alone.
|
||||
# No network access. Windows: double-click onion-perf-setup.bat instead.
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
||||
case "$0" in
|
||||
*/*) GAME=$(cd "${0%/*}" && pwd) || die "cannot find the script's folder" ;;
|
||||
*) GAME=$(pwd) ;;
|
||||
esac
|
||||
|
||||
TIER=""
|
||||
YES=0
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
deck|mid|high) [ -z "$TIER" ] || die "more than one tier given"; TIER=$a ;;
|
||||
-y|--yes) YES=1 ;;
|
||||
-h|--help) sed -n '2,11p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) die "unknown argument '$a' (use: deck | mid | high, optionally --yes)" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
PERF="$GAME/config/onion-perf"
|
||||
[ -d "$PERF/mid" ] || die "$PERF not found. Update the modpack first (it ships these
|
||||
settings), and run this script from the instance's game folder."
|
||||
|
||||
INTERACTIVE=0
|
||||
[ -t 0 ] && INTERACTIVE=1
|
||||
|
||||
if [ -z "$TIER" ]; then
|
||||
if [ "$INTERACTIVE" = 1 ]; then
|
||||
cat <<'EOF'
|
||||
Which tier fits this machine?
|
||||
deck Steam Deck, handheld, laptop/integrated graphics
|
||||
mid GTX 1060 / RX 580 class (~6 GB VRAM), 4-6 core CPU [pack default]
|
||||
high 8 GB+ VRAM graphics card (GTX 1080 Ti, RTX 3060 and up)
|
||||
EOF
|
||||
printf 'Tier [mid]: '
|
||||
read -r TIER || TIER=""
|
||||
[ -n "$TIER" ] || TIER=mid
|
||||
case "$TIER" in deck|mid|high) ;; *) die "unknown tier '$TIER'" ;; esac
|
||||
else
|
||||
TIER=mid
|
||||
fi
|
||||
fi
|
||||
SRC="$PERF/$TIER"
|
||||
[ -d "$SRC" ] || die "$SRC not found. Update the modpack first."
|
||||
|
||||
if [ "$YES" != 1 ]; then
|
||||
[ "$INTERACTIVE" = 1 ] || die "not running interactively: close Minecraft, then re-run with --yes"
|
||||
printf 'Is Minecraft closed? [y/N]: '
|
||||
read -r ans || ans=""
|
||||
case "$ans" in y|Y|yes|YES|Yes) ;; *) echo "Close Minecraft and run this again."; exit 1 ;; esac
|
||||
fi
|
||||
|
||||
STAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP="$GAME/onion-perf-backup/$STAMP"
|
||||
n=1
|
||||
while [ -e "$BACKUP" ]; do BACKUP="$GAME/onion-perf-backup/$STAMP-$n"; n=$((n + 1)); done
|
||||
mkdir -p "$BACKUP" || die "cannot create $BACKUP"
|
||||
|
||||
backup() { # backup <path relative to the game dir>, if it exists
|
||||
[ -f "$GAME/$1" ] || return 0
|
||||
case "$1" in */*) mkdir -p "$BACKUP/${1%/*}" || die "cannot create backup folder" ;; esac
|
||||
cp -p "$GAME/$1" "$BACKUP/$1" || die "cannot back up $1"
|
||||
}
|
||||
|
||||
LIST=$(mktemp "${TMPDIR:-/tmp}/onion-perf.XXXXXX") || die "mktemp failed"
|
||||
REPORT=$(mktemp "${TMPDIR:-/tmp}/onion-perf.XXXXXX") || die "mktemp failed"
|
||||
trap 'rm -f "$LIST" "$REPORT"' EXIT
|
||||
(cd "$SRC" && find config -type f) | sort > "$LIST" || die "cannot list $SRC"
|
||||
|
||||
replaced=0
|
||||
unchanged=0
|
||||
echo "Applying tier '$TIER' to $GAME"
|
||||
while IFS= read -r rel; do
|
||||
if [ -f "$GAME/$rel" ] && cmp -s "$SRC/$rel" "$GAME/$rel"; then
|
||||
unchanged=$((unchanged + 1))
|
||||
continue
|
||||
fi
|
||||
backup "$rel"
|
||||
case "$rel" in */*) mkdir -p "$GAME/${rel%/*}" || die "cannot create folder for $rel" ;; esac
|
||||
cp "$SRC/$rel" "$GAME/$rel" || die "cannot write $rel"
|
||||
echo " replaced $rel"
|
||||
replaced=$((replaced + 1))
|
||||
done < "$LIST"
|
||||
|
||||
OPT="$GAME/options.txt"
|
||||
if [ ! -f "$SRC/options.txt" ]; then
|
||||
echo " (tier has no options.txt)"
|
||||
elif [ ! -f "$OPT" ]; then
|
||||
echo " options.txt not found: skipped (Default Options seeds it on first launch)"
|
||||
else
|
||||
TMP="$OPT.onion-perf.tmp"
|
||||
awk -v rep="$REPORT" '
|
||||
NR == FNR {
|
||||
sub(/\r$/, "")
|
||||
i = index($0, ":")
|
||||
if (i == 0) next
|
||||
k = substr($0, 1, i - 1)
|
||||
if (k == "version" || k == "maxFps" || k == "enableVsync") next
|
||||
if (!(k in want)) order[++n] = k
|
||||
want[k] = substr($0, i + 1)
|
||||
next
|
||||
}
|
||||
{
|
||||
cr = ""
|
||||
if (sub(/\r$/, "")) { cr = "\r"; crlf = 1 }
|
||||
i = index($0, ":")
|
||||
if (i > 0) {
|
||||
k = substr($0, 1, i - 1)
|
||||
if ((k in want) && !(k in done)) {
|
||||
v = substr($0, i + 1)
|
||||
if (v != want[k]) print " " k ": " v " -> " want[k] > rep
|
||||
print k ":" want[k] cr
|
||||
done[k] = 1
|
||||
next
|
||||
}
|
||||
}
|
||||
print $0 cr
|
||||
}
|
||||
END {
|
||||
for (j = 1; j <= n; j++) {
|
||||
k = order[j]
|
||||
if (!(k in done)) {
|
||||
print " " k ": (unset) -> " want[k] > rep
|
||||
print k ":" want[k] (crlf ? "\r" : "")
|
||||
}
|
||||
}
|
||||
}
|
||||
' "$SRC/options.txt" "$OPT" > "$TMP" || { rm -f "$TMP"; die "cannot merge options.txt"; }
|
||||
if [ -s "$REPORT" ]; then
|
||||
backup options.txt
|
||||
mv "$TMP" "$OPT" || die "cannot write options.txt"
|
||||
echo " options.txt keys changed:"
|
||||
cat "$REPORT"
|
||||
else
|
||||
rm -f "$TMP"
|
||||
echo " options.txt: already matches the tier"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$(ls -A "$BACKUP")" ]; then
|
||||
rmdir "$BACKUP"
|
||||
BACKUP="(nothing changed, no backup needed)"
|
||||
fi
|
||||
echo "Done: tier '$TIER'; $replaced file(s) replaced, $unchanged already up to date."
|
||||
echo "Backup: $BACKUP"
|
||||
echo "Also set Java memory in your launcher (5 GB is enough): see PLAYER-TUNING.md section 2."
|
||||
@@ -6,7 +6,7 @@ pack-format = "packwiz:1.1.0"
|
||||
[index]
|
||||
file = "index.toml"
|
||||
hash-format = "sha256"
|
||||
hash = "121d9177b8ca74535005b85160a3e314bc9c95f8c87671b3d6947cb8d52c37e1"
|
||||
hash = "f4b8ec15830662e033bd5c7723357a58aeeaf9dad1c5957f103961fb83ee0fd1"
|
||||
|
||||
[versions]
|
||||
minecraft = "1.21.1"
|
||||
|
||||
Reference in new issue
Block a user