@@ -1,7 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::Config;
|
||||
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
use bevy::prelude::*;
|
||||
use iroh::protocol::Router;
|
||||
use iroh::Endpoint;
|
||||
use iroh_gossip::api::{GossipReceiver, GossipSender};
|
||||
use iroh_gossip::net::Gossip;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use iroh::{
|
||||
Endpoint,
|
||||
protocol::Router,
|
||||
};
|
||||
use iroh_gossip::{
|
||||
api::{
|
||||
GossipReceiver,
|
||||
GossipSender,
|
||||
},
|
||||
net::Gossip,
|
||||
proto::TopicId,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
/// Message envelope for gossip sync
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SyncMessage {
|
||||
@@ -56,13 +67,9 @@ pub struct GossipTopic(pub TopicId);
|
||||
|
||||
/// Bevy resource for tracking gossip initialization task
|
||||
#[derive(Resource)]
|
||||
pub struct GossipInitTask(pub bevy::tasks::Task<Option<(
|
||||
Endpoint,
|
||||
Gossip,
|
||||
Router,
|
||||
GossipSender,
|
||||
GossipReceiver,
|
||||
)>>);
|
||||
pub struct GossipInitTask(
|
||||
pub bevy::tasks::Task<Option<(Endpoint, Gossip, Router, GossipSender, GossipReceiver)>>,
|
||||
);
|
||||
|
||||
/// Bevy message: a new message that needs to be published to gossip
|
||||
#[derive(Message, Clone, Debug)]
|
||||
@@ -70,7 +77,8 @@ pub struct PublishMessageEvent {
|
||||
pub message: lib::Message,
|
||||
}
|
||||
|
||||
/// Bevy message: a message received from gossip that needs to be saved to SQLite
|
||||
/// Bevy message: a message received from gossip that needs to be saved to
|
||||
/// SQLite
|
||||
#[derive(Message, Clone, Debug)]
|
||||
pub struct GossipMessageReceived {
|
||||
pub sync_message: SyncMessage,
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::{
|
||||
fs,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::{
|
||||
Context,
|
||||
Result,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -45,8 +54,7 @@ impl Config {
|
||||
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
|
||||
let content = fs::read_to_string(path.as_ref())
|
||||
.context(format!("Failed to read config file: {:?}", path.as_ref()))?;
|
||||
let config: Config = toml::from_str(&content)
|
||||
.context("Failed to parse config file")?;
|
||||
let config: Config = toml::from_str(&content).context("Failed to parse config file")?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
@@ -68,15 +76,12 @@ impl Config {
|
||||
hostname: "lonni-daemon".to_string(),
|
||||
state_dir: "./tailscale-state".to_string(),
|
||||
},
|
||||
grpc: GrpcConfig {
|
||||
port: 50051,
|
||||
},
|
||||
grpc: GrpcConfig { port: 50051 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
|
||||
let content = toml::to_string_pretty(self)
|
||||
.context("Failed to serialize config")?;
|
||||
let content = toml::to_string_pretty(self).context("Failed to serialize config")?;
|
||||
fs::write(path.as_ref(), content)
|
||||
.context(format!("Failed to write config file: {:?}", path.as_ref()))?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
use crate::db::schema::{deserialize_embedding, serialize_embedding};
|
||||
use crate::models::*;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use rusqlite::{params, Connection, OptionalExtension, Result, Row};
|
||||
use chrono::{
|
||||
TimeZone,
|
||||
Utc,
|
||||
};
|
||||
use rusqlite::{
|
||||
Connection,
|
||||
OptionalExtension,
|
||||
Result,
|
||||
Row,
|
||||
params,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::schema::{
|
||||
deserialize_embedding,
|
||||
serialize_embedding,
|
||||
},
|
||||
models::*,
|
||||
};
|
||||
|
||||
/// Insert a new message into the database
|
||||
pub fn insert_message(conn: &Connection, msg: &lib::Message) -> Result<i64> {
|
||||
@@ -71,7 +86,10 @@ pub fn insert_message_embedding(
|
||||
}
|
||||
|
||||
/// Get message embedding
|
||||
pub fn get_message_embedding(conn: &Connection, message_id: i64) -> Result<Option<MessageEmbedding>> {
|
||||
pub fn get_message_embedding(
|
||||
conn: &Connection,
|
||||
message_id: i64,
|
||||
) -> Result<Option<MessageEmbedding>> {
|
||||
conn.query_row(
|
||||
"SELECT id, message_id, embedding, model_name, created_at
|
||||
FROM message_embeddings WHERE message_id = ?1",
|
||||
@@ -203,7 +221,7 @@ pub fn list_emotions(
|
||||
) -> Result<Vec<Emotion>> {
|
||||
let mut query = String::from(
|
||||
"SELECT id, message_id, emotion, confidence, model_version, created_at, updated_at
|
||||
FROM emotions WHERE 1=1"
|
||||
FROM emotions WHERE 1=1",
|
||||
);
|
||||
|
||||
if emotion_filter.is_some() {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use rusqlite::{Connection, Result};
|
||||
use rusqlite::{
|
||||
Connection,
|
||||
Result,
|
||||
};
|
||||
use tracing::info;
|
||||
|
||||
pub fn initialize_database(conn: &Connection) -> Result<()> {
|
||||
@@ -9,14 +12,17 @@ pub fn initialize_database(conn: &Connection) -> Result<()> {
|
||||
|
||||
// Try to load the vector extension (non-fatal if it fails for now)
|
||||
match unsafe { conn.load_extension_enable() } {
|
||||
Ok(_) => {
|
||||
| Ok(_) => {
|
||||
match unsafe { conn.load_extension(vec_path, None::<&str>) } {
|
||||
Ok(_) => info!("Loaded sqlite-vec extension"),
|
||||
Err(e) => info!("Could not load sqlite-vec extension: {}. Vector operations will not be available.", e),
|
||||
| Ok(_) => info!("Loaded sqlite-vec extension"),
|
||||
| Err(e) => info!(
|
||||
"Could not load sqlite-vec extension: {}. Vector operations will not be available.",
|
||||
e
|
||||
),
|
||||
}
|
||||
let _ = unsafe { conn.load_extension_disable() };
|
||||
}
|
||||
Err(e) => info!("Extension loading not enabled: {}", e),
|
||||
},
|
||||
| Err(e) => info!("Extension loading not enabled: {}", e),
|
||||
}
|
||||
|
||||
// Create messages table
|
||||
@@ -172,10 +178,7 @@ pub fn initialize_database(conn: &Connection) -> Result<()> {
|
||||
|
||||
/// Helper function to serialize f32 vector to bytes for storage
|
||||
pub fn serialize_embedding(embedding: &[f32]) -> Vec<u8> {
|
||||
embedding
|
||||
.iter()
|
||||
.flat_map(|f| f.to_le_bytes())
|
||||
.collect()
|
||||
embedding.iter().flat_map(|f| f.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
/// Helper function to deserialize bytes back to f32 vector
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use iroh::protocol::Router;
|
||||
use iroh::Endpoint;
|
||||
use iroh_gossip::api::{GossipReceiver, GossipSender};
|
||||
use iroh_gossip::net::Gossip;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use iroh::{
|
||||
Endpoint,
|
||||
protocol::Router,
|
||||
};
|
||||
use iroh_gossip::{
|
||||
api::{
|
||||
GossipReceiver,
|
||||
GossipSender,
|
||||
},
|
||||
net::Gossip,
|
||||
proto::TopicId,
|
||||
};
|
||||
|
||||
/// Initialize Iroh endpoint and gossip for the given topic
|
||||
pub async fn init_iroh_gossip(
|
||||
|
||||
@@ -8,20 +8,24 @@ mod models;
|
||||
mod services;
|
||||
mod systems;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::{
|
||||
Context,
|
||||
Result,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use config::Config;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
// Re-export init function
|
||||
pub use iroh_sync::init_iroh_gossip;
|
||||
|
||||
// Import components and systems
|
||||
use components::*;
|
||||
use config::Config;
|
||||
use iroh_gossip::proto::TopicId;
|
||||
// Re-export init function
|
||||
pub use iroh_sync::init_iroh_gossip;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::Connection;
|
||||
use systems::*;
|
||||
|
||||
fn main() {
|
||||
@@ -29,11 +33,11 @@ fn main() {
|
||||
|
||||
// Load configuration and initialize database
|
||||
let (config, us_db) = match initialize_app() {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
| Ok(data) => data,
|
||||
| Err(e) => {
|
||||
eprintln!("Failed to initialize app: {}", e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Create a topic ID for gossip (use a fixed topic for now)
|
||||
@@ -85,8 +89,7 @@ fn initialize_app() -> Result<(Config, Arc<Mutex<Connection>>)> {
|
||||
|
||||
// Initialize database
|
||||
println!("Initializing database at {}", config.database.path);
|
||||
let conn =
|
||||
Connection::open(&config.database.path).context("Failed to open database")?;
|
||||
let conn = Connection::open(&config.database.path).context("Failed to open database")?;
|
||||
|
||||
db::initialize_database(&conn).context("Failed to initialize database schema")?;
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use chrono::{
|
||||
DateTime,
|
||||
Utc,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
|
||||
/// Represents a message stored in our database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
use crate::db;
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::{
|
||||
Context,
|
||||
Result,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use rusqlite::Connection;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio::time;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use tokio::{
|
||||
sync::{
|
||||
Mutex,
|
||||
mpsc,
|
||||
},
|
||||
time,
|
||||
};
|
||||
use tracing::{
|
||||
debug,
|
||||
error,
|
||||
info,
|
||||
warn,
|
||||
};
|
||||
|
||||
use crate::db;
|
||||
|
||||
pub struct ChatPollerService {
|
||||
chat_db_path: String,
|
||||
@@ -33,12 +50,15 @@ impl ChatPollerService {
|
||||
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
info!("Starting chat poller service");
|
||||
info!("Polling {} every {:?}", self.chat_db_path, self.poll_interval);
|
||||
info!(
|
||||
"Polling {} every {:?}",
|
||||
self.chat_db_path, self.poll_interval
|
||||
);
|
||||
|
||||
// Get last processed rowid from database
|
||||
let us_db = self.us_db.lock().await;
|
||||
let mut last_rowid = db::get_last_processed_rowid(&us_db)
|
||||
.context("Failed to get last processed rowid")?;
|
||||
let mut last_rowid =
|
||||
db::get_last_processed_rowid(&us_db).context("Failed to get last processed rowid")?;
|
||||
drop(us_db);
|
||||
|
||||
info!("Starting from rowid: {}", last_rowid);
|
||||
@@ -49,7 +69,7 @@ impl ChatPollerService {
|
||||
interval.tick().await;
|
||||
|
||||
match self.poll_messages(last_rowid).await {
|
||||
Ok(new_messages) => {
|
||||
| Ok(new_messages) => {
|
||||
if !new_messages.is_empty() {
|
||||
info!("Found {} new messages", new_messages.len());
|
||||
|
||||
@@ -74,10 +94,10 @@ impl ChatPollerService {
|
||||
} else {
|
||||
debug!("No new messages");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
},
|
||||
| Err(e) => {
|
||||
error!("Error polling messages: {}", e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,12 +105,14 @@ impl ChatPollerService {
|
||||
async fn poll_messages(&self, last_rowid: i64) -> Result<Vec<lib::Message>> {
|
||||
// Check if chat.db exists
|
||||
if !Path::new(&self.chat_db_path).exists() {
|
||||
return Err(anyhow::anyhow!("chat.db not found at {}", self.chat_db_path));
|
||||
return Err(anyhow::anyhow!(
|
||||
"chat.db not found at {}",
|
||||
self.chat_db_path
|
||||
));
|
||||
}
|
||||
|
||||
// Open chat.db (read-only)
|
||||
let chat_db = lib::ChatDb::open(&self.chat_db_path)
|
||||
.context("Failed to open chat.db")?;
|
||||
let chat_db = lib::ChatDb::open(&self.chat_db_path).context("Failed to open chat.db")?;
|
||||
|
||||
// Get messages with rowid > last_rowid
|
||||
// We'll use the existing get_our_messages but need to filter by rowid
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
use crate::db;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::{error, info, warn};
|
||||
use tokio::sync::{
|
||||
Mutex,
|
||||
mpsc,
|
||||
};
|
||||
use tracing::{
|
||||
error,
|
||||
info,
|
||||
warn,
|
||||
};
|
||||
|
||||
use crate::db;
|
||||
|
||||
/// Service responsible for generating embeddings for messages and words
|
||||
pub struct EmbeddingService {
|
||||
@@ -47,11 +56,11 @@ impl EmbeddingService {
|
||||
// Get message ID from our database
|
||||
let us_db = self.us_db.lock().await;
|
||||
let message_id = match db::get_message_id_by_chat_rowid(&us_db, msg.rowid)? {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
| Some(id) => id,
|
||||
| None => {
|
||||
warn!("Message {} not found in database, skipping", msg.rowid);
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Check if embedding already exists
|
||||
@@ -61,8 +70,8 @@ impl EmbeddingService {
|
||||
|
||||
// Skip if message has no text
|
||||
let text = match &msg.text {
|
||||
Some(t) if !t.is_empty() => t,
|
||||
_ => return Ok(()),
|
||||
| Some(t) if !t.is_empty() => t,
|
||||
| _ => return Ok(()),
|
||||
};
|
||||
|
||||
drop(us_db);
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
use crate::db;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use rusqlite::Connection;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tracing::{error, info, warn};
|
||||
use tokio::sync::{
|
||||
Mutex,
|
||||
mpsc,
|
||||
};
|
||||
use tracing::{
|
||||
error,
|
||||
info,
|
||||
warn,
|
||||
};
|
||||
|
||||
use crate::db;
|
||||
|
||||
/// Service responsible for classifying emotions in messages
|
||||
pub struct EmotionService {
|
||||
@@ -56,11 +65,11 @@ impl EmotionService {
|
||||
// Get message ID from our database
|
||||
let us_db = self.us_db.lock().await;
|
||||
let message_id = match db::get_message_id_by_chat_rowid(&us_db, msg.rowid)? {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
| Some(id) => id,
|
||||
| None => {
|
||||
warn!("Message {} not found in database, skipping", msg.rowid);
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Check if emotion classification already exists
|
||||
@@ -70,8 +79,8 @@ impl EmotionService {
|
||||
|
||||
// Skip if message has no text
|
||||
let text = match &msg.text {
|
||||
Some(t) if !t.is_empty() => t,
|
||||
_ => return Ok(()),
|
||||
| Some(t) if !t.is_empty() => t,
|
||||
| _ => return Ok(()),
|
||||
};
|
||||
|
||||
drop(us_db);
|
||||
@@ -82,7 +91,13 @@ impl EmotionService {
|
||||
|
||||
// Store emotion classification
|
||||
let us_db = self.us_db.lock().await;
|
||||
db::insert_emotion(&us_db, message_id, &emotion, confidence, &self.model_version)?;
|
||||
db::insert_emotion(
|
||||
&us_db,
|
||||
message_id,
|
||||
&emotion,
|
||||
confidence,
|
||||
&self.model_version,
|
||||
)?;
|
||||
|
||||
// Randomly add to training set based on sample rate
|
||||
if rand::random::<f64>() < self.training_sample_rate {
|
||||
|
||||
@@ -4,4 +4,4 @@ pub mod emotion_service;
|
||||
|
||||
pub use chat_poller::ChatPollerService;
|
||||
pub use embedding_service::EmbeddingService;
|
||||
pub use emotion_service::EmotionService;
|
||||
pub use emotion_service::EmotionService;
|
||||
|
||||
@@ -3,10 +3,7 @@ use bevy::prelude::*;
|
||||
use crate::components::*;
|
||||
|
||||
/// System: Poll chat.db for new messages using Bevy's task system
|
||||
pub fn poll_chat_db(
|
||||
_config: Res<AppConfig>,
|
||||
_db: Res<Database>,
|
||||
) {
|
||||
pub fn poll_chat_db(_config: Res<AppConfig>, _db: Res<Database>) {
|
||||
// TODO: Use Bevy's AsyncComputeTaskPool to poll chat.db
|
||||
// This will replace the tokio::spawn chat poller
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::components::*;
|
||||
|
||||
/// System: Poll the gossip init task and insert resources when complete
|
||||
pub fn poll_gossip_init(
|
||||
mut commands: Commands,
|
||||
mut init_task: Option<ResMut<GossipInitTask>>,
|
||||
) {
|
||||
pub fn poll_gossip_init(mut commands: Commands, mut init_task: Option<ResMut<GossipInitTask>>) {
|
||||
if let Some(mut task) = init_task {
|
||||
// Check if the task is finished (non-blocking)
|
||||
if let Some(result) = bevy::tasks::block_on(bevy::tasks::futures_lite::future::poll_once(&mut task.0)) {
|
||||
if let Some(result) =
|
||||
bevy::tasks::block_on(bevy::tasks::futures_lite::future::poll_once(&mut task.0))
|
||||
{
|
||||
if let Some((endpoint, gossip, router, sender, receiver)) = result {
|
||||
println!("Inserting gossip resources");
|
||||
|
||||
@@ -72,17 +72,19 @@ pub fn publish_to_gossip(
|
||||
|
||||
// Serialize the message
|
||||
match serialize_sync_message(&sync_message) {
|
||||
Ok(bytes) => {
|
||||
| Ok(bytes) => {
|
||||
// TODO: Publish to gossip
|
||||
// For now, just log that we would publish
|
||||
println!("Would publish {} bytes to gossip", bytes.len());
|
||||
|
||||
// Note: Direct async broadcasting from Bevy systems is tricky due to Sync requirements
|
||||
// We'll need to use a different approach, possibly with channels or a dedicated task
|
||||
}
|
||||
Err(e) => {
|
||||
// Note: Direct async broadcasting from Bevy systems is tricky
|
||||
// due to Sync requirements We'll need to use a
|
||||
// different approach, possibly with channels or a dedicated
|
||||
// task
|
||||
},
|
||||
| Err(e) => {
|
||||
eprintln!("Failed to serialize sync message: {}", e);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,19 +100,18 @@ pub fn receive_from_gossip(
|
||||
}
|
||||
|
||||
// TODO: Implement proper async message reception
|
||||
// This will require spawning a long-running task that listens for gossip events
|
||||
// and sends them as Bevy messages. For now, this is a placeholder.
|
||||
// This will require spawning a long-running task that listens for gossip
|
||||
// events and sends them as Bevy messages. For now, this is a
|
||||
// placeholder.
|
||||
}
|
||||
|
||||
/// System: Save received gossip messages to SQLite
|
||||
pub fn save_gossip_messages(
|
||||
mut events: MessageReader<GossipMessageReceived>,
|
||||
_db: Res<Database>,
|
||||
) {
|
||||
pub fn save_gossip_messages(mut events: MessageReader<GossipMessageReceived>, _db: Res<Database>) {
|
||||
for event in events.read() {
|
||||
println!("Received message {} from gossip (published by {})",
|
||||
event.sync_message.message.rowid,
|
||||
event.sync_message.publisher_node_id);
|
||||
println!(
|
||||
"Received message {} from gossip (published by {})",
|
||||
event.sync_message.message.rowid, event.sync_message.publisher_node_id
|
||||
);
|
||||
// TODO: Save to SQLite if we don't already have it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy::tasks::AsyncComputeTaskPool;
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
tasks::AsyncComputeTaskPool,
|
||||
};
|
||||
|
||||
use crate::components::*;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user