diff --git a/Cargo.lock b/Cargo.lock index a264541..f27fa22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1881,14 +1881,16 @@ dependencies = [ name = "skybook-parser" version = "0.0.0" dependencies = [ + "derive_more", "teleparse 0.0.5", + "thiserror 2.0.9", ] [[package]] name = "skybook-runtime" version = "0.0.0" dependencies = [ - "skybook-acorn", + "blueflame", "skybook-parser", ] diff --git a/packages/blueflame/src/boot.rs b/packages/blueflame/src/boot.rs index cdef610..a584658 100644 --- a/packages/blueflame/src/boot.rs +++ b/packages/blueflame/src/boot.rs @@ -3,8 +3,23 @@ use std::sync::Arc; use uking_relocate_lib::singleton::{ObjType, Singleton, SingletonCreator}; use uking_relocate_lib::{Env, Program}; +use crate::error::Error as CrateError; use crate::memory::{align_down, align_up, Memory, MemoryFlags, Proxies, Region, RegionType, SimpleHeap, PAGE_SIZE, REGION_ALIGN}; use crate::processor::Processor; +use crate::Core; + +/// Error that only happens during boot +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("no PMDM singleton found in program image")] + NoPmdm, + #[error("PMDM address is impossible to satisfy: 0x{0:08x}")] + InvalidPmdmAddress(u64), + #[error("heap is too small: need at least {0} bytes")] + HeapTooSmall(u32), + #[error("region overlap: {0} and {1}")] + RegionOverlap(RegionType, RegionType), +} /// Initialize memory for the process @@ -15,44 +30,67 @@ pub fn init_memory( stack_start: u64, stack_size: u32, pmdm_address: u64, - heap_free_size: u32, -) -> (Arc, Proxies) { - + heap_size: u32, +) -> Result<(Memory, Proxies), CrateError> { - let pmdm_info = image.singleton_by_id(Singleton::PauseMenuDataMgr).unwrap(); // TODO: error - // type - // heap_start + rel_start = pmdm_address - // heap_start = pmdm_address - rel_start - let pmdm_rel_start = pmdm_info.rel_start; - if pmdm_rel_start as u64 > pmdm_address { - panic!("pmdm_rel_start > pmdm_address"); // TODO: handle error + // calculate heap start address + // we need the heap to be as small as possible, + // but the relative address of the singleton could be really big + // (e.g. a few GBs), so we need to adjust the heap start accordingly + // + // 0 heap_start s1 pmdm s2 + // |<--heap_adjustment-->| + // |<----pmdm_rel_address--->| + // |<----------pmdm.rel_start--------------------->| + // |<----------min_rel_start------->| + // |<---------------------------max_rel_start--------------------->| + // |<------------------------pmdm_address------------------>| + // || + // + // for any singleton: + // rel_start - heap_adjustment + heap_start = address + // + // heap_adjustment is positive and guarateed to be less than rel_start + // of any singleton + let pmdm = image.singleton_by_id(Singleton::PauseMenuDataMgr).ok_or(Error::NoPmdm)?; + if pmdm.rel_start as u64 > pmdm_address { + return Err(Error::InvalidPmdmAddress(pmdm_address).into()); } - let heap_start = align_down!(pmdm_address - pmdm_rel_start as u64, REGION_ALIGN); - let heap_adjustment = (pmdm_address - heap_start) - pmdm_rel_start as u64; - - // heap_start + heap_adjustment + singleton.rel_start = address for that singleton + let min_heap_start = pmdm_address - pmdm.rel_start as u64; + let min_rel_start = image.singletons().iter().map(|s| s.rel_start).min().unwrap_or_default(); + let max_heap_start = min_heap_start + min_rel_start as u64; + let heap_start = align_down!(max_heap_start, REGION_ALIGN); + if heap_start < min_heap_start { + // somehow align down made it smaller + // maybe possible with some pmdm_address + return Err(Error::InvalidPmdmAddress(pmdm_address).into()); + } + let heap_adjustment = heap_start - min_heap_start; // calculate how much space will be needed for all the singletons - let mut heap_end = heap_start; - let singletons = image.singletons(); - for singleton in singletons { - let singleton_end = heap_start + heap_adjustment + singleton.rel_start as u64 + singleton.size as u64; - heap_end = heap_end.max(singleton_end); - } + let max_rel_start = image.singletons().iter().map(|s| s.rel_start).max().unwrap_or_default(); + let heap_end = min_heap_start + max_rel_start as u64; // align up to the next page, and reserve 1 page for some spacing - heap_end = align_up!(heap_end, PAGE_SIZE as u64) + PAGE_SIZE as u64; - let heap_size = (heap_end - heap_start) as u32 + heap_free_size; - let heap_start_alloc = heap_end + 0x428; // make it look random + let heap_singletons_end = align_up!(heap_end, PAGE_SIZE as u64) + PAGE_SIZE as u64; + let heap_singletons_size = (heap_singletons_end - heap_start) as u32; + // make the first alloc look random + let page_off_alloc_start = 0x428; + let heap_min_size = heap_singletons_size + page_off_alloc_start; + let heap_size = align_up!(heap_size, PAGE_SIZE); + + if heap_size < heap_min_size { + return Err(Error::HeapTooSmall(heap_min_size).into()); + } - // check if program/stack/heap overlap (TODO: remove the panics) + // check the regions don't overlap before allocating memory if overlaps(image.program_start, image.program_size, stack_start, stack_size) { - panic!("program and stack overlap"); + return Err(Error::RegionOverlap(RegionType::Program, RegionType::Stack).into()); } if overlaps(image.program_start, image.program_size, heap_start, heap_size) { - panic!("program and heap overlap"); + return Err(Error::RegionOverlap(RegionType::Program, RegionType::Heap).into()); } if overlaps(stack_start, stack_size, heap_start, heap_size) { - panic!("stack and heap overlap"); + return Err(Error::RegionOverlap(RegionType::Stack, RegionType::Heap).into()); } // construct the memory @@ -64,7 +102,8 @@ pub fn init_memory( image.regions()).unwrap()); // TODO: error type // let stack_region = Arc::new(Region::new_rw(RegionType::Stack, stack_start, stack_size)); - let heap_region = Arc::new(SimpleHeap::new(heap_start, heap_size, heap_start_alloc)); + let heap_region = Arc::new(SimpleHeap::new( + heap_start, heap_size, heap_min_size as u64 + heap_start)); let flags = MemoryFlags { enable_strict_region: true, @@ -74,25 +113,23 @@ pub fn init_memory( let mut memory = Memory::new(flags, program_region, stack_region, heap_region); - // create the processor to initialize the singletons + // create a temporary processor to initialize the singletons let mut processor = Processor::default(); - let proxies = Proxies::default(); + let mut proxies = Proxies::default(); let mut singleton_init = SingletonInit { env: image.env, program_start: image.program_start, - processor: &mut processor, - memory: &mut memory, - proxies: &proxies, - heap_start_adjusted: heap_start + heap_adjustment, + core: processor.attach(&mut memory, &mut proxies), + heap_start_adjusted: heap_start - heap_adjustment, }; - for singleton in singletons { - singleton.create_instance(&mut singleton_init).unwrap(); // TODO: error + for singleton in image.singletons() { + singleton.create_instance(&mut singleton_init)?; } - (Arc::new(memory), proxies) + Ok((memory, proxies)) } fn overlaps(a_start: u64, a_size: u32, b_start: u64, b_size: u32) -> bool { @@ -104,14 +141,12 @@ fn overlaps(a_start: u64, a_size: u32, b_start: u64, b_size: u32) -> bool { pub struct SingletonInit<'p, 'm, 'x> { env: Env, program_start: u64, - processor: &'p mut Processor, - memory: &'m mut Memory, - proxies: &'x Proxies, + core: Core<'p, 'm, 'x>, heap_start_adjusted: u64, } impl SingletonCreator for SingletonInit<'_, '_, '_> { - type Error = (); // TODO: error type from executing code + type Error = CrateError; fn set_main_rel_pc(&mut self, pc: u32) -> Result<(), Self::Error> { let main_offset = self.env.main_offset(); diff --git a/packages/blueflame/src/error.rs b/packages/blueflame/src/error.rs index 0109488..8f8d71a 100644 --- a/packages/blueflame/src/error.rs +++ b/packages/blueflame/src/error.rs @@ -1,11 +1,12 @@ -use crate::memory; +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { -pub enum Crash { - - Boot, // TODO: Boot crash (?) + #[error("boot crash: {0}")] + Boot(#[from] crate::boot::Error), + #[error("execution error")] Cpu, // TODO: CPU errors - /// Memory error - Mem(memory::error::Error), + #[error("memory error: {0}")] + Mem(crate::memory::Error), } diff --git a/packages/blueflame/src/lib.rs b/packages/blueflame/src/lib.rs index 525ae3a..53bede0 100644 --- a/packages/blueflame/src/lib.rs +++ b/packages/blueflame/src/lib.rs @@ -4,11 +4,11 @@ use processor::Processor; pub struct Core<'p, 'm, 'x> { pub cpu: &'p mut Processor, pub mem: &'m mut Memory, - pub proxies: &'x Proxies, + pub proxies: &'x mut Proxies, } /// Internal bindings to invoke functions -trait CoreInternal { +impl Core<'_, '_, '_> { // these functions are called internally by the call // to execute commands @@ -18,19 +18,24 @@ trait CoreInternal { // 0x96efb8 - fn pmdm_item_get(&self, actor: &str, value: i32, modifier_info: u64) -> - Result<(), ()>; + pub fn pmdm_item_get(&self, actor: &str, value: i32, modifier_info: u64) -> + Result<(), error::Error> { + todo!(); + } } /// Memory implementation -mod memory; +pub mod memory; -mod error; +pub mod error; mod loader; -mod processor; +pub mod processor; /// Initialization for the memory mod boot; + +/// Proxy objects +mod proxy; diff --git a/packages/blueflame/src/memory/access.rs b/packages/blueflame/src/memory/access.rs index 1e0323b..b7ef89d 100644 --- a/packages/blueflame/src/memory/access.rs +++ b/packages/blueflame/src/memory/access.rs @@ -2,7 +2,7 @@ use enumset::{enum_set, EnumSet, EnumSetType}; /// Information for accessing memory for tracking and reporting -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct MemAccess { /// The type of access pub typ: AccessType, diff --git a/packages/blueflame/src/memory/error.rs b/packages/blueflame/src/memory/error.rs index 7c59e41..c908829 100644 --- a/packages/blueflame/src/memory/error.rs +++ b/packages/blueflame/src/memory/error.rs @@ -4,7 +4,7 @@ use super::region::RegionType; use super::access::MemAccess; /// Memory errors -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Clone, thiserror::Error)] pub enum Error { #[error("permission denied: {0}")] PermissionDenied(MemAccess), diff --git a/packages/blueflame/src/memory/mod.rs b/packages/blueflame/src/memory/mod.rs index 909ff67..573542e 100644 --- a/packages/blueflame/src/memory/mod.rs +++ b/packages/blueflame/src/memory/mod.rs @@ -1,14 +1,13 @@ mod page; pub use page::*; -pub mod error; +mod error; +pub use error::*; mod heap; pub use heap::*; mod region; pub use region::*; mod access; pub use access::*; -mod program; -pub use program::*; mod read; pub use read::*; mod write; diff --git a/packages/blueflame/src/memory/program.rs b/packages/blueflame/src/memory/program.rs deleted file mode 100644 index 69c0050..0000000 --- a/packages/blueflame/src/memory/program.rs +++ /dev/null @@ -1,8 +0,0 @@ - -/// The region where the program segments are loaded into memory -/// -/// This region is aligned to 0x10000, and contains the .text (RX) -/// .rodata (R) and .data (RW) segments of all modules loaded (rtld, -/// main, subsdk, sdk, in this order). -pub struct ProgramMemory { -} diff --git a/packages/blueflame/src/memory/proxy.rs b/packages/blueflame/src/memory/proxy.rs index 42383dc..bed4b86 100644 --- a/packages/blueflame/src/memory/proxy.rs +++ b/packages/blueflame/src/memory/proxy.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use rand_xoshiro::rand_core::{RngCore, SeedableRng}; use rand_xoshiro::Xoshiro256PlusPlus; @@ -6,72 +6,32 @@ use sha2::{Digest, Sha256}; use super::error::Error; use super::memory::Memory; -use super::RegionType; +use super::region::RegionType; /// The maximum number of proxy objects per type pub const MAX_OBJECTS: u32 = 1024000; /// Holds all proxy objects in memory -#[derive(Default)] +#[derive(Default, Clone)] pub struct Proxies { // just as a placeholder - string_proxies: Arc>>, -} - -impl Clone for Proxies { - fn clone(&self) -> Self { - Self { - string_proxies: self.string_proxies.clone(), - } - } + string_proxies: Arc>, } impl Proxies { /// (EXAMPLE) operate on a string proxy at the given address - pub fn with_string R>(&self, mem: &Memory, address: u64, f: F) -> Result { - Self::with_read(&self.string_proxies, mem, address, f) + pub fn get_string(&self, mem: &Memory, address: u64) -> Result<&String, Error> { + self.string_proxies.get_at_addr(mem, address) } /// (EXAMPLE) operate on a string proxy at the given address for mutation - pub fn with_string_mut R>(&self, mem: &mut Memory, address: u64, f: F) -> Result { - Self::with_write(&self.string_proxies, mem, address, f) + pub fn mut_string<'s>(&'s mut self, mem: &mut Memory, address: u64) -> Result<&'s mut String, Error> { + Arc::make_mut(&mut self.string_proxies).mut_at_addr(mem, address) } /// (EXAMPLE) allocate a string proxy in memory and returns a pointer to it - pub fn allocate_string(&self, mem: &mut Memory, s: String) -> Result { - Self::allocate(&self.string_proxies, mem, s) - } - - #[inline] - fn with_read R>( - list: &Arc>>, mem: &Memory, address: u64, f:F) -> Result { - let list = match list.read() { - Ok(l) => l, - Err(_) => return Err(Error::Unexpected("failed to lock proxy list, for reading".to_string())), - }; - let t = list.get_at_addr(mem, address)?; - Ok(f(t)) - } - - #[inline] - fn with_write R>( - list: &Arc>>, mem: &mut Memory, address: u64, f:F) -> Result { - let mut list = match list.write() { - Ok(l) => l, - Err(_) => return Err(Error::Unexpected("failed to lock proxy list for writing".to_string())), - }; - let t = list.mut_at_addr(mem, address)?; - Ok(f(t)) - } - - #[inline] - fn allocate( - list: &Arc>>, mem: &mut Memory, t: T) -> Result { - let mut list = match list.write() { - Ok(l) => l, - Err(_) => return Err(Error::Unexpected("failed to lock proxy list for writing".to_string())), - }; - list.allocate(mem, t) + pub fn allocate_string(&mut self, mem: &mut Memory, s: String) -> Result { + Arc::make_mut(&mut self.string_proxies).allocate(mem, s) } } @@ -82,10 +42,10 @@ impl ProxyObject for String { } } - +#[derive(Clone)] pub struct ProxyList { rng: Xoshiro256PlusPlus, - objects: Vec>, + objects: Vec>>, } impl Default for ProxyList { @@ -98,13 +58,16 @@ impl Default for ProxyList { } } -pub struct Entry { +#[derive(Clone)] +struct Entry { + /// The proxy object (clone on write) obj: T, + /// The hash of the object data in memory, initialized as random integrity: [u8; 32], } impl ProxyList { - /// Allocate the proxy object in memory and return the address + /// Allocate a new proxy object in memory and return its address pub fn allocate(&mut self, mem: &mut Memory, t: T) -> Result { // allocate the proxy object in memory let pointer = mem.heap_mut().alloc(t.mem_size())?; @@ -124,18 +87,19 @@ impl ProxyList { if self.objects.len() >= MAX_OBJECTS as usize { return Err(Error::ProxyOutOfMemory); } - let mut entry = Entry { - obj: t, - integrity: [0; 32], - }; + let mut integrity = [0; 32]; let handle = self.objects.len() as u32; - self.write_proxy_object(mem, pointer, handle, &entry.obj, &mut entry.integrity)?; - self.objects.push(entry); + Self::write_proxy_object(&mut self.rng, mem, pointer, handle, &t, &mut integrity)?; + // creating entry here to help elide copying + self.objects.push(Arc::new(Entry { + obj: t, + integrity + })); Ok(handle) } /// Write a proxy object to memory - fn write_proxy_object(&mut self, mem: &mut Memory, pointer: u64, handle: u32, t: &T, hash_out: &mut [u8; 32]) -> Result<(), Error> { + fn write_proxy_object(rng: &mut Xoshiro256PlusPlus, mem: &mut Memory, pointer: u64, handle: u32, t: &T, hash_out: &mut [u8; 32]) -> Result<(), Error> { let size = t.mem_size(); if size < 4 { return Err(Error::InvalidProxyObjectSize(size)); @@ -149,13 +113,13 @@ impl ProxyList { let garbage_size = size - 4; let chunks = garbage_size / 8; for _ in 0..chunks { - let n = self.rng.next_u64(); + let n = rng.next_u64(); w.write_u64(n)?; hash.update(&n.to_le_bytes()); } let remaining = garbage_size % 8; if remaining > 0 { - let n = self.rng.next_u64(); + let n = rng.next_u64(); let bytes = n.to_le_bytes(); for i in 0..remaining { w.write_u8(bytes[i as usize])?; @@ -168,22 +132,39 @@ impl ProxyList { /// Get the object at the given address in memory as a proxy object pub fn get_at_addr(&self, mem: &Memory, address: u64) -> Result<&T, Error> { - let e = self.get_entry(mem, address)?; + let handle = self.get_checked_handle(mem, address)?; + let e = &self.objects[handle as usize]; Ok(&e.obj) } /// Get the object at the given address in memory as a proxy object - /// for mutation. The object will be cloned, and the memory will be - /// updated with the new handle - pub fn mut_at_addr(&mut self, mem: &mut Memory, pointer: u64) -> Result<&mut T, Error> { - let e = self.get_entry(mem, pointer)?; - let cloned = e.obj.clone(); - let handle = self.create_entry(mem, pointer, cloned)?; - Ok(&mut self.objects[handle as usize].obj) + /// for mutation. + /// + /// The proxy object is currently shared, it will be cloned, + /// and the proxy will receive a new integrity hash. However, + /// no cloning or updating will occur if the object is not shared. + pub fn mut_at_addr<'s>(&'s mut self, mem: &mut Memory, pointer: u64) -> Result<&'s mut T, Error> { + let handle = self.get_checked_handle(mem, pointer)?; + // get mut object, clone on write + // use pointer equality to check if it's cloned + // note we cannot make multiple make_mut or get_mut calls, + // because it's possible the object is changed in between + let ptr_old = Arc::as_ptr(&self.objects[handle as usize]) as usize; + let entry = Arc::make_mut(&mut self.objects[handle as usize]); + let copied = (std::ptr::from_ref(entry) as usize) != ptr_old; + + // update the object in memory to a fresh copy + if copied { + Self::write_proxy_object(&mut self.rng, mem, pointer, handle, &entry.obj, &mut entry.integrity)?; + } + Ok(&mut entry.obj) } - /// Get a proxy object from memory - fn get_entry(&self, mem: &Memory, pointer: u64) -> Result<&Entry, Error> { + /// Read the object at the given address and check its integrity. + /// If OK, return the handle + /// + /// The entry is not returned to avoid borrowing + fn get_checked_handle(&self, mem: &Memory, pointer: u64) -> Result { let mut hash = Sha256::new(); // read the handle let mut r = mem.read(pointer, Some(RegionType::Heap.into()), false)?; @@ -204,13 +185,13 @@ impl ProxyList { return Err(Error::CorruptedProxyObject(handle, pointer, size)); } - Ok(&entry) + Ok(handle) } } -pub trait ProxyObject: Clone { +pub trait ProxyObject: Clone + Send + Sync { /// Get the size of the object to mock in memory /// The size must be at least 4 bytes fn mem_size(&self) -> u32; diff --git a/packages/blueflame/src/processor.rs b/packages/blueflame/src/processor/mod.rs similarity index 91% rename from packages/blueflame/src/processor.rs rename to packages/blueflame/src/processor/mod.rs index 9cc611e..fbe241b 100644 --- a/packages/blueflame/src/processor.rs +++ b/packages/blueflame/src/processor/mod.rs @@ -1,3 +1,4 @@ + use std::collections::HashMap; use crate::{memory::{Memory, Proxies}, Core}; @@ -15,7 +16,7 @@ impl Default for Processor { impl Processor { /// Attach the processor to a memory instance - pub fn attach<'p, 'm, 'x>(&'p mut self, mem: &'m mut Memory, proxies: &'x Proxies) -> Core<'p, 'm, 'x> { + pub fn attach<'p, 'm, 'x>(&'p mut self, mem: &'m mut Memory, proxies: &'x mut Proxies) -> Core<'p, 'm, 'x> { Core { cpu: self, mem, diff --git a/packages/blueflame/src/proxy/mod.rs b/packages/blueflame/src/proxy/mod.rs new file mode 100644 index 0000000..348344e --- /dev/null +++ b/packages/blueflame/src/proxy/mod.rs @@ -0,0 +1,4 @@ + +/// TODO: TriggerParam +pub struct GdtTriggerParam { +} diff --git a/packages/parser/Cargo.toml b/packages/parser/Cargo.toml index 58a9934..8fed9e1 100644 --- a/packages/parser/Cargo.toml +++ b/packages/parser/Cargo.toml @@ -5,4 +5,6 @@ edition = "2021" publish = false [dependencies] +derive_more = { version = "1.0.0", features = ["deref", "deref_mut"] } teleparse = "0.0.5" +thiserror = "2.0.9" diff --git a/packages/parser/src/cir/item_meta.rs b/packages/parser/src/cir/item_meta.rs new file mode 100644 index 0000000..4968fb8 --- /dev/null +++ b/packages/parser/src/cir/item_meta.rs @@ -0,0 +1,324 @@ +use teleparse::{tp, Span}; + +use crate::error::{ErrorReport, Error}; +use crate::item_search::ItemResolver; +use crate::syn; +use crate::cir; + +use super::MetaParser; + +/// Item metadata used to select or specify item +#[derive(Debug, Default)] +pub struct ItemMeta { + /// The value of the item + /// + /// settable by: + /// - `life=100` -> 100 + /// - `value=100` -> 100 + /// - `durability=1` -> 100 + pub value: Option, + + /// If the item is equipped + /// + /// settable by `equip`, `equipped` + pub equip: Option, + + /// Settable by key `life_recover, hp, modpower` + pub life_recover: Option, + /// Settable by `time` + pub effect_duration: Option, + /// Settable by `price` (set), `modifier` (add) + pub sell_price: Option, + /// Settable by `effect` name + pub effect_id: Option, + /// Settable by `level` + pub effect_level: Option, + + /// Settable by `ingr` + pub ingredients: Vec, +} + +impl ItemMeta { + pub async fn parse(meta: &syn::ItemMeta, resolver: &R, errors: &mut Vec) -> ItemMeta { + let parser = Parser { + meta: ItemMeta::default(), + resolver, + }; + cir::parse_meta(meta, parser, errors).await + } +} + +struct Parser<'r, R: ItemResolver> { + meta: ItemMeta, + resolver: &'r R, +} + +impl MetaParser for Parser<'_, R> { + type Output = ItemMeta; + + async fn visit_start(&mut self, _meta: &syn::ItemMeta, _errors: &mut Vec) { + } + + async fn visit_entry(&mut self, span: Span, key: &tp::String, value: &tp::Option, errors: &mut Vec) { + let key_str = key.to_ascii_lowercase(); + match key_str.trim() { + "life" | "value" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + self.meta.value = Some(x as i32); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "durability" | "dura" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + self.meta.value = Some((x * 100) as i32); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "equip" | "equipped"=> { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Bool(x)) => { + self.meta.equip = Some(x); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "life_recover" | "hp" | "modpower" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + self.meta.life_recover = Some(x as i32); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "time" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + self.meta.effect_duration = Some(x as i32); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "price" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + self.meta.sell_price = Some(x as i32); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "modifier" | "modtype" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + // integer => same as price + self.meta.sell_price = Some(x as i32); + } + Ok(cir::MetaValue::String(x)) => { + // string modifier, parse it and add it + match parse_weapon_modifier_bits(&x) { + Some(m) => self.meta.sell_price = Some(self.meta.sell_price.unwrap_or_default() | m), + None => { + errors.push( + Error::InvalidWeaponModifier(x) + .spanned(value) + ); + } + } + } + Ok(mv) => { + errors.push( + Error::InvalidWeaponModifier(mv.to_string()) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "effect" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + // integer => set it without checking + self.meta.effect_id = Some(x as i32); + } + Ok(cir::MetaValue::String(x)) => { + // string modifier, parse it + match parse_cook_effect(&x) { + Some(m) => self.meta.effect_id = Some(m), + None => { + errors.push( + Error::InvalidCookEffect(x) + .spanned(value) + ); + } + } + } + Ok(mv) => { + errors.push( + Error::InvalidWeaponModifier(mv.to_string()) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "level" => { + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::Int(x)) => { + self.meta.effect_level = Some(x as f32); + } + Ok(cir::MetaValue::Float(x)) => { + self.meta.effect_level = Some(x as f32); + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + }, + "ingr" => { + if self.meta.ingredients.len() >= 5 { + errors.push( + Error::TooManyIngredients.spanned(value) + ); + return; + } + match cir::MetaValue::parse_option(value.as_ref()) { + Ok(cir::MetaValue::String(x)) => { + // currently we only support looking up by english + match self.resolver.resolve(&x).await { + Some(item) => { + self.meta.ingredients.push(item); + } + None => { + errors.push( + Error::InvalidItem(x) + .spanned(value) + ); + } + } + } + Ok(mv) => { + errors.push( + Error::InvalidMetaValue(key_str, mv) + .spanned(value) + ); + } + Err(e) => { + errors.push(e); + } + } + } + _ => { + errors.push( + Error::UnusedMetaKey(key_str).spanned_warning(&span) + ); + } + } + } + + async fn visit_end(&mut self, meta: &syn::ItemMeta, errors: &mut Vec) { + todo!() + } + + async fn finish(self) -> Self::Output { + todo!() + } +} + +fn parse_weapon_modifier_bits(value: &str) -> Option { + let value = value.replace("_", "").replace("-", "").replace(" ", "").to_ascii_lowercase(); + match value.trim() { + "attack" | "attackup" | "addpower" => Some(0x1), + "addpowerplus" => Some(0x80000001u32 as i32), + "durability" | "durabilityup" | "addlife" => Some(0x2), + "addlifeplus" => Some(0x80000002u32 as i32), + "critical" | "criticalhit" => Some(0x4), + "longthrow" | "throw" => Some(0x8), + "multishot" | "spreadfire" => Some(0x10), + "zoom" => Some(0x20), + "quickshot" | "rapidfire" => Some(0x40), + "surfmaster" | "surf" | "shieldsurf" | "shieldsurfup" | "surfup" => Some(0x80), + "guard" | "guardup" | "addguard" => Some(0x100), + "addguardplus" => Some(0x80000100u32 as i32), + "plus" | "yellow" => Some(0x80000000u32 as i32), + _ => None, + } +} + +fn parse_cook_effect(value: &str) -> Option { + let value = value.replace("_", "").replace("-", "").replace(" ", "").to_ascii_lowercase(); + match value.trim() { + "hearty" | "lifemaxup" => Some(2), + "chilly" | "chill" | "resisthot"=> Some(4), + "spicy" | "resistcold" => Some(5), + "electro" | "resistelectric" => Some(6), + "mighty" | "attack" | "attackup" => Some(10), + "tough" | "defense" | "defenseup" => Some(11), + "sneaky" | "quiet" | "stealth" | "stealthup" | "quietness" => Some(12), + "speed" | "speedup" | "allspeed" | "movingspeed" => Some(13), + "energizing" | "stamina" | "staminaup" | "stam" | "stamup" | "gutsrecover" | "guts" => Some(14), + "enduring" | "endura" | "endur" | "exgutsmaxup" | "exguts" => Some(15), + "fire" | "fireproof" | "resistflame" | "resistfire" => Some(16), + _ => None, + } +} diff --git a/packages/parser/src/cir/mod.rs b/packages/parser/src/cir/mod.rs new file mode 100644 index 0000000..a8655a2 --- /dev/null +++ b/packages/parser/src/cir/mod.rs @@ -0,0 +1,118 @@ +use teleparse::{tp, Span, ToSpan}; + +use crate::error::Error; +use crate::syn; +use crate::{error::ErrorReport}; + +mod item_meta; + + +pub trait MetaParser { + type Output; + + async fn visit_start(&mut self, meta: &syn::ItemMeta, errors: &mut Vec); + async fn visit_entry(&mut self, span: Span, key: &tp::String, value: &tp::Option, errors: &mut Vec); + async fn visit_end(&mut self, meta: &syn::ItemMeta, errors: &mut Vec); + async fn finish(self) -> Self::Output; +} + +pub async fn parse_meta(meta: &syn::ItemMeta, mut parser: T, errors: &mut Vec) -> T::Output { + parser.visit_start(meta, errors).await; + let span = meta.span(); + for entry in &meta.entries { + parser.visit_entry(span, &entry.key, &entry.value, errors).await; + } + parser.visit_end(meta, errors).await; + parser.finish().await +} + +#[derive(Debug, Clone)] +pub enum MetaValue { + Bool(bool), + Int(i64), + Float(f64), + String(String), +} + +impl std::fmt::Display for MetaValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MetaValue::Bool(b) => write!(f, "{}", b), + MetaValue::Int(i) => write!(f, "{}", i), + MetaValue::Float(fl) => write!(f, "{}", fl), + MetaValue::String(s) => write!(f, "{}", s), + } + } +} + +impl MetaValue { + pub fn parse_option(value: Option<&syn::ItemMetaValue>) -> Result { + match value { + Some(v) => Ok(Self::parse(&v.value)?), + None => Ok(Self::Bool(true)), + } + } + pub fn parse(value: &syn::MetaValueLiteral) -> Result { + match value { + syn::MetaValueLiteral::Word(x) => { + let s = x.trim(); + match s { + "true" => Ok(Self::Bool(true)), + "false" => Ok(Self::Bool(false)), + _ => Ok(Self::String(s.to_string())), + } + } + syn::MetaValueLiteral::Number(x) => { + let int_part: &str = &*x.int_part; + let int_part = match int_part.strip_prefix("0x") { + Some(rest) => i64::from_str_radix(rest, 16) + .map_err(|_| { + Error::IntFormat(x.int_part.to_string()).spanned(x) + })?, + + None => int_part.parse() + .map_err(|_| { + Error::IntFormat(x.int_part.to_string()).spanned(x) + })? + }; + let float_part = match &*x.float_part { + Some(fp) => fp, + None => return Ok(Self::Int(int_part)), + }; + let decimal_part = match &*float_part.1 { + Some(dp) => dp, + // Integer followed by dot, like 3. + None => return Ok(Self::Float(int_part as f64)), + }; + let decimal_str: &str = &*decimal_part; + let decimal_num = match decimal_part.strip_prefix("0x") { + Some(_) => { + // float part can't be hex + return Err( + Error::FloatFormat(format!("{}.{}", int_part, decimal_str)).spanned(x) + ) + } + + None => decimal_part.parse::() + .map_err(|_| { + Error::FloatFormat(format!("{}.{}", int_part, decimal_str)).spanned(x) + })? + }; + // float part can't be negative + if decimal_num < 0 { + return Err( + Error::FloatFormat(format!("{}.{}", int_part, decimal_str)).spanned(x) + ) + } + let full_str = format!("{}.{}", int_part, decimal_str); + let value = full_str.parse::() + .map_err(|_| { + Error::FloatFormat(format!("{}.{}", int_part, decimal_str)).spanned(x) + })?; + return Ok(Self::Float(value)); + } + } + + } +} + diff --git a/packages/parser/src/error.rs b/packages/parser/src/error.rs new file mode 100644 index 0000000..de906a0 --- /dev/null +++ b/packages/parser/src/error.rs @@ -0,0 +1,53 @@ +use teleparse::ToSpan; + +use crate::cir; + + +pub struct ErrorReport { + pub span: (usize, usize), + pub is_warning: bool, + pub error: Error, +} + +impl ErrorReport { + pub fn spanned(t: &T, error: Error) -> Self { + let span = t.span(); + Self { + span: (span.lo, span.hi), + is_warning: false, + error, + } + } +} + +#[derive(Debug, Clone, thiserror::Error)] +pub enum Error { + #[error("failed to resolve item: {0}")] + InvalidItem(String), + #[error("invalid integer format: {0}")] + IntFormat(String), + #[error("invalid number format: {0}")] + FloatFormat(String), + #[error("unused meta key: {0}")] + UnusedMetaKey(String), + #[error("key `{0}` has invalid value: {0}")] + InvalidMetaValue(String, cir::MetaValue), + #[error("invalid weapon modifier: {0}")] + InvalidWeaponModifier(String), + #[error("invalid cook effect: {0}")] + InvalidCookEffect(String), + #[error("item has too many ingredients (max 5)")] + TooManyIngredients, +} + +impl Error { + pub fn spanned(self, t: &T) -> ErrorReport { + ErrorReport::spanned(t, self) + } + + pub fn spanned_warning(self, t: &T) -> ErrorReport { + let mut report = ErrorReport::spanned(t, self); + report.is_warning = true; + report + } +} diff --git a/packages/parser/src/item_search.rs b/packages/parser/src/item_search.rs new file mode 100644 index 0000000..1ee1285 --- /dev/null +++ b/packages/parser/src/item_search.rs @@ -0,0 +1,12 @@ + +pub trait ItemResolver { + type Future: std::future::Future + Send + 'static; + + /// Resolve an item to its actor name + fn resolve(&self, word: &str) -> Self::Future>; + + /// Resolve a quote item word "like this" to its actor name + fn resolve_quoted(&self, word: &str) -> Self::Future>; + + +} diff --git a/packages/parser/src/lib.rs b/packages/parser/src/lib.rs index daa2d8e..9dbaee7 100644 --- a/packages/parser/src/lib.rs +++ b/packages/parser/src/lib.rs @@ -1,241 +1,12 @@ -use teleparse::{derive_lexicon, derive_syntax, tp}; -pub fn test_message(n: u64) -> String { - format!("Hello from Rust! You passed in {}", n) -} - -#[derive_lexicon] -#[teleparse(ignore(r"\s+", r"//.*\n", r"#.*\n"))] -pub enum TT { - #[teleparse(terminal( - SymLAngle = "<", - SymRAngle = ">", - SymLParen = "(", - SymRParen = ")", - SymLBracket = "[", - SymRBracket = "]", - SymLBrace = "{", - SymRBrace = "}", - SymEqual = "=", - SymColon = ":", - SymComma = ",", - SymSemi = ";", - SymQuote = "\"", - ))] - Symbol, - - #[teleparse(regex(r"(\d(_?\d)*)|(0x[\da-fA-F](_?[\da-fA-F])*)"), terminal(Number))] - Number, - - // #[teleparse(terminal( - // CmdInit = "init", - // CmdInitGdt = "init-gdt", - // - // CmdGet = "get", - // CmdPickUp = "pick-up", - // CmdBuy = "buy", - // CmdCook = "cook", - // - // CmdEat = "eat", - // CmdSell = "sell", - // CmdEatAll = "eat-all", // tyupe - // CmdSellAll = "sell-all", //type - // CmdDropAll = "drop-all", //type - // - // CmdHold = "hold", - // CmdUnhold = "unhold", - // CmdHoldSmuggle = "hold-smuggle", - // CmdHoldAttach = "hold-attach", - // CmdDrop = "drop", - // CmdPutAside = "put-aside", - // CmdDnp = "dnp", - // - // CmdRoast = "roast", - // CmdBoil = "boil", - // CmdFreeze = "freeze", - // - // CmdEquip = "equip", - // CmdUnequip = "unequip", // type - // CmdUnequipThe = "unequip-the", // item - // CmdShoot = "shoot-arrow", - // - // CmdSort = "sort", // type - // // - // CmdSave = "save", - // CmdSaveAs = "save-as", - // CmdReload = "reload", - // - // CmdCloseGame = "close-game", - // CmdNewGame = "new-game", - // KwFrom = "from", - // KwSlot = "slot", - // ))] - - #[teleparse(regex(r"[_a-zA-Z][-0-9a-zA-Z_]*"), terminal(Word))] - Word, - - #[teleparse(regex(r#""[^"]*""#), terminal(QuotedWord))] - QuotedWord, - - Variable, - Name, - Type, - - Amount, - - Keyword, - Command, -} - -#[derive_syntax] -#[teleparse(root)] -#[derive(Debug)] -pub struct CommandInit { - #[teleparse(literal("init"), semantic(Command))] - pub cmd: Word, - pub items: AddItemList, -} - -/// List of item for adding to or setting the inventory -#[derive_syntax] -#[derive(Debug)] -pub enum AddItemList { - /// Single item, e.g. `apple` - Single(Item), - /// multiple items with amounts, e.g. `5 apples, 3 royal_claymore` - List(tp::Vec), -} - -#[derive_syntax] -#[derive(Debug)] -pub struct NumberedItem { - #[teleparse(semantic(Amount))] - pub num: Number, - pub item: Item, -} - -#[derive_syntax] -#[derive(Debug)] -pub struct NumberedOrAllItem { - pub num: NumOrAll, - pub item: Item, -} - -#[derive_syntax] -#[derive(Debug)] -pub struct NumberedOrInfiniteItem { - pub num: NumOrInfinite, - pub item: Item, -} - -/// Specify an item and metadata -#[derive_syntax] -#[derive(Debug)] -pub struct Item { - #[teleparse(semantic(Name))] - pub name: ItemName, - pub meta: tp::Option, -} - -/// Specify the name of the item -#[derive_syntax] -#[derive(Debug)] -pub enum ItemName { - /// Using `-` separated word to search item by English name - Word(Word), - /// Use quoted value to search by name in any language - Quoted(QuotedWord), - /// Use angle brackets to use the literal as the actor name - /// e.g. `` - Angle(ItemNameAngle), -} - -/// A word surrounded by angle brackets, e.g. `` -#[derive_syntax] -#[derive(Debug)] -pub struct ItemNameAngle { - /// The opening angle bracket - pub open: SymLAngle, - /// The word inside the angle brackets - pub name: Word, - /// The closing angle bracket - pub close: SymRAngle, -} - -/// Metadata specifier for an item, e.g. `[key1:value1, key2=value2]` -#[derive_syntax] -#[derive(Debug)] -pub struct ItemMeta { - pub open: SymLBracket, - pub entries: tp::Punct, - pub close: SymRBracket, -} - -/// A key-value pair in an item's metadata specifier -#[derive_syntax] -#[derive(Debug)] -pub struct ItemMetaKeyValue { - /// The key of the key-value pair - #[teleparse(semantic(Variable))] - pub key: Word, - pub value: tp::Option -} +/// Command syntax +mod syn; +/// Command intermediate representation +mod cir; -/// Value after the key in an item's metadata specifier -#[derive_syntax] -#[derive(Debug)] -pub struct ItemMetaValue { - /// The seperator between the key and value - pub sep: MetaSep, - /// The value of the key-value pair - pub value: WordOrNum, -} - -/// A word or a number -#[derive_syntax] -#[derive(Debug)] -pub enum WordOrNum { - Word(Word), - Number(Number), -} +mod error; +mod item_search; -/// A number or the string "all" -#[derive_syntax] -#[derive(Debug)] -pub enum NumOrAll { - All(AmtAll), - Number(Number), -} - -/// A number or the string "infinite" -#[derive_syntax] -#[derive(Debug)] -pub enum NumOrInfinite { - Infinite(AmtInfinite), - Number(Number), -} - -/// The keyword "all" in the context of a number to specify all items -#[derive_syntax] -#[derive(Debug)] -pub struct AmtAll { - #[teleparse(literal("all"), semantic(Amount))] - pub all: Word, -} - -/// The keyword "infinite" in the context of a number to specify infinite items -#[derive_syntax] -#[derive(Debug)] -pub struct AmtInfinite { - #[teleparse(literal("infinite"), semantic(Amount))] - pub infinite: Word, -} - -/// Seperator for item meta key-value pairs (`:` or `=`) -#[derive_syntax] -#[derive(Debug)] -pub enum MetaSep { - Colon(SymColon), - Equal(SymEqual), +pub fn test_message(n: u64) -> String { + format!("Hello from Rust! You passed in {}", n) } - diff --git a/packages/parser/src/syn/category.rs b/packages/parser/src/syn/category.rs new file mode 100644 index 0000000..6322560 --- /dev/null +++ b/packages/parser/src/syn/category.rs @@ -0,0 +1,72 @@ +use teleparse::derive_syntax; + +use super::token::{KwArmor, KwArmors, KwBow, KwBows, KwFood, KwFoods, KwKeyItem, KwKeyItems, KwMaterial, KwMaterials, KwShield, KwShields, KwWeapon, KwWeapons}; + +/// Category specifier +#[derive_syntax] +#[derive(Debug)] +pub enum Category { + Weapon(CatWeapon), + Bow(CatBow), + Shield(CatShield), + Armor(CatArmor), + Material(CatMaterial), + Food(CatFood), + KeyItem(CatKeyItem), +} + +/// The "weapon" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatWeapon { + Singular(KwWeapon), + Plural(KwWeapons), +} + +/// The "bow" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatBow { + Singular(KwBow), + Plural(KwBows), +} + +/// The "shield" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatShield { + Singular(KwShield), + Plural(KwShields), +} + +/// The "armor" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatArmor { + Singular(KwArmor), + Plural(KwArmors), +} + +/// The "material" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatMaterial { + Singular(KwMaterial), + Plural(KwMaterials) +} + +/// The "food" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatFood { + Singular(KwFood), + Plural(KwFoods) +} + +/// The "key item" tab/category +#[derive_syntax] +#[derive(Debug)] +pub enum CatKeyItem { + Singular(KwKeyItem), + Plural(KwKeyItems), +} diff --git a/packages/parser/src/syn/command.rs b/packages/parser/src/syn/command.rs new file mode 100644 index 0000000..76d05de --- /dev/null +++ b/packages/parser/src/syn/command.rs @@ -0,0 +1,342 @@ +//! Syntax for commands + +use teleparse::{derive_syntax, tp}; + +use super::token::{KwGet, KwBuy, KwHoldAttach +}; +use super::item_list::{ItemListFinite, ItemListConstrained}; +use super::{Category, ItemMeta, ItemWithSlot, ItemWithSlotOrCategory, KwBake, KwBoil, KwCloseGame, KwCloseInventory, KwCook, KwDestroy, KwDnp, KwDrop, KwEat, KwEntangle, KwEnter, KwEquip, KwExit, KwFreeze, KwHold, KwHoldSmuggle, KwLeave, KwNewGame, KwOpenInventory, KwPickUp, KwReload, KwRoast, KwSave, KwSaveAs, KwSell, KwShoot, KwSort, KwTalkTo, KwUnequip, KwUnhold, KwUntalk, KwUse, TimesClause, Word}; + +#[derive_syntax] +#[derive(Debug)] +pub enum Command { + // ==== adding items ==== + + /// `get ITEMS` + Get(CmdGet), + /// `buy ITEMS` + Buy(CmdBuy), + /// `pick-up ITEMS` + PickUp(CmdPickup), + + // ==== holding items ==== + + /// `hold ITEMS` + Hold(CmdHold), + /// `hold-smuggle ITEMS` + HoldSmuggle(CmdHoldSmuggle), + /// `hold-attach ITEMS` + HoldAttach(CmdHoldAttach), + /// `unhold` + Unhold(KwUnhold), + /// `drop` or `drop ITEMS` + Drop(CmdDrop), + /// `dnp ITEMS` + Dnp(CmdDnp), + /// `cook` or `cook ITEMS` + Cook(CmdCook), + + // ==== removing items ==== + + /// `eat ITEMS` + Eat(CmdEat), + /// `sell ITEMS` + Sell(CmdSell), + + // ==== equipments ==== + + /// `equip ITEM` + Equip(CmdEquip), + /// `unequip ITEM` or `unequip CATEGORY` + UnEquip(CmdUnequip), + /// `use CATEGORY X times` + Use(CmdUse), + /// `shoot X times` + Shoot(CmdShoot), + + // ==== overworld ==== + + /// `roast ITEMS` + Roast(CmdRoast), + /// `bake ITEMS` - same as roast + Bake(CmdBake), + /// `boil ITEMS` - same as roast except for eggs + Boil(CmdBoil), + /// `freeze ITEMS` + Freeze(CmdFreeze), + /// `destroy ITEMS` + Destroy(CmdDestroy), + + // ==== inventory ==== + + /// `sort CATEGORY` + Sort(CmdSort), + /// `entangle CATEGORY [tab=X, rol=R, col=C]` + Entangle(CmdEntangle), + + // ==== saves ==== + + /// `save` + Save(KwSave), + /// `save-as NAME` + SaveAs(CmdSaveAs), + /// `reload` or `reload NAME` + Reload(CmdReload), + /// `close-game` + CloseGame(KwCloseGame), + /// `new-game` + NewGame(KwNewGame), + + // ==== scopes ==== + OpenInventory(KwOpenInventory), + CloseInventory(KwCloseInventory), + TalkTo(KwTalkTo), + Untalk(KwUntalk), + + // ==== trials ==== + + /// `enter TRIAL` + Enter(CmdEnter), + /// `exit` - exit current trial + Exit(KwExit), + /// `leave` - leave current trial without clearing it + Leave(KwLeave), + +} + +/// `get ITEMS` - items come from the area +#[derive_syntax] +#[derive(Debug)] +pub struct CmdGet { + pub lit: KwGet, + pub items: ItemListFinite, +} + +/// `buy ITEMS` - items come from shop in the area +#[derive_syntax] +#[derive(Debug)] +pub struct CmdBuy { + pub lit: KwBuy, + pub items: ItemListFinite, +} + +/// `pick-up ITEMS` - items come from ground +#[derive_syntax] +#[derive(Debug)] +pub struct CmdPickup { + pub lit: KwPickUp, + pub items: ItemListConstrained, +} + +/// `hold ITEMS` - items come from inventory +#[derive_syntax] +#[derive(Debug)] +pub struct CmdHold { + pub lit: KwHold, + pub items: ItemListConstrained, +} + +/// `hold-smuggle ITEMS` - items come from inventory, will not hold in overworld +#[derive_syntax] +#[derive(Debug)] +pub struct CmdHoldSmuggle { + pub lit: KwHoldSmuggle, + pub items: ItemListConstrained, +} + +/// `hold-attach ITEMS` - items come from inventory, +/// dropping happens after returning to overworld scope +#[derive_syntax] +#[derive(Debug)] +pub struct CmdHoldAttach { + pub lit: KwHoldAttach, + pub items: ItemListConstrained, +} + +/// `drop` or `drop ITEMS` +/// +/// `drop ITEMS` is a shorthand, which holds the items, then drop them. +/// Cannot perform if already holding items. The exception is if the +/// item has "drop" prompt instead of "hold" prompt (equipments), +/// it will just drop the item instead +#[derive_syntax] +#[derive(Debug)] +pub struct CmdDrop { + pub lit: KwDrop, + pub items: tp::Option, +} + +/// `dnp ITEMS` - shorthand for `drop ITEMS` and `pick-up ITEMS` +#[derive_syntax] +#[derive(Debug)] +pub struct CmdDnp { + pub lit: KwDnp, + pub items: ItemListConstrained, +} + +/// `cook` or `cook ITEMS` - cook items in inventory +/// +/// `cook ITEMS` is a shorthand, which holds the items, then cook them. +#[derive_syntax] +#[derive(Debug)] +pub struct CmdCook { + pub lit: KwCook, + pub items: tp::Option, +} + +/// `eat ITEMS` - execute eat prompt on targeted items. +/// The number is the times to eat the item. +#[derive_syntax] +#[derive(Debug)] +pub struct CmdEat { + pub lit: KwEat, + pub items: ItemListConstrained, +} + +/// `sell ITEMS` - sell items to shop in the area. +#[derive_syntax] +#[derive(Debug)] +pub struct CmdSell { + pub lit: KwSell, + pub items: ItemListConstrained, +} + +/// `equip ITEM` - equip one thing +#[derive_syntax] +#[derive(Debug)] +pub struct CmdEquip { + pub lit: KwEquip, + pub items: ItemWithSlot, +} + +/// `unequip ITEM` - unequip one thing, or (all items) in one category +#[derive_syntax] +#[derive(Debug)] +pub struct CmdUnequip { + pub lit: KwUnequip, + pub items: ItemWithSlotOrCategory, +} + +/// `use CATEGORY X times` - use the item +#[derive_syntax] +#[derive(Debug)] +pub struct CmdUse { + pub lit: KwUse, + pub category: Category, + pub times: TimesClause, +} + +/// `shoot X times` is shorthand for `use bow X times` +#[derive_syntax] +#[derive(Debug)] +pub struct CmdShoot { + pub lit: KwShoot, + pub times: TimesClause, +} + +/// `roast ITEMS` - roast items on the ground or in inventory +/// +/// Items on the ground has priority, if there are not enough, +/// but there are items in inventory, then `drop ITEMS` will be +/// used to drop the items on the ground. +#[derive_syntax] +#[derive(Debug)] +pub struct CmdRoast { + pub lit: KwRoast, + pub items: ItemListConstrained, +} + +/// Alias for `roast` +#[derive_syntax] +#[derive(Debug)] +pub struct CmdBake { + pub lit: KwBake, + pub items: ItemListConstrained, +} + +/// Same as `roast`, except for eggs, where you will get boiled eggs +/// instead of campfire eggs +#[derive_syntax] +#[derive(Debug)] +pub struct CmdBoil { + pub lit: KwBoil, + pub items: ItemListConstrained, +} + +/// Similar to `roast`, but get the frozen variants +#[derive_syntax] +#[derive(Debug)] +pub struct CmdFreeze { + pub lit: KwFreeze, + pub items: ItemListConstrained, +} + +/// Destroy items on the ground +/// +/// This will just delete it from simulation. In the game, +/// there are various way to do it: throw it into the sea, +/// throw it into the lava, bomb it, etc. +/// +/// If not enough items are on the ground, items from the inventory +/// is used +#[derive_syntax] +#[derive(Debug)] +pub struct CmdDestroy { + pub lit: KwDestroy, + pub items: ItemListConstrained, +} + +/// `sort CATEGORY` - sort the category +#[derive_syntax] +#[derive(Debug)] +pub struct CmdSort { + pub lit: KwSort, + pub category: Category, +} + +/// `entangle CATEGORY [tab=X, rol=R, col=C]` - activate prompt entanglement +#[derive_syntax] +#[derive(Debug)] +pub struct CmdEntangle { + pub lit: KwEntangle, + pub category: Category, + pub meta: tp::Option +} + +/// `save-as NAME` - save the game to a named slot +#[derive_syntax] +#[derive(Debug)] +pub struct CmdSaveAs { + pub lit: KwSaveAs, + pub name: tp::Vec, +} + +/// `reload` - reload the game from manual or named save slot +/// +/// This can also be used to start the game, then reload a save +#[derive_syntax] +#[derive(Debug)] +pub struct CmdReload { + pub lit: KwReload, + pub name: tp::Vec, +} + +/// `enter TRIAL` - enter a trial +/// +/// # Trials: +/// - eventide +/// - tots/trial of the sword +/// - beginning trial (when you clear a TOTS for the first time, MS will be automatically upgraded, +/// which constitutes a gamedata sync +/// - middle trial +/// - final trial +/// - thunderblight refight (when you clear a refight for the first time, ability will be upgraded) +/// - windblight refight +/// - waterblight refight +/// - fireblight refight +#[derive_syntax] +#[derive(Debug)] +pub struct CmdEnter { + pub lit: KwEnter, + pub trial: tp::Vec, +} diff --git a/packages/parser/src/syn/item.rs b/packages/parser/src/syn/item.rs new file mode 100644 index 0000000..bee2a05 --- /dev/null +++ b/packages/parser/src/syn/item.rs @@ -0,0 +1,127 @@ +//! Base syntax for specifying an item + +use teleparse::{derive_syntax, tp}; + +use super::token::{ + AngledWord, ColonOrEqual, + NumOrAll, NumOrInfinite, Number, + QuotedWord, SymComma, SymLBracket, + SymRBracket, Word, MetaValueLiteral, KwAll, SlotClause}; + +use super::category::Category; + +/// Syntax for an item prefixed with a numeric amount +#[derive_syntax] +#[derive(Debug)] +pub struct NumberedItem { + #[teleparse(semantic(Amount))] + pub num: Number, + pub item: Item, +} + +/// Syntax for an item prefixed with an amount or "all" +#[derive_syntax] +#[derive(Debug)] +pub enum NumberedOrAllItemOrCategory { + Numbered(NumberedItem), + All(AllItemOrCategory), +} + +/// Syntax for an item prefixed with "all" +#[derive_syntax] +#[derive(Debug)] +pub struct AllItemOrCategory { + #[teleparse(semantic(Amount))] + pub all: KwAll, + pub items: ItemOrCategory, +} + +/// Syntax for an item or a category +#[derive_syntax] +#[derive(Debug)] +pub enum ItemOrCategory { + Item(Item), + Category(Category), +} + +/// Syntax for specifying a single item with a slot +#[derive_syntax] +#[derive(Debug)] +pub struct ItemWithSlot { + pub item: Item, + pub slot: tp::Option, +} + +/// Syntax for an item with a slot or a category +#[derive_syntax] +#[derive(Debug)] +pub enum ItemWithSlotOrCategory { + Item(ItemWithSlot), + Category(Category), +} + +/// Syntax for an item prefixed with an amount or "infinite" +#[derive_syntax] +#[derive(Debug)] +pub struct NumberedOrInfiniteItem { + #[teleparse(semantic(Amount))] + pub num: NumOrInfinite, + pub item: Item, +} + +/// Syntax for an item +/// +/// # Example +/// - `item` +/// - `item[meta]` +/// - `"item"` +/// - `` +#[derive_syntax] +#[derive(Debug)] +pub struct Item { + #[teleparse(semantic(Name))] + pub name: ItemName, + pub meta: tp::Option, +} + +/// Syntax for the name of an item, like `item`, `"item"`, or `` +#[derive_syntax] +#[derive(Debug)] +pub enum ItemName { + /// Using `-` or `_` separated word to search item by English name + Word(Word), + /// Use quoted value to search by name in any language + Quoted(QuotedWord), + /// Use angle brackets to use the literal as the actor name + /// e.g. `` + Angle(AngledWord), +} + +/// Syntax for the metadata specifier for an item, e.g. `[key1:value1, key2=value2, key3]` +#[derive_syntax] +#[derive(Debug)] +pub struct ItemMeta { + pub open: SymLBracket, + pub entries: tp::Punct, + pub close: SymRBracket, +} + +/// A key-value pair in an item's metadata specifier +#[derive_syntax] +#[derive(Debug)] +pub struct ItemMetaKeyValue { + /// The key of the key-value pair + #[teleparse(semantic(Variable))] + pub key: tp::String, + pub value: tp::Option +} + +/// Value after the key in an item's metadata specifier +#[derive_syntax] +#[derive(Debug)] +pub struct ItemMetaValue { + /// The seperator between the key and value + pub sep: ColonOrEqual, + /// The value of the key-value pair + pub value: MetaValueLiteral, +} diff --git a/packages/parser/src/syn/item_list.rs b/packages/parser/src/syn/item_list.rs new file mode 100644 index 0000000..453af8a --- /dev/null +++ b/packages/parser/src/syn/item_list.rs @@ -0,0 +1,51 @@ +//! Syntax for specifying a list of items + +use teleparse::{derive_syntax, tp}; + +use super::item::{Item, ItemWithSlot, NumberedItem, NumberedOrInfiniteItem, NumberedOrAllItemOrCategory}; +use super::token::SlotClause; + +/// Specifying an unconstrained, finite list of items +/// +/// This is usually used for adding items +#[derive_syntax] +#[derive(Debug)] +pub enum ItemListFinite { + /// Single item, e.g. `apple` + Single(Item), + /// multiple items with amounts, e.g. `5 apples 3 royal_claymore` + List(tp::Nev), +} + +/// Specifying an unconstrained list of items that allows +/// an infinite amount of items +#[derive_syntax] +#[derive(Debug)] +pub enum ItemListInfinite { + /// Single item, e.g. `apple` + Single(Item), + /// multiple items with amounts, e.g. `5 apples infinite royal_claymore` + List(tp::Nev), +} + +/// Specifying items from a contrained list +/// +/// This is usually used for selecting items from a list (for example, +/// for removal) +#[derive_syntax] +#[derive(Debug)] +pub enum ItemListConstrained { + /// Single item, e.g. `apple :from slot 3` + Single(ItemWithSlot), + /// multiple items with amounts, e.g. `5 apples 3 royal_claymore :from slot 3` + List(ItemListWithSlot), +} + + +/// Syntax for specifying a list of items with a slot +#[derive_syntax] +#[derive(Debug)] +pub struct ItemListWithSlot { + pub items: tp::Nev, + pub slot: tp::Option, +} diff --git a/packages/parser/src/syn/mod.rs b/packages/parser/src/syn/mod.rs new file mode 100644 index 0000000..7e15c21 --- /dev/null +++ b/packages/parser/src/syn/mod.rs @@ -0,0 +1,31 @@ +use teleparse::{derive_syntax, tp}; + +mod token; +pub use token::*; + +mod item; +pub use item::*; + +mod item_list; +pub use item_list::*; + +mod command; +pub use command::*; + +mod category; +pub use category::*; + +#[derive_syntax] +#[teleparse(root)] +#[derive(Debug)] +pub struct Script { + pub stmts: tp::Loop, +} + +#[derive_syntax] +#[derive(Debug)] +pub struct Statement { + pub cmd: Command, + pub semi: tp::Option, +} + diff --git a/packages/parser/src/syn/token.rs b/packages/parser/src/syn/token.rs new file mode 100644 index 0000000..56aa047 --- /dev/null +++ b/packages/parser/src/syn/token.rs @@ -0,0 +1,214 @@ + +use derive_more::derive::{Deref, DerefMut}; +use teleparse::{derive_lexicon, derive_syntax, tp}; + +/// Token type +#[derive_lexicon] +#[teleparse(ignore(r"\s+", r"//.*\n", r"#.*\n"))] +pub enum TT { + #[teleparse(terminal( + SymLAngle = "<", + SymRAngle = ">", + SymLParen = "(", + SymRParen = ")", + SymLBracket = "[", + SymRBracket = "]", + SymLBrace = "{", + SymRBrace = "}", + SymEqual = "=", + SymColon = ":", + SymComma = ",", + SymSemi = ";", + SymQuote = "\"", + SymPeriod = ".", + ))] + Symbol, + + #[teleparse(regex(r"((-)?\d(_?\d)*)|(0x[\da-fA-F](_?[\da-fA-F])*)"), terminal(SymNumber))] + Number, + + #[teleparse(terminal( + // KwInit = "init", + // CmdInitGdt = "init-gdt", + // + KwGet = "get", + KwBuy = "buy", + + KwHold = "hold", + KwUnhold = "unhold", + KwHoldSmuggle = "hold-smuggle", + KwHoldAttach = "hold-attach", + KwDrop = "drop", + KwDnp = "dnp", + KwPickUp = "pick-up", + KwCook = "cook", + + KwEat = "eat", + KwSell = "sell", + + KwEquip = "equip", + KwUnequip = "unequip", + KwShoot = "shoot", + KwUse = "use", + + KwRoast = "roast", + KwBake = "bake", + KwBoil = "boil", + KwFreeze = "freeze", + KwDestroy = "destroy", + + KwSort = "sort", + KwEntangle = "entangle", + + KwSave = "save", + KwSaveAs = "save-as", + KwReload = "reload", + KwCloseGame = "close-game", + KwNewGame = "new-game", + + KwOpenInventory = "open-inventory", + KwCloseInventory = "close-inventory", + KwTalkTo = "talk-to", + KwUntalk = "untalk", + + KwEnter = "enter", + KwExit = "exit", + KwLeave = "leave", + + // reserved + + KwGoto = "go-to", + ))] + Command, + + #[teleparse(terminal( + KwAll = "all", + KwInfinite = "infinite", + KwWeapon = "weapon", + KwWeapons = "weapons", + KwBow = "bow", + KwBows = "bows", + KwShield = "shield", + KwShields = "shields", + KwArmor = "armor", + KwArmors = "armors", + KwMaterial = "material", + KwMaterials = "materials", + KwFood = "food", + KwFoods = "foods", + KwKeyItem = "key-item", + KwKeyItems = "key-items", + KwTime = "time", + KwTimes = "times", + ))] + Keyword, + + #[teleparse(regex(r"[_a-zA-Z][-0-9a-zA-Z_]*"), terminal(Word))] + Word, + + #[teleparse(regex(r#""[^"]*""#), terminal(QuotedWord))] + QuotedWord, + + Variable, + Name, + Type, + + Amount, + + SuperCommand, + Annotation, +} +#[derive_syntax] +#[derive(Debug, Deref, DerefMut)] +pub struct Number(pub tp::String); + +/// A word surrounded by angle brackets, e.g. `` +#[derive_syntax] +#[derive(Debug)] +pub struct AngledWord { + /// The opening angle bracket + pub open: SymLAngle, + /// The word inside the angle brackets + pub name: Word, + /// The closing angle bracket + pub close: SymRAngle, +} + +#[derive_syntax] +#[derive(Debug)] +pub enum MetaValueLiteral { + /// A string literal - could be true/false or a string + Word(tp::String>), + /// A numeric literal + #[teleparse(semantic(Number))] + Number(MetaValueNumber), +} + +#[derive_syntax] +#[derive(Debug)] +pub struct MetaValueNumber { + pub int_part: Number, + pub float_part: tp::Option<(SymPeriod, tp::Option)>, +} + +#[derive_syntax] +#[derive(Debug)] +pub struct SymMinus { + #[teleparse(literal("-"))] + pub minus: Word, +} + +/// A number or the string "all" +#[derive_syntax] +#[derive(Debug)] +pub enum NumOrAll { + All(KwAll), + Number(Number), +} + +/// A number or the string "infinite" +#[derive_syntax] +#[derive(Debug)] +pub enum NumOrInfinite { + Infinite(KwInfinite), + Number(Number), +} + +/// Colon or equal as separator +#[derive_syntax] +#[derive(Debug)] +pub enum ColonOrEqual { + Colon(SymColon), + Equal(SymEqual), +} + + +/// Syntax for specifying a slot (:from slot X, :in slot X, :at slot X) +#[derive_syntax] +#[derive(Debug)] +pub struct SlotClause { + pub colon: SymColon, + #[teleparse(literal("from"), + literial("in"), + literial("at"), + semantic(Keyword))] + pub kw: Word, + #[teleparse(literal("slot"), semantic(Keyword))] + pub kw_slot: Word, + + pub idx: Number, +} + +#[derive_syntax] +#[derive(Debug)] +pub struct TimesClause { + pub times: Number, + pub kw: KwTimes, +} + +#[derive_syntax] +#[derive(Debug)] +pub enum Time { + Singular(KwTime), + Plural(KwTimes), +} diff --git a/packages/runtime/Cargo.toml b/packages/runtime/Cargo.toml index 925e464..33927af 100644 --- a/packages/runtime/Cargo.toml +++ b/packages/runtime/Cargo.toml @@ -5,6 +5,6 @@ edition = "2021" publish = false [dependencies] -skybook-acorn = { path = "../acorn" } +blueflame = { path = "../blueflame" } skybook-parser = { path = "../parser" } diff --git a/packages/runtime/src/lib.rs b/packages/runtime/src/lib.rs index cc66cd4..6f15c2f 100644 --- a/packages/runtime/src/lib.rs +++ b/packages/runtime/src/lib.rs @@ -1,8 +1,114 @@ +use std::collections::{HashMap, VecDeque}; -pub struct Runtime { +use blueflame::{error::Error, memory::{Memory, Proxies}}; + +mod scheduler; +use scheduler::Scheduler; + + +pub struct Runtime { + scheduler: S, + + states: Vec } +#[derive(Clone)] pub struct State { + // named gamedata in saves + saves: HashMap, + + // gamedata in manual save + manual_save: u64, + + /// Current game state + game: Game, + + /// Current screen, only valid if game is running + screen: Screen, + + /// If inventory/dialog screen is activated manually, + /// so auto-scoping will be disabled until returned to overworld screen + is_manual_scope: bool, + + /// If auto scope is enabled at all + enable_auto_scope: bool, +} + +#[derive(Clone)] +pub enum Game { + /// Game is not booted + Off, + /// Game is running + Running(GameState), + /// Game has crashed (must manually reboot) + Crashed(Error) // TODO: more crash info (dump, stack trace, etc) +} + +#[derive(Clone)] +pub enum Screen { + /// In the overworld, no additional screens + Overworld, + /// In the inventory screen + Inventory, + /// In an unknown dialog (could be sell/statue, or other) + Dialog, + /// In sell dialog + DialogSell, + /// In statue dialog + DialogStatue, +} + +/// State available when the game is running +#[derive(Clone)] +pub struct GameState { + // gamedata TriggerParam* + gamedata: u64, + // memory states + // + /// Full process memory + memory: Memory, + + /// Proxy objects in memory + proxies: Proxies, + + /// Current actors in the overworld + /// TODO: make this copy on write and Arc + ovwd_weapon: Option, + ovwd_shield: Option, + ovwd_bow: Option, + ovwd_armor_head: Option, + ovwd_armor_upper: Option, + ovwd_armor_lower: Option, + + ovwd_dropped_materials: VecDeque, + ovwd_dropped_equipments: VecDeque, + + ovwd_holding_materials: VecDeque, + + entangled_slots: Vec, +} + +#[derive(Clone)] +pub struct ActorState { + pub name: String, + pub life: u32, + pub modifier_bits: u32, + pub modifier_value: i32, +} + +impl GameState { + + // just a placeholder + // probably some kind of macro to generate these + pub async fn get_item(mut self, scheduler: impl Scheduler, item: &str) -> Result { + scheduler.run_on_core(move |p| { + let core = p.attach(&mut self.memory, &mut self.proxies); + // todo: real function + core.pmdm_item_get(item, 0, 0)?; + + Ok(self) + }).await + } } diff --git a/packages/runtime/src/scheduler/mod.rs b/packages/runtime/src/scheduler/mod.rs new file mode 100644 index 0000000..6ab7149 --- /dev/null +++ b/packages/runtime/src/scheduler/mod.rs @@ -0,0 +1,9 @@ +use blueflame::processor::Processor; + +pub trait Scheduler: Clone { + /// Future type for this scheduler + type Future: std::future::Future + Send + 'static; + + /// Schedule a task a on a processor + fn run_on_core T>(&self, f: F) -> Self::Future; +}