72 Commits
Author SHA1 Message Date
Claude cc474268e7 Launcher: a server on another computer, run over SSH
An Entry on the Servers list can name a host: its folder is then on that
computer, and Entry::command runs the same horizon-server arguments there
as `ssh HOST horizon-server --dir DIR ...`, with the system's ssh and the
player's keys (BatchMode, so no password is asked for or kept). Each
argument goes to the remote shell in single quotes, and a host that ssh
could read as an option is refused. remote::check asks the server there
for its status and says in plain words what is missing: a key, the host,
horizon-server, or a server in that folder.

Lists written before keep reading as they were; nothing on the page uses
remote entries yet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
2026-11-05 17:48:14 +00:00
Claude a94f171297 Merge main into fix/native-build-smart-app-control-5911
Resolves the launcher/lang/*.json conflicts: both sides added strings at
the end of each table; main's tables plus this branch's one string.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y255SLkXwxkdNVZnS8dNbf
2026-11-05 16:49:33 +00:00
Claude 469f8d9b31 Native builds: explain Smart App Control only when it blocks (#5911)
Tested on Windows 11 with Smart App Control on (VerifiedAndReputablePolicyState
1): a build whose programs already ran before it was turned on still
succeeds, so refusing every build when the registry value is 1 stopped
builds that work. The check before the download is gone; a build that hits
os error 4551 still leads its error with what to do.

The message is translatable now, and says Windows also blocks some of the
toolchain's own programs (dlltool.exe was refused in a player's build), not
only the ones compiled on the computer.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y255SLkXwxkdNVZnS8dNbf
2026-11-05 15:42:23 +00:00
Claude 918db2564f Explain Smart App Control blocking native builds (#5911)
Windows 11's Smart App Control lets only signed programs run, and a native
build runs programs it has just compiled (build scripts, horizon-mixins, the
game), so every build failed with os error 4551 and an opaque cargo error.

The launcher now checks Smart App Control before downloading anything
(VerifiedAndReputablePolicyState 1) and says how to turn it off; a build
that hits os error 4551 anyway leads its error with the same explanation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y255SPgcPcs89pQpqu1eES
2026-11-05 13:21:51 +00:00
Claude ce86945277 Size LabPBR maps from the texture budget
LabPBR normal and specular maps had a fixed 64 MiB budget, so with about
1300 block layers they always loaded at 64 px, however high
textureBudgetMiB was set. They now get a quarter of the current budget:
the default budget keeps the old 64 MiB, and 1536 MiB gives 256 px maps.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QPo9BpqkYWfY6y5Nwu
2026-11-04 23:55:03 +00:00
Claude 82f601072b Server panel: the launcher's Glass look, a page for each part, in-page dialogs
The panel takes the Glass look from docs/design-system.md: the sunset with pixel ridges, frosted
plates, Geist and Bricolage Grotesque Heading, the theme's Hugeicons. What it already did gets a page
each in a sidebar (overview, players, console, backups, crash reports), and confirm()/prompt() become
in-page dialogs. The supervisor serves the theme's fonts and the mark; the CSP gains font-src 'self'.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Jh5A2PzdusBChqE38VZ3zD
2026-11-04 23:29:16 +00:00
Claude 7a767d30db Merge remote-tracking branch 'upstream/main' into feat/native-mob-submit 2026-11-01 12:43:51 +00:00
Claude 88784b6910 Merge remote-tracking branch 'upstream/main' into fix/native-summon-orbs-passengers
# Conflicts:
#	crates/horizon-client/src/game/native_entities.rs
2026-11-01 02:50:55 +00:00
Claude 64aa0b4473 Merge remote-tracking branch 'upstream/main' into feat/native-camera-shake
# Conflicts:
#	docs/native-mods.md
2026-11-01 02:50:26 +00:00
Claude a62c609529 Merge remote-tracking branch 'upstream/main' into feat/native-shader-params
# Conflicts:
#	docs/native-mods.md
2026-11-01 02:50:25 +00:00
Claude c6a5386e9f Merge remote-tracking branch 'upstream/main' into feat/native-local-sound
# Conflicts:
#	docs/native-mods.md
2026-11-01 02:50:24 +00:00
Claude a8b7cdc12e Merge remote-tracking branch 'upstream/main' into feat/native-falling-blocks
# Conflicts:
#	crates/horizon-client/src/game.rs
#	docs/native-mods.md
2026-11-01 02:50:23 +00:00
Claude 82dee152fb Launcher: keep counting a secret another process renewed in the file
Review fix for the secrets file (#5095): when the keychain took a
secret but another process had written a newer copy to the file
meanwhile, the newer copy rightly stayed, yet the run stopped counting
it as seen in the file. Once that run stopped asking the keychain and
another process handed the copy over, the run couldn't read it.

The clean-up now says whether the file still has the entry, and the run
keeps counting it while it does.

Test: a launcher on the file renews A during a start's hand-over of A,
the keychain refuses B; after another start hands both over, the first
start reads the renewed A and B.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 01:32:00 +00:00
Claude a307a24a63 Launcher: count the secrets file's keys before the start's hand-over
Review fix for the secrets file (#5095): a start counted what its failed
hand-over left in the file only afterwards, so a hand-over by another
process in between left it reporting the keychain away for those
accounts. The start now counts the file's keys first, and drops each
one it hands over itself.

Test: another process hands the account over right as this start's
hand-over fails; the start still reads it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 01:25:42 +00:00
Claude f9fe596cba Launcher: a run started on an old secrets file finds it after a hand-over
Review fix for the secrets file (#5095): a launcher that started while
the keychain was away, on a file a previous run left, asked the
keychain again only for secrets it had written itself. Once another
process handed the file over, it reported the keychain away for
accounts the keychain now held.

It now also counts what it read from the file and what its own start
left there. Test: two launchers started on an old file while the
keychain is away (one read the account, one didn't) both read it after
a third hands it over.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 01:15:21 +00:00
Claude f795e86510 Launcher: serialize keychain writes so a hand-over can't undo a sign-out
Review fixes for the secrets file (#5095):

- A start's hand-over wrote a snapshot of the file to the keychain, so
  a token another process signed out or renewed meanwhile could come
  back or be overwritten. Every keychain write now holds
  account-secrets.keychain.lock, and the hand-over reads each entry
  again under it. The file's own lock still never waits on a prompt.
- MCP secrets_file is true only while the file holds a sign-in: not
  for sign-out marks alone, nor for a file that can't be read.
- Tests: a hand-over held on a prompt while a running launcher signs
  out or renews the same account; what counts as a sign-in in the file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 01:03:11 +00:00
Claude 82ea5d145f Launcher: never hold the secrets file's lock while the keychain prompts
Review fix for the secrets file (#5095): Layered asked the keychain
while holding account-secrets.lock, so an unlock prompt held up every
other launcher and CLI process.

- Each call reads its file entry under the lock, asks the keychain
  with the lock released, then removes the entry only if it still
  holds what was read: another process's newer write stays. The start's
  hand-over works the same way, from a snapshot.
- Once the keychain refuses or doesn't answer (a dismissed prompt),
  the run stops asking it and parks new secrets in the file, until it
  finds that another process handed the keychain one it parked.
- Tests: a keychain waiting on a prompt while another process reads and
  writes the file, a dismissed prompt asked once a run, and a half
  refused hand-over followed by a sign-out.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 00:54:18 +00:00
Claude ab8797a5bd Launcher: keep the Tailscale token out of the secrets file, say where sign-ins sit
Review fixes for the secrets file (#5095):

- The Tailscale access token administers the player's whole tailnet:
  it goes through account::keychain(), the keychain or memory for the
  run, never the secrets file.
- MCP account_list says secrets_file: true while sign-ins sit in the
  file, beside keychain (sign-ins outlast the launcher).
- launcher/README.md and the sign-out help (docs/launcher-commands.md,
  regenerated) no longer say tokens are never in a file.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 00:42:09 +00:00
Claude b617e637b4 Launcher: read the keychain and the secrets file together on Linux
Review fixes for the secrets file (#5095):

- No store is picked once per run any more: on Linux and FreeBSD every
  read takes the file's copy first and the keychain's otherwise, and
  every write goes to the keychain, to the file only when it refuses.
  A launcher that wrote to the file keeps reading, renewing and signing
  out after another process hands the file to the keychain.
- Each start hands the keychain the file's secrets one at a time, each
  leaving the file once the keychain took it, so a move it half refuses
  never leaves a secret in both places, and signing out removes it.
- Signing out while the keychain is away leaves an empty entry that
  hides the keychain's copy until it can be deleted.
- macOS and Windows keep the keychain or memory, as before.
- The concurrency test now runs real child processes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 00:37:06 +00:00
Claude d1eca94f9c Launcher: lock the secrets file across processes, hand it back to the keychain
Review fixes for the secrets file (#5095):

- Every read-modify-replace holds account-secrets.lock (std File::lock),
  so launcher and CLI processes signing in or out at once no longer
  overwrite each other; each write uses a temporary file of its own.
- The file no longer sticks once made: at the first start the keychain
  answers, its secrets move into the keychain and the file is removed,
  so an account kept in the keychain before a start it missed comes
  back beside the ones signed in meanwhile. If the keychain refuses any,
  the file stays whole and in use.
- A broken file's error names only the error kind, line and column:
  serde's own message can quote a string the file holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 00:26:28 +00:00
Claude e7b5953ba4 Launcher: fix the secrets-file tests' names and comparison
kept() clashed with the tests' own helper, and Secrets has no PartialEq.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-11-01 00:04:41 +00:00
Claude e878abf1fd Launcher: keep Microsoft sign-ins in a file where no keychain answers
Many Linux systems run no Secret Service (GNOME Keyring, KWallet), and
the launcher then kept sign-ins in memory only: players signed in again
at every start (#5095).

Where the keychain doesn't answer, the secrets now go to
account-secrets.json in the launcher's data folder, readable by the
player only (0600 on Unix) and replaced whole through a temporary file.
The keychain stays first; a secrets file from an earlier start stays in
use so a flaky keychain doesn't sign players in and out, and signing the
last account out removes it. HORIZON_NO_KEYCHAIN still keeps secrets in
memory for the launcher's tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01DG1mYGWXNxQzmhMnjB36p1
2026-10-31 23:58:12 +00:00
Claude 4a361d034b native_summon: a root a mod kept out has no id
spawn_loaded_entities hands out the id before native_entity_joins runs,
and a cancelled orb doesn't give it back, so the fallback returned an id
no entity holds. Take it only when an orb holds that id (root_id), and
test both cases.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QT61YPiPzVNysmVHsH
2026-10-31 22:48:42 +00:00
Claude f2d940e4c8 Biome modifiers: test one biome id and a list of biomes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NLEqWLKW2LHgtLer35pT8m
2026-10-31 20:54:36 +00:00
Claude 1fedaa1c5a Content mods: place mod features in biomes (biome modifiers)
A content mod's placed features now generate in vanilla's biomes through
NeoForge's biome modifiers (data/<ns>/neoforge/biome_modifier/, and
forge/biome_modifier/ for older mods): add_features, remove_features and
none. When the content root builds they rewrite the biomes' feature
lists, an added feature at the end of its step as NeoForge adds it; the
generator's FeatureSorter then orders them as vanilla does. Biomes no
modifier names keep their text, and a root with no modifiers keeps
vanilla's bundle. An unknown biome, tag, feature, step or type fails the
build with the mod and file named.

Part of #4953, closes #5431.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NLEqWLKW2LHgtLer35pT8m
2026-10-31 20:45:38 +00:00
Claude 8a6eaac999 native_summon: return experience orbs' ids, and place passengers without a Pos
Follow-up to #5408. A summoned orb spawned but native_summon returned None,
since no command Target lookup covers orbs: return the stack's first
spawned id. Passengers without Pos were not placed and got lost: share
/summon's recursive place helper (place_summoned), which also supplies
the Fire and Rotation defaults.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QT61YPiPzVNysmVHsH
2026-10-31 20:39:58 +00:00
Claude 436d039047 native_explode: a nested blast starts sourceless and gives the outer blast its state back
Called from native_explosion_detonate, native_explode took the outer
blast's source, cause, owner and hive/rail flags. Save and clear them
around server_explode, then restore them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QT61YPiPzVNysmVHsH
2026-10-31 20:32:34 +00:00
Claude 6b5780b686 Merge remote-tracking branch 'upstream/main' into feat/native-camera-shake 2026-10-31 19:33:10 +00:00
Claude f58fe06685 Native hooks: finite keeps its own doc comment 2026-10-31 19:33:10 +00:00
Claude 1ec53c7a3f Merge remote-tracking branch 'upstream/main' into feat/native-falling-blocks
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-31 19:21:14 +00:00
Claude 4e72c2b5b5 Merge remote-tracking branch 'upstream/main' into feat/native-entities
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-31 19:21:12 +00:00
Claude b7b36ccbc7 Merge remote-tracking branch 'upstream/main' into feat/native-explode
# Conflicts:
#	crates/horizon-client/src/game.rs
2026-10-31 19:21:02 +00:00
Claude d356420e76 Merge main into feat/native-shader-params
Resolves the conflicts with the water reflection fields: Frame.mods now
follows reflection_resolution at offset 528.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01XnU6QRonhDhVY6koHuncJx
2026-10-31 19:10:34 +00:00
Claude 71955880b7 Draw a head profile's texture override instead of its skin
A ResolvableProfile can carry a `texture` id (PlayerSkin.Patch body, a
ClientAsset.ResourceTexture). Vanilla draws a player head with that
resource-pack texture instead of the downloaded or default skin; Horizon
ignored it and drew the profile's default skin. Placed heads and head
items now read the patch, loading non-vanilla namespaces' textures from
the resource packs into a skin slot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01P1ZTfB3AfwKgGnv7UFKYVD
2026-10-31 18:22:56 +00:00
Claude d63d89bb86 Show the armour stand preview in the smithing table
SmithingScreen.updateArmorStandPreview puts the result on a stand without
its base plate and with its arms (armour in its slot, anything else in the
off hand), drawn by extractEntityInInventory beside the slots. The world's
armour stand drawing is split out per stand so the screen can reuse it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:46:36 +00:00
Claude 72f66bb71c Draw map markers on the cartography table's preview
MapRenderer.render in a screen draws the map's decorations over its image,
all of them since the map isn't in an item frame.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:42:28 +00:00
Claude aced527885 Draw the enchanting table's book in its screen
EnchantmentScreen.tickBook/extractBook: the book opens while an offer has a
cost and turns to a new page when the enchanted item changes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:26:59 +00:00
Claude bec89a8927 Draw the parrots on the player's shoulders
A parrot that lands on its owner is kept in ShoulderEntityLeft/Right, but
nothing drew it, so it seemed to vanish. Port ParrotOnShoulderLayer and
AbstractClientPlayer.getParrotVariantOnShoulder: the parrot model in
ParrotModel.Pose.ON_SHOULDER with the player's walk animation, age and
head turn, 0.4 to the side and 1.5 (1.3 crouching) up the player's model,
on the player in third person and in the inventory preview.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:26:58 +00:00
Claude 648531c6da Play the happy ghast's riding sound while the player rides one
LocalPlayer.startRiding starts a RidingEntitySoundInstance for a happy
ghast (entity.happy_ghast.riding, volume 5 * clampedLerp(speed, 0, 1)),
as it does the two inside sounds for a minecart; Horizon only had the
minecart's. Port the instance: it stops when the player gets off or the
ghast is silenced, and is silent under water and below 0.01 speed.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-30 18:26:58 +00:00
Claude 396ba2e5e7 Turn the player when a server sends PlayerLookAt
ClientboundPlayerLookAtPacket was decoded as an unknown packet, so
`/teleport ... facing` and `/rotate ... facing` on a server never turned
the camera. Decode it and port ClientPacketListener.handleLookAt:
Entity.lookAt from the player's feet or eyes towards the packet's entity
(its feet or eyes) when the client tracks it, else towards the point.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-29 22:42:00 +00:00
Claude 0801c954b5 Drag the stonecutter and villager trade scrollers
The scrollers on the stonecutter and merchant screens only moved with the
mouse wheel: pressing and dragging them did nothing. Port
StonecutterScreen.mouseClicked/mouseDragged and
MerchantScreen.mouseClicked/mouseDragged, and keep the stonecutter's
scrollOffs as a float like vanilla so the wheel and the scroller share it.

Part of #3518.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Rx5M6NS763D2WVnb2HmcTH
2026-10-29 22:14:39 +00:00
Claude f4bdc66781 Never wait on the owner's profile fetch: list its skin once it lands
The owner's login to its server thread is main's again: no properties
and no wait. The player list gives the integrated server's owner the
signed-in account's properties instead (Hub::player_info), taken from
the background fetch only if it has already answered, so a slow, failing
or absent network can't delay opening a world. A LAN guest joins after
the world is open, well after the fetch.

Tests: the owner's login doesn't wait on a fetch that never answers; the
player list's owner entry before, after and without the fetch; and in
tests/lan_bot.rs (needs the prepared resources) a TCP guest's player
list carries the owner's textures and none of its own.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CxQvrTahuwEpRxJpTvWpjG
2026-10-29 17:15:39 +00:00
Claude ba6575314c Draw the empty main hand on the main arm, and hide it while invisible
renderArmWithItem draws an empty main hand with renderPlayerArm on the
player's main arm (the left one when the main hand is set to left) and
only when the player isn't invisible. hand_geometry always drew the right
arm and drew it while invisible. The map arms (#3444) already follow
both rules.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-30 00:23:45 +10:00
Claude f4f9897c9e Extras: show your own name tag in third person
A new extra, Own name tag (ownNameTag, off by default), under a Third
person header on the Extras page. With it on, the player's own model in
third person gets the name tag other players see: the profile name in
its team's colours, the below-name score, faded while crouching. It goes
through the same checks as other players' tags (team name tag
visibility, distance, F1) and hides in first person, while sleeping, in
spectator mode and while invisible. Vanilla never names the camera
entity, so this stays off by default.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 23:26:36 +10:00
Claude 26fa54369c Show your own skin: offline skins from the launcher, the host's skin on LAN
- The game now reads HORIZON_OFFLINE_SKIN / HORIZON_OFFLINE_SKIN_MODEL,
  which the launcher already sets for an offline name, and draws that skin
  (slim or classic) on the local player where neither the server nor a
  signed-in account gives one. It was read for capes only.
- On the window's own server thread (single player, and the world opened
  to LAN), the owner's memory connection now logs in with the signed-in
  account's profile properties (IntegratedServer.getSingleplayerProfile),
  so the player list carries the owner's signed skin to LAN guests instead
  of the default one. The profile is fetched while the world loads; the
  login waits for it at most 10 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01CxQvrTahuwEpRxJpTvWpjG
2026-10-29 12:55:49 +00:00
Claude 0e4c964d0a Keep Player.attack's no-damage guard before the attacker's sprint-hit cut
Player.attack ends before hurtOrSimulate and causeExtraKnockback when
neither the scaled ATTACK_DAMAGE (clamped to [0, 2048], so Weakness can't
push it below 0) nor the scaled enchantment damage is left. The remote
path cut the attacker's speed on knockback alone, so an empty-hand sprint
hit under Weakness I still slowed the client.

The decision moves to attack_strength::attacker_slows_after_hit, with the
knockback formula shared with the local path as
attack_strength::extra_knockback, and unit tests for a charged and an
uncharged sprint hit, a knockback weapon, no sprint, a no-damage hit
under Weakness, enchantment damage alone, and a server-raised attack
speed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 14:06:14 +10:00
Claude 2228a7456e Apply the attacker's 0.6 speed cut after a knockback hit on another player
MultiPlayerGameMode.attack runs Player.attack on the client too. For a
mob the client's hurtOrSimulate fails, so the hit has no attacker side,
but RemotePlayer.hurtClient is true: a sprinting (or knockback-enchanted)
hit on another player runs causeExtraKnockback's attacker side on the
client, cutting horizontal speed to 0.6 and stopping the sprint. The
remote attack path only sent the packet, so after such a hit the client
kept its speed while the server's player slowed, and the server pulled
the player back (seen on Hypixel when sprint-hitting a nearby player).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 13:57:52 +10:00
Claude 0291a5864f Look up players' teams by name for client-side pushes on servers
remote_pushes found teams through mob_teams, which keys entities by UUID
text and the local player by PLAYER_NAME. On a server, players are team
members by profile name (Player.getScoreboardName), so neither lookup hit
and every player pushed the local player, even on teams with
collisionRule never. The server applied no push and corrected the
position, so walking into players jittered (seen on Hypixel).

Use the profile name for the local player and the tab list name for other
players; other entities keep their UUID.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018ksHJwFp11RfYk6S9xnyCf
2026-10-29 00:13:19 +10:00
Claude Codeandminimax-m3.1-flash 2dc154ff7f The structure pieces ask isSolidRender, not the collision cube
The legacy structure pieces asked "is the collision shape a full block"
for the blocks around them, when they choose a chest's facing
(StructurePiece.reorient) and when the mineshaft checks its floor. That
is a different set of blocks than vanilla's BlockState.isSolidRender:
glass, leaves, ice, a spawner and a barrier are full collision blocks
but do not draw as solid, and soul sand and mud draw as solid without
a full collision shape.

The pieces now ask the blocks' own facts, the flag the worldgen already
keeps for every state (BlockProperty::SolidRender, which the exporter
writes from state.isSolidRender()), the way the monster room's own
reorient does. The chest's facing is decided in one function that takes
the blocks around the chest, so the rule can be checked against
vanilla's.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 19:50:19 +05:00
Claude Codeandminimax-m3.1-flash 25b84dfc2e Format the world preset test
rustfmt keeps the registry read on one line.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash d98dc8edc9 Read the test registry without the explicit deref
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash bf05176692 Add the amplified, large-biome and single-biome world types
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.

The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 16:11:14 +05:00
Claude Codeandminimax-m3.1-flash 0104eeaf33 Format the world preset test
rustfmt keeps the registry read on one line.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 05:06:02 +05:00
Claude Codeandminimax-m3.1-flash 729a224eef Read the test registry without the explicit deref
clippy's explicit_auto_deref asks for the LazyLock itself, which
derefs to the registry the generator data keeps.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:52:31 +05:00
Claude Codeandminimax-m3.1-flash 52f0e38d41 Add the amplified, large-biome and single-biome world types
The create screen only offered Default and Superflat, so the amplified
world type of #1873 could not be chosen at all, and the generator
always built the overworld's noise settings.

The world-type button now cycles through the presets vanilla's
WorldPresetTags.NORMAL tag holds (normal, flat, large_biomes, amplified,
single_biome_surface), the chosen preset is written into the new world's
world_gen_settings.dat with the generator
worldgen/world_preset/<preset>.json has, and the generator is built from
that preset, so an amplified world gets the amplified noise settings.
A world opened again reads its preset back from its generator.

Co-Authored-By: minimax-m3.1-flash (Claude Code) <[email protected]>
2026-10-27 04:20:01 +05:00
Claude 71edc9d41c Prepare with Minecraft's own Java 25 when no JDK is installed
On a Mac without a JDK, preparing failed with "Java 25 is needed ...
/usr/bin/javac -version printed no version: Unable to locate a Java
Runtime": /usr/bin/javac is macOS's placeholder. Mojang's Java runtimes
are whole JDKs, javac included (java-runtime-epsilon is 25.0.1 on every
platform), and the official launcher has already downloaded one for
anyone who played 26.3.

- horizon-prepare: java::find also tries Minecraft's runtimes
  (<game dir>/runtime/<component>/<platform>/<component>, jre.bundle on
  macOS) and the launcher's pick (Options::java_home), and names the
  macOS placeholder for what it is.
- launcher: when no JDK is found, downloads the runtime the version JSON
  names (javaVersion.component) from Mojang into its own game folder,
  every file checked against its sha1, executables and links as the
  official launcher lays them out. A version's own preparer binary or
  Python script gets it through JAVA_HOME and the PATH.

Checked for real on Linux: the download is 147 files, its javac 25.0.1
compiles and runs a class with --release 25.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AxbYUJt4ShJFCdyLNCxF21
2026-10-22 14:12:42 +00:00
Claude e01296f99b Keep the pointer locked on macOS when the game captures it
winit's macOS set_cursor_position re-associates the mouse with the
pointer (CGAssociateMouseAndMouseCursorPosition(true)), so warping to
the window centre right after the Locked grab released it again and the
pointer moved freely in first person. Warp before grabbing on macOS;
Wayland still warps after, since it moves a pointer only while locked.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0188LkUPntcZdfMF3wAqR2tK
2026-10-22 13:48:04 +00:00
Claude 5bad717213 Perlin noise without libm floor or int-to-float conversions, same values
floor_i32 is now Java's Mth.floor (truncate, step down for negative
fractions): f64::floor is a libm call on baseline x86-64, made for every
Perlin sample. Perlin's gradient table is stored as floats (every
component is -1, 0 or 1, exact either way), so the corner dot products
skip three conversions each. About 10% fewer instructions generating
terrain; values unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
2026-10-22 08:43:40 +00:00
Claude bc9e5afacc Faster feature decoration: region grid, ore column fast path, lazy biome cache
TerrainRegion indexes its chunks through a grid over their bounding box.
OreFeature asks the world whether any column under its blob reaches the
floor heightmap in one call, and tracks tested positions in a bitset.
BiomeTemperature no longer preallocates its map and queue. Same output.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
2026-10-22 06:26:36 +00:00
Claude dfe795bc09 Faster Perlin volumes and climate search, same results
PerlinNoise::add_volume worked out each Y coordinate's cell, offset and
fade once per X/Z column. Every column walks the same Y coordinates, so
they are now computed once per volume. The arithmetic is unchanged, so
the output is bit for bit the same.

ClimateIndex kept its tree as nodes with a Vec of child ids each. The
built tree is now renumbered breadth first into flat arrays, so a
branch's children are one contiguous run. The search visits the same
nodes in the same order, so ties resolve exactly as before.

terrain_bench (seed 12345, 8x8 chunks, 1 thread, medians of 3 runs):
terrain 14.5 -> 12.4 ms/chunk, biomes 1.20 -> 1.06 ms/chunk.
Checked against vanilla 26.3 in-process: 2304 chunks on seeds 12345, -1
and 987654321987 identical, and the biome-source and biome-generation
parity suites (2.1M lines) match.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01M3pNrSHA2ifNrH7SAMHgSW
2026-10-22 02:42:52 +00:00
Claude 9019d708d0 Streaming: answer "near a player?" from a grid of the players' squares
On a server every player is an extra area, and streaming asks of every
loaded and wanted column, several times a tick (unloading, dropping
protos, finishing, decoration, the generation scan), whether one of
those squares reaches it: a scan of every player's square per question.
The squares are now one grid of distances, made again when a player
changes chunk, and in_extra reads it. Differential test against the scan
over 60 random layouts, every margin.

At 50 players, 6081 columns, 3 questions each: 0.45 ms -> 0.04 ms, plus
0.02 ms to make the grid.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:40:47 +00:00
Claude 186777110f Hoppers: skip the item scan when no item reaches the hopper's column
HopperBlockEntity.suckInItems scanned every item entity for each hopper
that ticked without a container above. During the block-entity pass the
game now keeps the set of block columns item boxes reach; a hopper whose
column is not in it has nothing to suck, and the scan (same order, same
item taken) runs only for the rest. Items do not move within the pass:
removals leave a superset, and additions (pushed at the end with a new
id) or removing the last item rebuild the set. It is dropped at the end
of the pass.

  500 hoppers,  200 items:  1.36 -> 0.05 ms per tick
  2000 hoppers, 500 items: 13.52 -> 0.26 ms per tick
  2000 hoppers, 2000 items: 60.6 -> 3.3 ms per tick

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:40:38 +00:00
Claude cb0bbe7d19 Sculk: skip the loaded-chunk diff while no chunk came or went
sculk_sync_index collected every loaded chunk key into a set and diffed
it against the known chunks every ten ticks, thousands of keys on a busy
server. It now remembers the chunk map's membership revision and skips
the diff while it is unchanged; a replaced chunk keeps its key and was
not rescanned before either.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:06 +00:00
Claude 863a309161 Allays and piglins: hoist per-tick lookups out of the per-mob loop
tick_allays scanned every block entity of the level for each allay,
every tick, to find a playing jukebox within 10 blocks. The playing
jukeboxes are now collected once, at the first allay (no allay starts
or stops one).

piglin_pickups looked up the piglin_loved tag and hashed every item
entity's id once per piglin. Each item's flag is now computed once and
kept until an item is added (always pushed at the end under a new id)
or removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:06 +00:00
Claude 3917f35e3e Natural spawning: answer mob box queries from a grid after a few scans
GameLevel's isUnobstructed, noCollision (living shulkers) and the
spawners' entity count each scanned every mob, computing each 1.5 KB
mob's box. A spawn pass holds the game immutably, so after eight
queries it builds a grid of the mobs' boxes once and reads candidates
from it; the first queries still scan, as the build costs a few scans.

  100 box tests, 2000 mobs: 3.48 -> 0.31 ms (grid build included)

Also gives the mob_ai_probe example the new others_index field.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude a893fa6125 Villagers: count the others' POI tickets once per search
pois_in_range (AcquirePoi and the job, bed and meeting point searches)
asked, for every candidate POI in range, how many tickets it had, and
each answer scanned every other mob for villagers holding it: POIs times
mobs per search. The others' tickets are now counted into a map at the
first candidate and read from it; this villager's own memories are
still checked per POI, as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 3b21a975bb Mob goals: read nearby mobs from the tick's grid in four more places
FollowParentGoal (every baby, every tick), the pufferfish's puff check,
the enderman's block placing and the iron golem's DefendVillage scanned
every mob. They now read candidates from the position grid around the
area they test (one block wider where they test positions, as a box
whose bottom face holds the position meets it), in snapshot order, and
test exactly as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 67ca65bb3b Mob tick: drop gone mobs' guest bindings with a set, not a scan per binding
On a server every ticked mob is bound to its nearest player, and each tick
the bindings of mobs that are gone were dropped by scanning every mob for
each binding. Mob is 1.5 KB, so the scan misses the cache on every mob:
with 1000 loaded mobs that was 0.44 ms a tick, with 2000 1.86 ms; a set of
the live ids makes it 0.02 and 0.04 ms (stand-in structs of Mob's size).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 6216b84669 Mob tick: build the mob world once a tick, index the snapshot
tick_creature_entities called mob_world_base for every mob. That rebuilt
the player view (its allies set scans every mob UUID) and, with any
scoreboard team, an owned copy of every mob's team, each a clone of the
team's player set: cubic in the mobs on a team. The base is now built
once and cloned per mob, with the teams borrowed behind an Arc.

  100 mobs, half on a team:  37.7 ms -> 0.06 ms per tick
  500 mobs, half on a team:  4297 ms -> 0.31 ms per tick

MobWorld::seen looked mobs up by scanning the snapshot; the mob ticks
now pass an id index (first slot wins, as the scan did): 2000 lookups
over 2000 mobs 1.09 -> 0.01 ms. The nearest-living-entity sensors, the
look-at goal, the ravager's roar and MobWorld::nearby read candidates
from the tick's position grid instead of every mob, in the same order:
2000 mobs 0.39 -> 0.11 ms per tick.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:10:05 +00:00
Claude 2ecbdd57eb Mob pushes: one list per mob once a crowd is pushing
Pushes between mobs wait in a list until their target ticks; each mob's
tick scanned the whole list, and take_mob_pushes removed each push from
the events in place, shifting the rest. In a pen where every mob pushes
its neighbours both grow with the square of the crowd per mob.

The pending pushes stay a list while short (64) and become one list per
mob past that, each in push order, so every velocity adds up the same
values in the same order. take_mob_pushes splits the events in one pass,
keeping the order of the others. A test checks velocities bit for bit
and the remaining events against the old code.

  30 mobs pushing each other:   0.11 -> 0.04 ms per tick
  100 mobs pushing each other:  1.73 -> 0.37 ms
  200 mobs pushing each other:  9.07 -> 2.61 ms

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:02:34 +00:00
Claude 62874d8dc4 Furnaces: look up the cooking recipe in an index, without cloning it
Every lit furnace, smoker and blast furnace called Recipes::cook each
tick, which scanned all cooking recipes (string compares and hash
lookups) and the furnace then cloned the recipe found, its ingredient's
item set included. Recipes now keeps, built at the first lookup, the
first recipe of each kind for each item, which is what the scan
returned; the furnace borrows it.

  1000 lookups over 320 recipes: 2.07 -> 0.08 ms

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:02:00 +00:00
Claude da314fa658 Server tick: cheaper view requeue and chunk ticket levels
Two costs that grow with players on the server thread:

- Hub::update_view rebuilt a player's chunk queue on every chunk crossing
  by scanning the queue (a Vec) once for each chunk of the view. It now
  checks a set. Same queue, same order (differential test against the old
  scan). 111 -> 16 us per crossing at view 10, 6.5 ms -> 0.13 ms at 32.
- ChunkTickets rebuilt a hash map of every ticketed chunk whenever any
  player changed chunk, and every block entity, mob and ticking chunk
  hashed into it. Levels are now a grid over the tickets' bounding box,
  with the map kept for tickets spread too far apart. At 50 players:
  build 0.23 -> 0.08 ms, lookups about 2x faster (differential test over
  200 random ticket layouts).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01UxXpf7d4RgpespL2G2UsGE
2026-10-22 01:02:00 +00:00