Bunch of refactoring and renaming to support built-in commands.
This commit is contained in:
parent
001e15e594
commit
82001b99b7
|
@ -6,7 +6,7 @@ use super::prompts::{execution_prompts, parsing_prompts, world_prompts};
|
|||
|
||||
use crate::kobold_api::Client as KoboldClient;
|
||||
use crate::models::coherence::{CoherenceFailure, SceneFix};
|
||||
use crate::models::commands::{Command, Commands, RawCommandExecution, VerbsResponse};
|
||||
use crate::models::commands::{ParsedCommand, ParsedCommands, RawCommandExecution, VerbsResponse};
|
||||
use crate::models::world::raw::{
|
||||
ExitSeed, ItemDetails, ItemSeed, PersonDetails, PersonSeed, SceneSeed,
|
||||
};
|
||||
|
@ -56,7 +56,7 @@ impl AiGenerator {
|
|||
self.person_creation_convo.reset();
|
||||
}
|
||||
|
||||
pub async fn parse(&self, cmd: &str) -> Result<Commands> {
|
||||
pub async fn parse(&self, cmd: &str) -> Result<ParsedCommands> {
|
||||
// If convo so far is empty, add the instruction header,
|
||||
// otherwise only append to existing convo.
|
||||
let prompt = match self.parsing_convo.is_empty() {
|
||||
|
@ -64,7 +64,7 @@ impl AiGenerator {
|
|||
false => parsing_prompts::continuation_prompt(&cmd),
|
||||
};
|
||||
|
||||
let mut cmds: Commands = self.parsing_convo.execute(&prompt).await?;
|
||||
let mut cmds: ParsedCommands = self.parsing_convo.execute(&prompt).await?;
|
||||
let verbs = self.find_verbs(cmd).await?;
|
||||
self.check_coherence(&verbs, &mut cmds).await?;
|
||||
Ok(cmds)
|
||||
|
@ -83,13 +83,13 @@ impl AiGenerator {
|
|||
.collect())
|
||||
}
|
||||
|
||||
async fn check_coherence(&self, verbs: &[String], commands: &mut Commands) -> Result<()> {
|
||||
async fn check_coherence(&self, verbs: &[String], commands: &mut ParsedCommands) -> Result<()> {
|
||||
// let coherence_prompt = parsing_prompts::coherence_prompt();
|
||||
// let mut commands: Commands = self.parsing_convo.execute(&coherence_prompt).await?;
|
||||
|
||||
// Non-LLM coherence checks: remove empty commands, remove
|
||||
// non-verbs, etc.
|
||||
let filtered_commands: Vec<Command> = commands
|
||||
let filtered_commands: Vec<ParsedCommand> = commands
|
||||
.clone()
|
||||
.commands
|
||||
.into_iter()
|
||||
|
@ -102,7 +102,7 @@ impl AiGenerator {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn execute_raw(&self, stage: &Stage, cmd: &Command) -> Result<RawCommandExecution> {
|
||||
pub async fn execute_raw(&self, stage: &Stage, cmd: &ParsedCommand) -> Result<RawCommandExecution> {
|
||||
let prompt = execution_prompts::execution_prompt(stage, &cmd);
|
||||
let raw_exec: RawCommandExecution = self.execution_convo.execute(&prompt).await?;
|
||||
Ok(raw_exec)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::db::Database;
|
||||
use crate::kobold_api::Client as KoboldClient;
|
||||
use crate::models::commands::{
|
||||
CommandExecution, Commands, ExecutionConversionResult, RawCommandExecution,
|
||||
AiCommand, ParsedCommands, ExecutionConversionResult, RawCommandExecution,
|
||||
};
|
||||
use crate::models::world::items::{Category, Item, Rarity};
|
||||
use crate::models::world::people::{Gender, Person, Sex};
|
||||
|
@ -40,23 +40,23 @@ impl AiLogic {
|
|||
}
|
||||
|
||||
pub async fn execute(
|
||||
&mut self,
|
||||
&self,
|
||||
stage: &Stage,
|
||||
cmd: &str,
|
||||
) -> Result<(Commands, CommandExecution)> {
|
||||
) -> Result<(ParsedCommands, RawCommandExecution)> {
|
||||
let parsed_cmd = self.generator.parse(cmd).await?;
|
||||
let execution = self.execute_parsed(stage, &parsed_cmd).await?;
|
||||
Ok((parsed_cmd, execution))
|
||||
}
|
||||
|
||||
pub async fn execute_parsed(
|
||||
&mut self,
|
||||
&self,
|
||||
stage: &Stage,
|
||||
parsed_cmd: &Commands,
|
||||
) -> Result<CommandExecution> {
|
||||
parsed_cmd: &ParsedCommands,
|
||||
) -> Result<RawCommandExecution> {
|
||||
//TODO handle multiple commands in list
|
||||
if parsed_cmd.commands.is_empty() {
|
||||
return Ok(CommandExecution::empty());
|
||||
return Ok(RawCommandExecution::empty());
|
||||
}
|
||||
|
||||
let cmd = &parsed_cmd.commands[0];
|
||||
|
@ -67,21 +67,12 @@ impl AiLogic {
|
|||
// Set aside anything with correct event, but wrong parameters.
|
||||
// Ask LLM to fix them, if possible
|
||||
//TODO make a aiclient::fix_execution
|
||||
let converted = command_converter::convert_raw_execution(raw_exec, &self.db).await;
|
||||
|
||||
self.generator.reset_commands();
|
||||
|
||||
//TODO handle the errored events aside from yeeting them out
|
||||
match converted {
|
||||
ExecutionConversionResult::Success(execution) => Ok(execution),
|
||||
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution),
|
||||
ExecutionConversionResult::Failure(failures) => {
|
||||
bail!("unhandled command execution failure: {:?}", failures)
|
||||
}
|
||||
}
|
||||
Ok(raw_exec)
|
||||
}
|
||||
|
||||
pub async fn create_person(&mut self, scene: &SceneSeed, seed: &PersonSeed) -> Result<Person> {
|
||||
pub async fn create_person(&self, scene: &SceneSeed, seed: &PersonSeed) -> Result<Person> {
|
||||
self.generator.reset_person_creation();
|
||||
let details = self.generator.create_person_details(scene, seed).await?;
|
||||
|
||||
|
@ -123,7 +114,7 @@ impl AiLogic {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn create_item(&mut self, scene: &SceneSeed, seed: &ItemSeed) -> Result<Item> {
|
||||
pub async fn create_item(&self, scene: &SceneSeed, seed: &ItemSeed) -> Result<Item> {
|
||||
let details = self.generator.create_item_details(scene, seed).await?;
|
||||
|
||||
// TODO these have to be sent to the AI
|
||||
|
@ -143,7 +134,7 @@ impl AiLogic {
|
|||
}
|
||||
|
||||
pub async fn create_scene_with_id(
|
||||
&mut self,
|
||||
&self,
|
||||
scene_type: &str,
|
||||
fantasticalness: &str,
|
||||
scene_id: &str,
|
||||
|
@ -156,7 +147,7 @@ impl AiLogic {
|
|||
}
|
||||
|
||||
pub async fn create_scene_from_stub(
|
||||
&mut self,
|
||||
&self,
|
||||
stub: SceneStub,
|
||||
connected_scene: &Scene,
|
||||
) -> Result<ContentContainer> {
|
||||
|
@ -181,7 +172,7 @@ impl AiLogic {
|
|||
}
|
||||
|
||||
pub async fn create_scene(
|
||||
&mut self,
|
||||
&self,
|
||||
scene_type: &str,
|
||||
fantasticalness: &str,
|
||||
) -> Result<ContentContainer> {
|
||||
|
@ -200,7 +191,7 @@ impl AiLogic {
|
|||
}
|
||||
|
||||
async fn fill_in_scene_from_stub(
|
||||
&mut self,
|
||||
&self,
|
||||
seed: SceneSeed,
|
||||
stub: SceneStub,
|
||||
) -> Result<ContentContainer> {
|
||||
|
@ -212,7 +203,7 @@ impl AiLogic {
|
|||
Ok(content)
|
||||
}
|
||||
|
||||
async fn fill_in_scene(&mut self, mut scene_seed: SceneSeed) -> Result<ContentContainer> {
|
||||
async fn fill_in_scene(&self, mut scene_seed: SceneSeed) -> Result<ContentContainer> {
|
||||
let mut content_in_scene = vec![];
|
||||
|
||||
// People in scene
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::ai::convo::AiPrompt;
|
||||
use crate::models::commands::{Command, CommandEvent, EventConversionFailures};
|
||||
use crate::models::commands::{ParsedCommand, CommandEvent, EventConversionFailures};
|
||||
use crate::models::world::scenes::{Scene, Stage};
|
||||
use strum::VariantNames;
|
||||
|
||||
|
@ -194,7 +194,7 @@ fn stage_info(stage: &Stage) -> String {
|
|||
info
|
||||
}
|
||||
|
||||
pub fn execution_prompt(stage: &Stage, cmd: &Command) -> AiPrompt {
|
||||
pub fn execution_prompt(stage: &Stage, cmd: &ParsedCommand) -> AiPrompt {
|
||||
let scene_info = stage_info(&stage);
|
||||
|
||||
let prompt = COMMAND_EXECUTION_PROMPT
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
use crate::models::commands::{BuiltinCommand, AiCommand};
|
||||
use crate::models::world::scenes::Stage;
|
||||
|
||||
pub fn check_builtin_command(stage: &Stage, cmd: &str) -> Option<BuiltinCommand> {
|
||||
match cmd {
|
||||
"look" => look_command(stage),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn look_command(_stage: &Stage) -> Option<BuiltinCommand> {
|
||||
Some(BuiltinCommand::Look)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
db::Database,
|
||||
models::commands::{
|
||||
CommandEvent, CommandExecution, EventCoherenceFailure, EventConversionError,
|
||||
CommandEvent, AiCommand, EventCoherenceFailure, EventConversionError,
|
||||
EventConversionFailures, ExecutionConversionResult, RawCommandEvent, RawCommandExecution,
|
||||
},
|
||||
};
|
||||
|
@ -42,8 +42,8 @@ struct Narrative {
|
|||
narration: String,
|
||||
}
|
||||
|
||||
fn from_raw_success(raw: Narrative, events: Vec<CommandEvent>) -> CommandExecution {
|
||||
CommandExecution {
|
||||
fn from_raw_success(raw: Narrative, events: Vec<CommandEvent>) -> AiCommand {
|
||||
AiCommand {
|
||||
events,
|
||||
valid: raw.valid,
|
||||
reason: match &raw.reason {
|
||||
|
@ -62,7 +62,7 @@ pub async fn convert_raw_execution(
|
|||
db: &Database,
|
||||
) -> ExecutionConversionResult {
|
||||
if !raw_exec.valid {
|
||||
return ExecutionConversionResult::Success(CommandExecution::from_raw_invalid(raw_exec));
|
||||
return ExecutionConversionResult::Success(AiCommand::from_raw_invalid(raw_exec));
|
||||
}
|
||||
|
||||
let narrative = Narrative {
|
||||
|
|
|
@ -1,2 +1,62 @@
|
|||
pub mod converter;
|
||||
use crate::{
|
||||
ai::logic::AiLogic,
|
||||
db::Database,
|
||||
models::{
|
||||
commands::{
|
||||
BuiltinCommand, AiCommand, ExecutionConversionResult, RawCommandExecution, CommandExecution,
|
||||
},
|
||||
world::scenes::Stage,
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub mod builtins;
|
||||
pub mod converter;
|
||||
|
||||
pub struct CommandExecutor {
|
||||
logic: Rc<AiLogic>,
|
||||
db: Rc<Database>,
|
||||
}
|
||||
|
||||
impl<'a> CommandExecutor {
|
||||
pub fn new(logic: Rc<AiLogic>, db: Rc<Database>) -> CommandExecutor {
|
||||
CommandExecutor { logic, db }
|
||||
}
|
||||
|
||||
pub async fn execute(&self, stage: &Stage, cmd: &str) -> Result<CommandExecution> {
|
||||
CommandExecution::AiCommand(AiCommand::empty());
|
||||
|
||||
if let Some(builtin) = builtins::check_builtin_command(stage, cmd) {
|
||||
return Ok(CommandExecution::Builtin(builtin));
|
||||
}
|
||||
|
||||
let cached_command = self.db.load_cached_command(cmd, &stage.scene).await?;
|
||||
|
||||
let raw_exec: RawCommandExecution = if let Some(cached) = cached_command {
|
||||
self.logic.execute_parsed(stage, &cached.commands).await?
|
||||
} else {
|
||||
let (cmds_to_cache, execution) = self.logic.execute(stage, cmd).await?;
|
||||
|
||||
self.db
|
||||
.cache_command(cmd, &stage.scene, &cmds_to_cache)
|
||||
.await?;
|
||||
|
||||
execution
|
||||
};
|
||||
|
||||
let converted = converter::convert_raw_execution(raw_exec, &self.db).await;
|
||||
|
||||
//TODO handle the errored events aside from getting rid of them
|
||||
let execution: AiCommand = match converted {
|
||||
ExecutionConversionResult::Success(execution) => Ok(execution),
|
||||
ExecutionConversionResult::PartialSuccess(execution, _) => Ok(execution),
|
||||
ExecutionConversionResult::Failure(failures) => Err(anyhow!(
|
||||
"unhandled command execution failure: {:?}",
|
||||
failures
|
||||
)),
|
||||
}?;
|
||||
|
||||
Ok(CommandExecution::AiCommand(execution))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::models::commands::{CachedCommand, Command, Commands};
|
||||
use crate::models::commands::{CachedParsedCommand, ParsedCommand, ParsedCommands};
|
||||
use crate::models::world::scenes::{Scene, Stage, StageOrStub};
|
||||
use crate::models::{Content, ContentContainer, Insertable};
|
||||
use anyhow::Result;
|
||||
|
@ -388,10 +388,10 @@ impl Database {
|
|||
&self,
|
||||
raw_cmd: &str,
|
||||
scene: &Scene,
|
||||
parsed_cmds: &Commands,
|
||||
parsed_cmds: &ParsedCommands,
|
||||
) -> Result<()> {
|
||||
let collection = self.collection(CMD_COLLECTION).await?;
|
||||
let doc = CachedCommand {
|
||||
let doc = CachedParsedCommand {
|
||||
raw: raw_cmd.to_string(),
|
||||
scene_key: scene._key.as_ref().cloned().expect("scene is missing key"),
|
||||
commands: parsed_cmds.clone(),
|
||||
|
@ -405,7 +405,7 @@ impl Database {
|
|||
&self,
|
||||
raw_cmd: &str,
|
||||
scene: &Scene,
|
||||
) -> Result<Option<CachedCommand>> {
|
||||
) -> Result<Option<CachedParsedCommand>> {
|
||||
let scene_key = scene._key.as_deref();
|
||||
let aql = AqlQuery::builder()
|
||||
.query(queries::LOAD_CACHED_COMMAND)
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use crate::db::Database;
|
||||
use crate::io::display;
|
||||
use crate::models::commands::CommandExecution;
|
||||
use crate::models::commands::{BuiltinCommand, AiCommand, CommandExecution};
|
||||
use crate::state::GameState;
|
||||
use crate::{commands::CommandExecutor, db::Database};
|
||||
use anyhow::Result;
|
||||
use reedline::{DefaultPrompt, Reedline, Signal};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct GameLoop {
|
||||
executor: CommandExecutor,
|
||||
state: GameState,
|
||||
db: Rc<Database>,
|
||||
editor: Reedline,
|
||||
|
@ -14,16 +15,21 @@ pub struct GameLoop {
|
|||
}
|
||||
|
||||
impl GameLoop {
|
||||
pub fn new(state: GameState, db: Rc<Database>) -> GameLoop {
|
||||
pub fn new(state: GameState, db: &Rc<Database>) -> GameLoop {
|
||||
let executor_db = db.clone();
|
||||
let loop_db = db.clone();
|
||||
let executor_logic = state.logic.clone();
|
||||
|
||||
GameLoop {
|
||||
state,
|
||||
db,
|
||||
db: loop_db,
|
||||
executor: CommandExecutor::new(executor_logic, executor_db),
|
||||
editor: Reedline::create(),
|
||||
prompt: DefaultPrompt::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> {
|
||||
async fn handle_ai_command(&mut self, execution: AiCommand) -> Result<()> {
|
||||
if !execution.valid {
|
||||
display!(
|
||||
"You can't do that: {}",
|
||||
|
@ -42,31 +48,24 @@ impl GameLoop {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_command(&mut self, cmd: &str) -> Result<CommandExecution> {
|
||||
let stage = &self.state.current_scene;
|
||||
let cached_command = self.db.load_cached_command(cmd, &stage.scene).await?;
|
||||
async fn handle_builtin(&mut self, builtin: BuiltinCommand) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let execution = if let Some(cached) = cached_command {
|
||||
self.state
|
||||
.logic
|
||||
.execute_parsed(stage, &cached.commands)
|
||||
.await?
|
||||
} else {
|
||||
let (cmds_to_cache, execution) = self.state.logic.execute(stage, cmd).await?;
|
||||
|
||||
self.db
|
||||
.cache_command(cmd, &stage.scene, &cmds_to_cache)
|
||||
.await?;
|
||||
|
||||
execution
|
||||
async fn handle_execution(&mut self, execution: CommandExecution) -> Result<()> {
|
||||
match execution {
|
||||
CommandExecution::Builtin(builtin) => self.handle_builtin(builtin).await?,
|
||||
CommandExecution::AiCommand(exec) => self.handle_ai_command(exec).await?,
|
||||
};
|
||||
|
||||
Ok(execution)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_input(&mut self, cmd: &str) -> Result<()> {
|
||||
if !cmd.is_empty() {
|
||||
let execution = self.execute_command(cmd).await?;
|
||||
//let execution = self.execute_command(cmd).await?;
|
||||
let mut stage = &self.state.current_scene;
|
||||
let execution = self.executor.execute(&mut stage, cmd).await?;
|
||||
self.handle_execution(execution).await?;
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ async fn main() -> Result<()> {
|
|||
base_client,
|
||||
));
|
||||
let db = Rc::new(Database::new(conn, "test_world").await?);
|
||||
let logic = AiLogic::new(client, &db);
|
||||
let logic = Rc::new(AiLogic::new(client, &db));
|
||||
|
||||
let mut state = GameState {
|
||||
logic,
|
||||
|
@ -114,7 +114,7 @@ async fn main() -> Result<()> {
|
|||
|
||||
load_root_scene(&db, &mut state).await?;
|
||||
|
||||
let mut game_loop = GameLoop::new(state, db.clone());
|
||||
let mut game_loop = GameLoop::new(state, &db);
|
||||
game_loop.run_loop().await?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -2,22 +2,24 @@ use serde::{Deserialize, Serialize};
|
|||
use strum::{EnumString, EnumVariantNames};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::world::scenes::Stage;
|
||||
|
||||
/// Stored in the database to bypass AI 'parsing' when possible.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CachedCommand {
|
||||
pub struct CachedParsedCommand {
|
||||
pub raw: String,
|
||||
pub scene_key: String,
|
||||
pub commands: Commands,
|
||||
pub commands: ParsedCommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Commands {
|
||||
pub commands: Vec<Command>,
|
||||
pub struct ParsedCommands {
|
||||
pub commands: Vec<ParsedCommand>,
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Command {
|
||||
pub struct ParsedCommand {
|
||||
pub verb: String,
|
||||
pub target: String,
|
||||
pub location: String,
|
||||
|
@ -49,6 +51,17 @@ pub struct RawCommandExecution {
|
|||
pub events: Vec<RawCommandEvent>,
|
||||
}
|
||||
|
||||
impl RawCommandExecution {
|
||||
pub fn empty() -> RawCommandExecution {
|
||||
RawCommandExecution {
|
||||
valid: true,
|
||||
reason: None,
|
||||
narration: "".to_string(),
|
||||
events: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RawCommandEvent {
|
||||
|
@ -87,17 +100,34 @@ pub enum CommandEvent {
|
|||
},
|
||||
}
|
||||
|
||||
/// A builtin command has more immediate access to necessary
|
||||
/// information, so we can be a bit more loose with what we give it. A
|
||||
/// builtin command is only created directly via checking for builtin
|
||||
/// commands. These commands may have little or no parameters, as they
|
||||
/// are meant for simple, direct commands like looking, movement, etc.
|
||||
pub enum BuiltinCommand {
|
||||
Look,
|
||||
}
|
||||
|
||||
pub enum CommandExecution {
|
||||
Builtin(BuiltinCommand),
|
||||
AiCommand(AiCommand),
|
||||
}
|
||||
|
||||
/// An "AI Command" is a command execution generated by the LLM and
|
||||
/// run through coherence validation/fixing, and (assuming it is
|
||||
/// valid) contains a series of events to apply to the game state.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct CommandExecution {
|
||||
pub struct AiCommand {
|
||||
pub valid: bool,
|
||||
pub reason: Option<String>,
|
||||
pub narration: String,
|
||||
pub events: Vec<CommandEvent>,
|
||||
}
|
||||
|
||||
impl CommandExecution {
|
||||
pub fn empty() -> CommandExecution {
|
||||
CommandExecution {
|
||||
impl AiCommand {
|
||||
pub fn empty() -> AiCommand {
|
||||
AiCommand {
|
||||
valid: true,
|
||||
reason: None,
|
||||
narration: "".to_string(),
|
||||
|
@ -105,8 +135,8 @@ impl CommandExecution {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_raw_invalid(raw: RawCommandExecution) -> CommandExecution {
|
||||
CommandExecution {
|
||||
pub fn from_raw_invalid(raw: RawCommandExecution) -> AiCommand {
|
||||
AiCommand {
|
||||
valid: raw.valid,
|
||||
reason: raw.reason,
|
||||
narration: "".to_string(),
|
||||
|
@ -117,8 +147,8 @@ impl CommandExecution {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ExecutionConversionResult {
|
||||
Success(CommandExecution),
|
||||
PartialSuccess(CommandExecution, EventConversionFailures),
|
||||
Success(AiCommand),
|
||||
PartialSuccess(AiCommand, EventConversionFailures),
|
||||
Failure(EventConversionFailures),
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use std::rc::Rc;
|
|||
|
||||
pub struct GameState {
|
||||
pub start_prompt: String,
|
||||
pub logic: AiLogic,
|
||||
pub logic: Rc<AiLogic>,
|
||||
pub db: Rc<Database>,
|
||||
pub current_scene: Stage,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue