skills/bevy/SKILL.md
Bevy 0.18 ECS patterns for the Endless colony sim. Use when writing Rust/WGSL for this project.
npx skillsauth add abix-/claude-blueprints bevyInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
4 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
https://github.com/bevyengine/bevy/tree/release-0.18.0/examples. Bevy's API changes significantly between versions. main branch and other release tags will have wrong signatures, removed types, or renamed modules.rust/src/, shaders: rust/assets/shaders/, assets: rust/assets/sprites/bevy_embedded_assets (ReplaceAndFallback mode for modding)docs/README.md (architecture), docs/roadmap.md (feature tracking)rust/src/lib.rs. build_app(), Step enum, system schedulingrust/src/systems/behavior.rs. Decision system (three-tier throttle), SystemParam bundle examplesrust/src/systems/ai_player.rs. AI town brain, personality-driven building/expansion/miningrust/src/constants.rs. BUILDING_REGISTRY, NPC_REGISTRY, tuning constantsrust/src/tests/mod.rs. Test framework infrastructurerust/src/tests/vertical_slice.rs. 8-phase end-to-end testrust/src/components.rs. All ECS componentsrust/src/render.rs. Camera, tilemap, sprite loading, click selection, box selectrust/src/npc_render.rs. Instanced NPC + building rendering pipelinerust/src/gpu.rs. Compute shader dispatch, readback, NpcGpuState, NpcVisualUploadrust/src/world.rs. WorldData, WorldGrid, building placement, place_building() unifiedrust/src/save.rs. Save/load with version checking# run builds automatically before launching
k3sc cargo-lock run --release --manifest-path /c/code/endless/rust/Cargo.toml 2>&1
# Tracy profiler support (connect with Tracy GUI while running):
k3sc cargo-lock run --release --manifest-path /c/code/endless/rust/Cargo.toml --features tracy 2>&1
Game exposes JSON-RPC on localhost:15702. See endless-cli skill for all commands.
EguiPlugin::default() not EguiPlugin (struct with fields, not unit struct)contexts.ctx_mut() returns Result. Use let Ok(ctx) = contexts.ctx_mut() else { return }; (NOT .unwrap(). Panics on first frame before fonts load)Local<bool> guard to skip frame 1EguiPrimaryContextPass schedule, NOT Update. Systems in Update render visually but buttons won't respond to clicksfn my_ui(mut contexts: EguiContexts) -> Result { let ctx = contexts.ctx_mut()?; ... Ok(()) }.into() on string literals when bevy_egui is in scope. Ambiguous From impls. Pass &str directly or use format!()#[derive(SystemParam)] bundles to group related params (see systems/behavior.rs, tests/mod.rs CleanupCore/CleanupExtra)#[derive(States, Default)] with #[default] on variant, app.init_state::<S>(), in_state(S::Variant) run condition, OnEnter/OnExit for transitions, ResMut<NextState<S>> to triggerNextState::set() now triggers transitions even to the same state (OnEnter/OnExit fire). Use set_if_neq() to only transition when the state actually changes.Four ordered phases via Step enum:
Step::Drain → Step::Spawn → ApplyDeferred → Step::Combat → Step::Behavior → collect_gpu_updates
GpuReadState resourcespawn_npc_system, apply_targets_systemprocess_proj_hits → cooldown → attack → damage → death → cleanup (chained)collect_gpu_updates runs after Behavior, batches all GpuUpdateMsg into GPU_UPDATE_QUEUEBevy 0.18 uses MessageWriter<T> / MessageReader<T> (not EventWriter/EventReader):
fn my_system(mut writer: MessageWriter<DamageMsg>) {
writer.write(DamageMsg { npc_index: idx, amount: 10.0 });
}
fn consume(mut reader: MessageReader<DamageMsg>) {
for msg in reader.read() { /* ... */ }
}
Register with .add_message::<T>() not .add_event::<T>().
Reader/Writer conflict: A system cannot have both MessageReader<T> and MessageWriter<T> for the same T. Fix: separate drain system that reads into a Resource flag, scheduled .before() the system that writes.
Systems emit GpuUpdateMsg(GpuUpdate::SetTarget { idx, x, y }) etc.
→ collect_gpu_updates drains into GPU_UPDATE_QUEUE (single Mutex lock)
→ populate_gpu_state (PostUpdate) drains into NpcGpuState (per-index dirty tracking, compute fields only)
→ build_visual_upload updates dirty visual slots in persistent NpcVisualUpload (event-driven, not full rebuild)
→ Extract phase reads both via Extract<Res<T>> (zero clone, immutable reference)
→ Compute data: per-dirty-index upload with coalescing. Visual data: per-dirty-index upload (full rebuild on startup/load).
→ Coalesced uploads: Adjacent dirty indices merged into single write_buffer calls. Two strategies: strict coalescing (exact adjacency only) for GPU-authoritative buffers (positions, arrivals). No gap merging; gap-based coalescing (merge nearby indices with gap threshold) for CPU-authoritative buffers (targets, speeds, factions, healths). Reduces wgpu call count dramatically.
SetTarget. Multiple systems submit intents with MovementPriority; resolve_movement_system picks highest priority per entity and emits the single SetTarget. Prevents competing systems from overwriting each other's movement goals.GpuComputePlugin adds render graph node NpcComputeNodeReadback)ShaderStorageBuffer assets as readback targets: npc_positions, combat_targets, npc_factions, npc_health, proj_hits, proj_positionsReadbackHandles resource (ExtractResource) holds handles, extracted to render worldcopy_buffer_to_buffer from compute buffers → readback asset buffers (via RenderAssets<GpuShaderStorageBuffer>)Readback::buffer(handle) entities fire ReadbackComplete observers each frame (async, no blocking poll)Res<GpuReadState>, Res<ProjHitState>, Res<ProjPositionState>GpuReadState + ProjPositionState have ExtractResource. Cloned to render world for instanced renderingNpcGpuState. Only changed compute slots get uploaded, not entire buffersTilemapChunk (Bevy built-in). Terrain (62K tiles) as one chunk. Built once from WorldGrid, not per-frame.RenderCommand pattern hooked into Transparent2d phase. 6 instanced layers (body + 5 overlay: weapon, helmet, armor, item, status/healing).RenderCommand + phase items insteadNpcInstanceData: position[2] + sprite[2] + color[4] + health + flash + scale + atlas_id = per-instanceprepare_npc_buffers builds instance buffer from Res<GpuReadState> (positions) + NpcVisualUpload (sprites, colors, equipment)npc_render.wgsl. Camera extracted from Bevy Camera2d transform, atlas sampling with alpha discardGpuSlotPool. LIFO free list with GPU lifecycle. alloc_reset() allocates + queues GPU state wipe, free() pushes to free list + queues GPU hide. Max=MAX_ENTITIES=200K (unified NPC+building namespace).
EntityUid(u64). Monotonically increasing, never reused. Solves ABA slot-reuse bugs where a freed slot gets reallocated to a different entity, but old references still point to the slot.
EntityUid (not raw slot): DamageMsg.target, NpcWorkState.occupied_building, BuildingInstance.npc_uid, Squad.membersEntityMap provides: uid_for_slot(slot), slot_for_uid(uid), entity_for_uid(uid)BuildingInstance.npc_uid is set in the same frame as spawnNextEntityUid resource allocates fresh UIDs; save/load preserves+restores the counterActivity (what they're doing) × CombatState (combat overlay). Replaced 13 marker components.
Activity: Idle, Working, OnDuty{ticks_waiting}, Patrolling, GoingToWork, GoingToRest, Resting, GoingToHeal, HealingAtFountain{recover_until}, Wandering, Raiding{target}, Returning{loot: Vec<(ItemKind, i32)>}CombatState: None, Fighting{origin}, Fleeingactivity.is_transit() → true for movement activities (Patrolling, GoingToWork, GoingToRest, GoingToHeal, Wandering, Raiding, Returning)Job::Farmer(0), Job::Archer(1), Job::Raider(2), Job::Fighter(3), Job::Miner(4), Job::Crossbow(5)GpuSlot(usize), Health(f32), CachedStats (max_health, damage, range, cooldown, speed, etc.), Energy(f32), Faction(i32), TownId(i32), BaseAttackType (Melee/Ranged), ManualTarget enum (Npc/Building/Position variants. Player-forced targets)Activity::Returning { loot: Vec<(ItemKind, i32)> }. Generic loot vec replaces has_food/gold fields. activity.add_loot() merges matching ItemKind.occupied_building + work_target_building as Option<EntityUid>. UID-based for ABA safety. Replaces old AssignedFarm/WorkPosition Vec2-based components.#[require] for invariants: When component B must always accompany component A, use #[require(B)] on A so the invariant is declarative, not a manual insert you can forget.These broke during the migration and were fixed in commits. Reference when touching render code:
RenderSet::* → RenderSystems::* (e.g. Prepare → PrepareResources, Queue → Queue)entry_point is Option<Cow<str>>. Use Some(Cow::from("vertex")) not "vertex".into()DepthStencilState: format Depth32Float, depth_write_enabled: false, depth_compare: GreaterEqual. Without this, nothing renders.&Msaa in queue_npcs and passed to pipeline specialization. specialize() key is (bool, u32) for (HDR, sample_count). MultisampleState::count must match the view's MSAA.render_device.create_bind_group_layout() → BindGroupLayoutDescriptor::new() (deferred creation)BindGroupLayoutDescriptor, get actual layout via pipeline_cache.get_bind_group_layout(&descriptor) at bind group creation timeSetMesh2dViewBindGroup removed from bevy::sprite_render. Texture bind group goes in slot 0commands.get_or_spawn(entity).insert(Component) no longer workscommands.spawn((Component, MainEntity::from(entity))). But these need lifecycle handling (despawn stale copies)MaterialDrawFunction replaced with phase-specific variants (e.g. MainPassOpaqueDrawFunction). Custom render pipelines must use the correct phase draw function.EntityRow → EntityIndex, Entity::row() → Entity::index() (0.18 rename)ROQueryItem takes two lifetime params: ROQueryItem<'w, 'w, ...> (not one)AppState::TestMenu (default) / AppState::Running. State machine drives test lifecycleTestState resource: shared by all tests, tracks phase, counters, flags, pass/failTestRegistry holds Vec<TestEntry> (name, description, phase_count, time_scale)test_is("name") run condition gates per-test systemssetup (OnEnter Running) + tick (Update after Behavior), both gated by test_is()OnExit(Running): despawn NpcIndex entities, reset all resourcesRunAllState with queue, auto_start_next_test fires on OnEnter(TestMenu)cleanup_test_world despawns NpcIndex entities, but if a test spawns other entities (FarmReadyMarker, projectiles, etc.), add a query for them too. Leaked entities break subsequent tests in Run All.auto_start_next_test pops RunAllState.queue), the completion handler should only check is_empty(), not also pop. Two consumers = skipped entries.github.com/bevyengine/bevy/tree/release-0.18.0/examples.
Always the version-tagged tree, never main.bevy-cheatbook.github.io): cross-version reference,
noisy on newer features but reliable for ECS fundamentals.bevyengine.org/learn/migration-guides):
read the one for the version you're upgrading to. Bevy breaks API
every minor release.Established Bevy patterns that win in this codebase and across the ecosystem:
Query<&Transform> is cheaper to iterate than
Query<(&Transform, &Velocity, &Health, &Equipment)> if you only
need the position.With<T> / Without<T> for filtering, not as a column. Filters
don't touch the data, just the archetype set.Changed<T> and Added<T> filters prune iteration to the
modified subset. Every &mut T access flips the change tick;
prefer Mut<T>::set_if_neq (Bevy 0.13+) to avoid spurious
invalidation.ParallelIterator (Query::par_iter_mut) for embarrassingly
parallel per-entity work. Setup cost is ~10us; not worth it for
<100 entities or work <1us each.EntityMap + spatial grid
before / after ordering over relying on insertion
order. Bevy's scheduler is deterministic but not stable across
refactors.apply_deferred / ApplyDeferred between systems that spawn
and systems that query the spawned entities. Bevy 0.16+ uses
ApplyDeferred as a system; insert it explicitly when the order
matters.Res<T> is read-only and parallel; ResMut<T>
is exclusive. Many ResMuts in one system serialize the schedule.
Split into smaller resources or use interior mutability sparingly.Local<T> for per-system state that doesn't need to be a
resource. Cheaper than a Resource and not visible to others.&mut T access invalidates change tick, even if the value
didn't change. Use set_if_neq(new) (PartialEq required) for
cheap deduplication.Resource change detection is the same: writing to ResMut<T> is
considered a mutation, even for a no-op write. Reach for messages
/ events when "did anything change" is the question.check_change_ticks).Approximate per-system overhead on modern hardware (Endless target):
| Work | Approx |
|------|--------|
| System dispatch (idle pass) | ~50ns |
| Query::iter() over 10k entities, narrow tuple | ~30us |
| Query::par_iter_mut setup | ~10us |
| Commands::spawn() | ~200ns (deferred; flush at ApplyDeferred) |
| world.spawn() (exclusive system) | ~150ns |
| Mut<T>::set_if_neq (no change) | ~5ns |
| BRP request roundtrip | ~1-5ms |
Use these as guides, not commitments. Always bench when "it should be fast" matters.
rust/benches/system_bench.rs for the affected system before merging. No exceptions.buffer[..npc_count] not buffer[..MAX_NPCS]. Flash decay, visual sync. Only process live slots.MessageWriter<BuildingGridDirtyMsg> / MessageReader<BuildingGridDirtyMsg> to gate expensive systems (building grid rebuild, patrol routes, squad cleanup, terrain sync). Replaced central DirtyFlags resource. DirtyWriters SystemParam bundles all dirty signal writers. Avoids Bevy's is_changed() false positives from ResMut borrows.rebuild_patrol_routes_system.for_each_nearby_kind_town() / find_nearest_worksite() backed by per-cell (kind, town, cell) buckets with SpatialBucketRef back-index for O(1) swap-remove. Cell-ring expansion for bounded spatial search.Without<Building>, Without<Dead> filters instead of entity_map.iter_npcs() + Query.get(). EntityMap retained only for keyed/spatial/building lookups. Pattern: focused per-system query tuple with only needed columns.hidden_indices from GpuUpdate::Hide clears stale visual/equip data. No full-array sentinel fill per frame. Building loop uses iter_instances() not full slot scan. resize(-1.0) initializes new capacity only.Vec<usize> tracks live projectile indices. Render iterates active_set instead of scanning 0..proj_count, skipping inactive slots entirely.DecisionNpcState, NpcDataQueries, DeathResources, SaveNpcQueries, DirtyWriters. Group related queries to stay under 16-param limit while enabling focused per-system access.materialize_test_world hook in tests/mod.rs runs on first Update in AppState::Running before Step::Behavior, calls world::materialize_generated_world(). All test scenes share this path. No per-test manual building spawns.NpcGpuData.npc_count, NOT positions.len() / 2. Buffers are pre-allocated to MAX_NPCS.SPRITE_SIZE must match atlas cell size (16px), not an arbitrary render size.Extract<Res<T>>) when you need custom logic or zero-clone ownership patterns. Render world cannot write back.Readback + ReadbackComplete. Prefer MessageWriter everywhere else.Res<Foo> AND a SystemParam bundle that also borrows Foo, Bevy silently skips the system (white screen, no error). Fix: remove the standalone Res<Foo> and access through the bundle.commands.spawn() batch entities must despawn stale copies first. Without this, render world accumulates duplicate entities every frame.row = higher Y = north on screen. Higher col = east.MovementPriority::Squad; the movement system deduplicates unchanged targets and the priority system resolves conflicts (Survival > Squad > JobRoute).&[NpcDef]): Single source of truth for all NPC types. Drives spawner logic, roster UI colors, upgrade categories, squad recruitment, start menu sliders, world gen.&[BuildingDef]): Static definitions with SpawnBehavior enum, HP, tower_stats, worksite, save_key, tileset index. Drives save/load, placement, healing, destruction, spawner resolution. No match arms needed.BuildingInstance has kind, position, slot, town_idx, faction, occupants, npc_uid, under_construction, etc. No WorldData.buildings. PlacedBuilding only in save.rs for backward compat.BuildingKind enum + BuildMenuContext with selected_build: Option<BuildingKind> + build_tab: DisplayCategory for category tabs (Economy/Military/Tower).UpgradeStatDef per NPC category (Military, Farmer, Miner, Town). UpgradeStatKind enum for stat types. Prereqs, multi-resource costs, EffectDisplay variants (Percentage, CooldownReduction, Unlock, FlatPixels, Discrete). Stored per-town in TownUpgrades.NpcDef.loot_drop: LootDrop (item, min, max). ItemKind enum (Food, Gold + 9 equipment variants: Helm, Armor, Weapon, Shield, Gloves, Boots, Belt, Amulet, Ring). ItemDef registry + item_def(kind) lookup. Death drops loot, buildings can be looted. equipment_drop_rate only non-zero for Raiders (0.30). All other NPC types have 0.0. Equipment items: roll_loot_item() → killer's CarriedLoot.equipment → delivered to TownInventory on return home → auto-equip system distributes hourly.NpcEquipment::all_items() + CarriedLoot.equipment transfer to killer at 50% per-item (deterministic hash roll). NPC killers → CarriedLoot, tower killers → TownInventory directly.place_building() (world.rs): Unified function for all building creation (player, AI, world gen, save/load). Validates position, deducts cost, creates BuildingInstance in EntityMap, allocates GPU slot, sets HP, fires dirty signals, updates wall auto-tile. Takes BuildContext for runtime validation (water/foreign territory rejection). hp_override for save/load.BUILDING_CONSTRUCT_SECS (10s at 1x). BuildingInstance.under_construction: f32 tracks remaining seconds. construction_tick_system in Step::Behavior. Spawners dormant during construction, growth system skips. World-gen buildings are instant.destroy_building() (world.rs): Grid-only cleanup (combat log + wall auto-tile). Callers send DamageMsg for entity death. Single Dead writer is death_system.let _ = place_building(). Placement errors (out of bounds, occupied, water) are silently discarded. Must verify free slots before placing via BRP.angle = f32(i) * 2.399 + f32(j) * 0.7dodge_unlocked param gates projectile dodge behavior (upgrade-driven)is_tower flag for tower auto-attack behaviordevelopment
YAML standards for config files, Ansible playbooks, k8s manifests, GitHub Actions, docker-compose, and any project config. Built from the YAML 1.2 spec, yamllint defaults, and the practical pitfalls (Norway problem, type coercion, anchor gotchas).
development
--- name: ueforge description: ueforge framework: the base layer every UE4SS Rust mod in the Grounded2Mods workspace builds on. Authoritative on the composition model (Effect/Trigger/Skill), the Def/Registry/Instance/Controller pattern, hot reload, discovery, hardening doctrine, and the five framework modules (rpg, stacks, difficulty, inventory, damage). Use when writing or modifying code under `ueforge/` in [abix-/Grounded2Mods](https://github.com/abix-/Grounded2Mods), or when promoting a patte
tools
TypeScript and JavaScript standards. Sourced from [abix-/chromium-extensions](https://github.com/abix-/chromium-extensions) (Hush + filter-anything-everywhere). Use when writing TS/JS, including browser extension bootstrap shims, MV3 service workers, and small web frontends.
development
--- name: schedule1 description: Modding Schedule 1 (TVGS, IL2CPP Unity + MelonLoader + Harmony). Authoritative on Schedule 1 game specifics: engine type, MelonLoader/Il2CppInterop references, eMployee mod root-cause findings, vanilla CookRoutine + StartMixingStationBehaviour internals, certainty-tracking discipline. Mod code lives in [`abix-/Schedule1Mods`](https://github.com/abix-/Schedule1Mods) (the `EmployeeReset` sidecar is the current shipped mod). Not for playing the game. user-invocable: