Skip to content

Commit 06f706e

Browse files
authored
feat(bevy_npc): add agnostic NPC plugin with proto-based data model (#7998)
* feat(bevy_npc): add agnostic NPC plugin with proto-based data model New bevy_npc package mirroring npcdb.proto as the source of truth. Includes NpcRegistry for O(1) lookup by ULID/slug, ECS components (NpcId, NpcSlug, NpcCombatStats, NpcBundle), a resolve system that hydrates NpcSlug entities into full NpcBundle, and optional serde support for JSON loading. * refactor(bevy_npc): switch to prost codegen matching bevy_items pattern Replace hand-written Rust types with prost-generated structs from npcdb.proto. Adds build.rs (gated behind BUILD_PROTO env var), NpcDb registry with ProtoNpcId hash keys, and from_bytes/from_json loaders. Drops ECS components and resolve system — games compose their own components on top of the registry, same as bevy_items.
1 parent 99a7260 commit 06f706e

File tree

9 files changed

+1527
-0
lines changed

9 files changed

+1527
-0
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ members = [
2222
'packages/rust/bevy/bevy_cam',
2323
'packages/rust/bevy/bevy_kbve_net',
2424
'packages/rust/bevy/bevy_items',
25+
'packages/rust/bevy/bevy_npc',
2526
]
2627

2728
[profile.dev]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "bevy_npc"
3+
authors = ["kbve", "h0lybyte"]
4+
version = "0.1.0"
5+
edition = "2024"
6+
rust-version = "1.94"
7+
license = "MIT"
8+
description = "Proto-driven NPC definitions for Bevy games — compiles npcdb.proto into typed Rust structs with a searchable registry."
9+
homepage = "https://kbve.com/"
10+
repository = "https://github.com/KBVE/kbve/tree/main/packages/rust/bevy/bevy_npc"
11+
keywords = ["bevy", "npc", "gamedev", "protobuf", "plugin"]
12+
categories = ["game-development", "game-engines"]
13+
14+
[dependencies]
15+
bevy = { version = "0.18", default-features = false, features = [
16+
"bevy_state",
17+
] }
18+
prost = { version = "0.14", features = ["derive"] }
19+
serde = { version = "1", features = ["derive"] }
20+
serde_json = "1"
21+
22+
[build-dependencies]
23+
prost-build = "0.14"
24+
25+
[package.metadata.docs.rs]
26+
all-features = true
27+
rustdoc-args = ["--cfg", "docsrs"]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use std::fs;
2+
use std::path::PathBuf;
3+
4+
fn main() {
5+
if std::env::var("BUILD_PROTO").is_err() {
6+
println!("cargo:warning=Skipping protobuf compilation (BUILD_PROTO not set)");
7+
return;
8+
}
9+
10+
let manifest_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
11+
12+
// Walk up to the workspace root (4 levels: bevy_npc → bevy → rust → packages → root)
13+
let workspace_root = manifest_dir
14+
.ancestors()
15+
.nth(4)
16+
.expect("Cannot find workspace root");
17+
let proto_root = workspace_root.join("packages/data/proto");
18+
19+
assert!(
20+
proto_root.exists(),
21+
"Proto root not found at: {}",
22+
proto_root.display()
23+
);
24+
25+
let npc_proto = proto_root.join("npc/npcdb.proto");
26+
let out_dir = manifest_dir.join("src/proto");
27+
fs::create_dir_all(&out_dir).unwrap();
28+
29+
prost_build::Config::new()
30+
.out_dir(&out_dir)
31+
.type_attribute(".npc", "#[derive(serde::Serialize, serde::Deserialize)]")
32+
.compile_protos(
33+
&[npc_proto.to_str().unwrap()],
34+
&[
35+
proto_root.join("npc").to_str().unwrap(),
36+
proto_root.to_str().unwrap(),
37+
],
38+
)
39+
.expect("Failed to compile npcdb.proto");
40+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! # bevy_npc
2+
//!
3+
//! Proto-driven NPC definitions for Bevy games.
4+
//!
5+
//! This crate compiles `npcdb.proto` into typed Rust structs via `prost` and
6+
//! wraps them in a searchable [`NpcDb`] Bevy resource. It is game-agnostic —
7+
//! any game can load the same proto NPC registry and query it by slug, ULID,
8+
//! type flags, rarity, or creature family.
9+
//!
10+
//! ## Loading from JSON
11+
//!
12+
//! ```rust,ignore
13+
//! use bevy::prelude::*;
14+
//! use bevy_npc::{BevyNpcPlugin, NpcDb};
15+
//!
16+
//! fn load_npcs(mut commands: Commands) {
17+
//! let json = include_str!("path/to/npcdb.json");
18+
//! let db = NpcDb::from_json(json).expect("Failed to parse NPC JSON");
19+
//! commands.insert_resource(db);
20+
//! }
21+
//! ```
22+
//!
23+
//! ## Loading from proto binary
24+
//!
25+
//! ```rust,ignore
26+
//! let bytes = include_bytes!("path/to/npcs.binpb");
27+
//! let db = NpcDb::from_bytes(bytes).expect("Failed to decode NPC registry");
28+
//! ```
29+
30+
mod proto;
31+
mod registry;
32+
33+
// Re-export all proto-generated NPC types
34+
pub use proto::npc::*;
35+
36+
// Re-export registry types
37+
pub use registry::{NpcDb, ProtoNpcId};
38+
39+
use bevy::prelude::*;
40+
41+
/// Bevy plugin that registers the [`NpcDb`] resource.
42+
///
43+
/// The resource is initialized empty. Games should populate it during
44+
/// startup by calling [`NpcDb::from_json`], [`NpcDb::from_bytes`],
45+
/// or [`NpcDb::from_proto`] and inserting it via [`Commands::insert_resource`].
46+
pub struct BevyNpcPlugin;
47+
48+
impl Plugin for BevyNpcPlugin {
49+
fn build(&self, app: &mut App) {
50+
app.init_resource::<NpcDb>();
51+
}
52+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// This file is @generated by prost-build.
2+
/// ULID - 26 character Crockford Base32 encoded string
3+
/// Pattern: \[0-9A-HJKMNP-TV-Z\]{26}
4+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
5+
pub struct Ulid {
6+
#[prost(string, tag = "1")]
7+
pub value: ::prost::alloc::string::String,
8+
}
9+
/// GUID - 32 character hex string (no dashes)
10+
/// Pattern: \[a-f0-9\]{32}
11+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
12+
pub struct Guid {
13+
#[prost(string, tag = "1")]
14+
pub value: ::prost::alloc::string::String,
15+
}
16+
/// UUID - Standard UUID format
17+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
18+
pub struct Uuid {
19+
#[prost(string, tag = "1")]
20+
pub value: ::prost::alloc::string::String,
21+
}
22+
/// 2D Vector (int)
23+
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
24+
pub struct Vec2i {
25+
#[prost(int32, tag = "1")]
26+
pub x: i32,
27+
#[prost(int32, tag = "2")]
28+
pub y: i32,
29+
}
30+
/// 2D Vector (float)
31+
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
32+
pub struct Vec2f {
33+
#[prost(float, tag = "1")]
34+
pub x: f32,
35+
#[prost(float, tag = "2")]
36+
pub y: f32,
37+
}
38+
/// 3D Vector
39+
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
40+
pub struct Vec3 {
41+
#[prost(float, tag = "1")]
42+
pub x: f32,
43+
#[prost(float, tag = "2")]
44+
pub y: f32,
45+
#[prost(float, tag = "3")]
46+
pub z: f32,
47+
}
48+
/// Quaternion rotation
49+
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
50+
pub struct Quat {
51+
#[prost(float, tag = "1")]
52+
pub x: f32,
53+
#[prost(float, tag = "2")]
54+
pub y: f32,
55+
#[prost(float, tag = "3")]
56+
pub z: f32,
57+
#[prost(float, tag = "4")]
58+
pub w: f32,
59+
}
60+
/// Transform (position + rotation + scale)
61+
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
62+
pub struct Transform {
63+
#[prost(message, optional, tag = "1")]
64+
pub position: ::core::option::Option<Vec3>,
65+
#[prost(message, optional, tag = "2")]
66+
pub rotation: ::core::option::Option<Quat>,
67+
#[prost(message, optional, tag = "3")]
68+
pub scale: ::core::option::Option<Vec3>,
69+
}
70+
/// Timestamp wrapper
71+
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
72+
pub struct Timestamp {
73+
#[prost(int64, tag = "1")]
74+
pub seconds: i64,
75+
#[prost(int32, tag = "2")]
76+
pub nanos: i32,
77+
}
78+
/// Generic result wrapper
79+
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
80+
pub struct Result {
81+
#[prost(bool, tag = "1")]
82+
pub success: bool,
83+
#[prost(string, tag = "2")]
84+
pub message: ::prost::alloc::string::String,
85+
#[prost(int32, tag = "3")]
86+
pub code: i32,
87+
}
88+
/// Key-value pair for dynamic properties
89+
#[derive(Clone, PartialEq, ::prost::Message)]
90+
pub struct KeyValue {
91+
#[prost(string, tag = "1")]
92+
pub key: ::prost::alloc::string::String,
93+
#[prost(oneof = "key_value::Value", tags = "2, 3, 4, 5")]
94+
pub value: ::core::option::Option<key_value::Value>,
95+
}
96+
/// Nested message and enum types in `KeyValue`.
97+
pub mod key_value {
98+
#[derive(Clone, PartialEq, ::prost::Oneof)]
99+
pub enum Value {
100+
#[prost(string, tag = "2")]
101+
StringValue(::prost::alloc::string::String),
102+
#[prost(int64, tag = "3")]
103+
IntValue(i64),
104+
#[prost(double, tag = "4")]
105+
FloatValue(f64),
106+
#[prost(bool, tag = "5")]
107+
BoolValue(bool),
108+
}
109+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod npc {
2+
include!("npc.rs");
3+
}

0 commit comments

Comments
 (0)