ASK, DON'T GUESS — Ambiguity is a stop signal. If types, error handling, vanilla behavior, generated data, or architecture is unclear, ask. No speculative coding.
HONEST BLOCKERS — Don't hide uncertainty behind speculative changes or workarounds. State the real issue and ask the user for missing logs, paths, data, or vanilla references.
VERIFY BEFORE ASKING — Inspect relevant Steel code, generated/extracted data, vanilla source, and tests before asking. If still blocked, say what you checked.
CHECK-IN DISCIPLINE — Project rules override generic "keep going" behavior. Persistence stops when the next step depends on missing project facts, vanilla references, extractor output, or architectural intent. Do not keep retrying builds/tests, invent fallbacks, or hardcode data to make progress. Ask the user a narrow question with what you verified, what is missing, why guessing risks vanilla divergence, and the exact path/log/extractor output/decision needed.
- Stop and ask after repeated failures in the same category instead of cycling through variants.
- If data should come from SteelExtractor, update or request the extractor/output; do not transcribe hardcoded registry, worldgen, block, item, or protocol values from memory or guesses.
- If temporary hardcoded data is explicitly requested for a prototype, label it as prototype-only and keep it out of shared foundations unless the user approves the architectural debt.
Template: "I verified [sources]. The blocker is [missing data/decision]. Guessing would risk [vanilla divergence]. Please provide/confirm [exact file/path/output/decision]."
FOUNDATIONAL INTEGRITY — Don't implement features on missing foundations. No stubs, mocks, fake vanilla data, or hardcoded values (todo!(), const user = {id:1}) unless explicitly prototyping. Partial implementations must not land in shared foundations that downstream code depends on unless the missing scope is documented and approved.
VANILLA FUNCTIONALITY - Keep 1:1 Vanilla functionality for gameplay, protocol, registry, worldgen, and other vanilla-observable mechanics. Verify behavior against local vanilla source or extracted data before implementing those areas. Non-gameplay server foundations (permissions, command UX, admin tooling, configuration, domains) may improve on vanilla when they do not change gameplay outcomes; don't replace Steel architecture with vanilla limitations without user approval. If a faster or more idiomatic Rust solution diverges from vanilla-observable behavior or structure, get explicit permission first and leave a concise comment/doc explaining why. Seeded vanilla RNG is required for worldgen, loot/component determinism, protocol-visible deterministic systems, and save/load-repeatable behavior; incidental live gameplay randomness may use Steel's unseeded runtime RNG when the vanilla probability/range and observable behavior stay compatible.
VANILLA TARGET VERSION — Cargo.toml [workspace.package].version is authoritative; its +mc... suffix defines the targeted Minecraft data/source version. minecraft-src/ must be generated by update-minecraft-src.sh and kept current. If it is missing or stale and cannot be updated, stop and state the mismatch.
SKETCHY WORKAROUND PROTOCOL — Halt and ask permission before using:
.clone()to appease borrow checker,.unwrap()/.expect()in production,unsafeother than the keyedDowncastTypeexception belowAny/TypeIddowncasting in plugin-extensible foundations. It is sound within one linked Rust build, but its identity is not deterministic across builds; use Steel's keyed downcasting foundation instead.- Disabling linters, ignoring errors, deprecated deps
Template: "This requires [Hack] which risks [Consequence]. Proceed or solve root cause?"
DOWNCASTING — Exact concrete downcasting uses steel_utils::{Downcast, DowncastType, DowncastTypeKey}.
- A keyed
unsafe impl DowncastTypewith a unique owner-scoped key and a concise// SAFETY:comment is approved by default and does not require separate unsafe permission. Other unsafe code still follows the workaround protocol above. - Keys identify concrete Rust implementations, not Minecraft registry entries or translation keys. Steel-owned types use the
steel:namespace; plugins own their namespace. - Steel may downcast Steel-owned types and a plugin may downcast its own types. A key does not make a Rust type ABI-stable or permit one owner to reinterpret another owner's private type.
- Use capability traits for shared behavior, vanilla interfaces, and class-hierarchy checks. Exact downcasting is only for callers that require a specific concrete implementation.
CONSTRUCTIVE DISSENT — Treat user claims as hypotheses. Verify against local code/vanilla, call out mismatches, and challenge XY problems. Before building a complex system, identify the required vanilla-compatible foundations and ask when architecture is unclear.
Registries
- Generate only what is needed. If Minecraft uses a hardcoded transform, mirror that structure instead of inventing a broader registry system.
- Design registry code with future modding and ABI compatibility in mind. The current change does not need to implement plugin support, but every value/registry NeoForge can change should have a plausible extension path.
- Vanilla extracted registry/worldgen data should be compiled by build scripts into typed Rust data, not parsed from JSON at runtime. Prefer generated references like
vanilla_blocks::STONE/vanilla_fluids::WATERand registry ref types overIdentifierwhen the referenced vanilla value is known. - Do not design for runtime datapack JSON loading. Future plugins should register their own typed blocks/features with their own refs; build scripts only need to generate vanilla data.
- Avoid raw
BlockStateIdin generated registry data. UseBlockRefplus explicit properties/default-state resolution so registry ordering can still evolve for plugin support.
Code standard
- Use vanilla names unless they are misleading or a Rust design is explicitly approved. Document intentional differences on the relevant struct, method, or module.
- Minimize duplication when it reduces maintenance risk, but a few local lines are usually fine.
- Keep files focused and reasonably sized. Split a file once it contains distinct responsibilities or becomes difficult to navigate.
- Plan module structure around expected growth. If an area will contain many implementations, start with a directory and separate modules instead of accumulating them in one large file.
- Treat foundational systems with extra rigor; shortcuts in shared interfaces like block behavior become expensive once implementations depend on them.
- No workarounds. Create the right helper/abstraction when the code needs one.
- Don't add trivial wrapper methods that just alias an existing method. If
height()already exists, don't addget_y_size()that returnsself.height(). Use the existing method directly. - Prefer associated functions on the relevant type over standalone free functions when there's a clear owner.
- Avoid deep indentation; prefer guard clauses,
if let, andlet Some(...) = ... else { return }. - Use
Resultfor recoverable failures; panic only for impossible or fatal states. - Don't multithread something unless you can explain why it needs multithreading.
- Don't use async unless you need disk or network I/O.
// TODO:comments are fine for non-foundational follow-up work. Don't use TODOs to hide missing foundations, skipped vanilla behavior, or incomplete shared interfaces that implementations depend on.- Keep comments concise.
- After fixing something, don't leave a comment that only explains the old bug.
- Currently this project is in early development, we don't need to provide migrations.
Particle routing
- In Vanilla,
Level.addParticleandplayLocalSoundcalls from shared ticks are client-local, so Steel generally has no server-side work to perform for them. An explanatory comment can still be useful when an omission would otherwise look accidental. - Vanilla
ServerLevel.sendParticlescorresponds toWorld::send_particles; level and entity events continue to use their existing packets.
Testing
- Test meaningful behavior, especially advanced systems, edge cases whose expected handling is not obvious, code using unsafe (always use
// SAFETY:comments), and behavior that must match vanilla determinism (ItemComponent hashing or worldgen). - Don't add tests merely to increase coverage. Avoid tests that only restate constants, field values, type shapes, or other facts the compiler or a direct code inspection already makes obvious.
- A test should catch a plausible regression or quickly communicate non-obvious behavior. If it would fail only when the implementation has been edited to contradict an equally obvious assertion, omit it.
- Keep small, focused tests beside their implementation. Move a growing test suite into a dedicated test module file or integration test directory before it makes the implementation file difficult to navigate.
- After code changes, run the narrowest check/test that exercises the touched code. If you cannot run it, state exactly why.
- Suppress clippy lints with
#[expect(clippy::lint_name, reason = "...")]. False positives and intentional deviations (e.g., function length for readability) are acceptable when explained.
GENERATED CODE — Never modify generated files directly:
- Generated Rust under these
src/generated/paths is intentionally ignored/untracked. Commit the build script, extractor output, or source asset that produces it, not the generated Rust output. steel-registry/src/generated/→ modifysteel-registry/build/steel-core/src/behavior/generated/→ modifysteel-core/build/steel-core/src/entity/generated/→ modifysteel-core/build/steel-worldgen/src/generated/→ modifysteel-worldgen/build/steel-utils/src/generated/→ modifysteel-utils/build/- Block/item behavior registration is generated from
#[block_behavior]/#[item_behavior]; add annotated structs understeel-core/src/behavior/, not manual generated registration. - Treat
*/build_assets/*.jsonand*/test_assets/*.jsonpopulated by SteelExtractor as extracted data, not hand-authored source. If extracted JSON data is wrong or missing, update the relevant SteelExtractor extractor, rerun it, and copy only the produced file(s) needed for the change. - If extracted JSON data is missing and the extractor path/output is not available, tell the user exactly what data is required; they can provide the extractor path.
- Generated Rust and extracted JSON may be minified onto one very long line. Line-limited reads like
sed -n '1,160p'orheadare not enough to understand these files; usergfor the target symbol/key first, then format a temporary copy if more context is needed. - To inspect generated Rust, write a disposable formatted copy outside the repo. Read/search the copy; never write formatted output back to
src/generated/or commit it. - To inspect extracted JSON, pretty-print a disposable copy outside the repo. If the formatter is unavailable, state that instead of guessing from a truncated one-line view.
SteelExtractor workflow
- Use the local SteelExtractor checkout when extracted data is needed; default output is
run/steel_extractor_output/inside that checkout. - Copy only needed produced files into matching repo-relative paths like
steel-registry/build_assets/,steel-core/build/,steel-core/test_assets/,steel-worldgen/build_assets/, orsteel-utils/build_assets/. - If the checkout, output, or file is missing and cannot be regenerated, ask for the exact path/command/output instead of recreating data by hand.
cargo build # Build
cargo run # Run
cargo check # Fast compile check
cargo test # Tests
cargo clippy -r --all-targets --all-features # CI lint
cargo check -p steel-core # Fast game/worldgen check
cargo fmt --all --check # CI formatting
typos # CI spelling
prek run --all-files # Full local precommit suiteUses nightly Rust.
Tooling: ast-grep is available for structural code search/rewrites.
Steel = Minecraft server in Rust.
Crates: steel (thin wrapper) -> steel-login (initial connection) → steel-core (game logic) → steel-worldgen (worldgen) → steel-protocol (packets) → steel-macros (derives) → steel-registry (generated data) → steel-utils/steel-math (common) → steel-crypto (encryption)
Steel:
| Area | Path |
|---|---|
| Entry / networking | steel/src/ |
| Game logic | steel-core/src/ |
| Player | steel-core/src/player/ (networking.rs = packet handlers) |
| World | steel-core/src/world/ |
| Worldgen orchestration | steel-core/src/worldgen/ |
| Worldgen algorithms/data | steel-worldgen/src/ |
| Chunks | steel-core/src/chunk/ |
| Block/item behaviors | steel-core/src/behavior/ |
| Block entities | steel-core/src/block_entity/ |
| Entities | steel-core/src/entity/ |
| Inventory / menus | steel-core/src/inventory/ |
| Packets | steel-protocol/src/packets/game/ |
| Codegen build scripts | steel-core/build/, steel-registry/build/, steel-worldgen/build/, steel-utils/build/ |
Vanilla (minecraft-src/minecraft/):
| Area | Path |
|---|---|
| Source root | src/net/minecraft/ |
| Packet handlers | src/.../server/network/ServerGamePacketListenerImpl.java |
| Blocks | src/.../world/level/block/ |
| Entities | src/.../world/entity/ |
| Player | src/.../server/level/ServerPlayer.java |
| Worldgen region | src/.../server/level/WorldGenRegion.java |
| Worldgen features/structures | src/.../world/level/levelgen/ |
The minecraft-src/ folder should be generated by update-minecraft-src.sh and kept current with the target version from Cargo.toml.
steel_utils::locks::SyncMutex<T>/steel_utils::locks::SyncRwLock<T>— aliased parking_lot for easier comprehensionWeak<T>— prevent reference cycles- If you have related values bundle them over a struct that is put behind a
SyncMutex