From d0f821add46f05884e79a6d573f2de1a4f520a70 Mon Sep 17 00:00:00 2001 From: Lucien Greathouse Date: Thu, 12 Apr 2018 23:49:58 -0700 Subject: [PATCH] Work on instance loading a little bit --- DESIGN.md | 34 ++++++---- server/src/file_route.rs | 19 ++++++ server/src/lib.rs | 1 + server/src/partition.rs | 10 +++ server/src/rbx_session.rs | 137 +++++++++++++++++++++++++++++--------- 5 files changed, 158 insertions(+), 43 deletions(-) create mode 100644 server/src/partition.rs diff --git a/DESIGN.md b/DESIGN.md index 61138039..44a26961 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -12,23 +12,32 @@ The aim is to rewrite most of Rojo to drop the notion of routes almost entirely * **Roblox Instance**: An actual Roblox object derived from `Instance` on the client. ## Initialization -On server initialization, Rojo reads all files in each partition and runs each through our middleware stack (renamed from plugins). Middleware transform a `VfsItem` into a list of `RbxInstance` objects with these properties: +On server initialization, Rojo reads all files in each partition and runs each through our middleware stack (renamed from plugins). Middleware transform a `VfsItem` into a set of `RbxInstance` objects with these properties: * name: `String` * class_name: `String` * parent: `Option` * properties: `HashMap` -* route: `Route` (does not have to serialize) +* file_route: `FileRoute` * id: `Id` * rbx_referent: `Option` The server should keep these book-keeping maps: -* `HashMap`, to manage filesystem changes -* `HashMap`, to manage changes from the client -* `HashMap`, to disambiguate and debounce changes written to the filesystem -* `HashMap`, when loading `rbxmx` format models from disk +* partitions: `HashMap`, to read partitions from disk +* partition_instances: `HashMap`, to track partition instances +* partition_files: `HashMap`, to serve as an in-memory filesystem +* instances: `HashMap`, to serve as an index +* instances_by_route: `HashMap`, to handle filesystem changes + +As a future extension, the server should also track: + +* `referent_to_id`: `HashMap`, when loading `rbxmx` format models from disk. + +`Partition` is defined as: + +* path: `PathBuf` +* target: `Vec` On initial sync, the client should receive: @@ -38,14 +47,13 @@ On initial sync, the client should receive: ## File Changes Whenever a file is changed, the server should: -* Convert its path to a `Route`. +* Convert its path to a `FileRoute`. * Read the file into a `VfsItem` object. -* Compare the contents of the file with the last-seen contents from the map from `Route` to `String`. +* Compare the contents of the file with the last-seen contents from the map from `FileRoute` to `String`. * If they're the same, that means this change was caused by the server itself, and should be discarded. * Else, clear the value from the map. -* Pass the `VfsItem` through the middleware chain to a tree of `RbxInstance` objects. - * TODO: How should this tree be represented? The normal `RbxInstance` case should just reference children by ID, but that's super inconvenient here. -* Check the existing map from `Route` to `Id`. +* Pass the `VfsItem` through the middleware chain to create a tree of `RbxInstance` objects. +* Check the existing map from `FileRoute` to `Id`. * If an existing instance exists with the root's `Id`, reuse it and replace the root's `Id`. * Else, generate a new ID. * For each child instance: @@ -69,7 +77,7 @@ Those changes should come in three flavors: * Deleted a known instance (need `id`) * Created a new instance (need `clientId`) -Every sync period, which could be around 100ms, the plugin should send a request with every Roblox Instance mutation all at once. +Every sync period, which could be around 50ms, the plugin should send a request with every Roblox Instance mutation all at once. In order to correctly serialize changes received from the client, some care needs to be taken. diff --git a/server/src/file_route.rs b/server/src/file_route.rs index ef6f6270..f1e500ec 100644 --- a/server/src/file_route.rs +++ b/server/src/file_route.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use partition::Partition; + // TODO: Add lifetime, switch to using Cow<'a, str> instead of String? It's // possible that it would be too cumbersome! #[derive(Debug, Clone, PartialEq, Hash)] @@ -34,4 +36,21 @@ impl FileRoute { result } + + /// This function is totally wrong and should be handled by middleware, heh. + pub fn name(&self, partition: &Partition) -> String { // I guess?? + if self.route.len() == 0 { + // This FileRoute refers to the partition itself + + if partition.target.len() == 0 { + // We're targeting the game itself! + "game".to_string() + } else { + partition.target.last().unwrap().clone() + } + } else { + // This FileRoute refers to an item in a partition + self.route.last().unwrap().clone() + } + } } diff --git a/server/src/lib.rs b/server/src/lib.rs index e8c645d3..bf922c12 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -13,6 +13,7 @@ extern crate tempfile; pub mod commands; pub mod file_route; pub mod id; +pub mod partition; pub mod pathext; pub mod project; pub mod rbx_session; diff --git a/server/src/partition.rs b/server/src/partition.rs new file mode 100644 index 00000000..dec43076 --- /dev/null +++ b/server/src/partition.rs @@ -0,0 +1,10 @@ +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct Partition { + /// The path on the filesystem that this partition maps to. + pub path: PathBuf, + + /// The route to the Roblox instance that this partition maps to. + pub target: Vec, +} diff --git a/server/src/rbx_session.rs b/server/src/rbx_session.rs index a0ae657a..4067b680 100644 --- a/server/src/rbx_session.rs +++ b/server/src/rbx_session.rs @@ -3,52 +3,95 @@ use std::collections::HashMap; use std::io::Read; use std::fs::{self, File}; -use id::{Id}; +use id::{Id, get_id}; use file_route::FileRoute; +use partition::Partition; /// Represents a file or directory that has been read from the filesystem. -// TODO: Keep track of file Path or FileRoute? #[derive(Debug, Clone)] enum FileItem { File { contents: String, + route: FileRoute, }, Directory { children: HashMap, + route: FileRoute, }, } +// TODO: Switch to enum to represent more value types +type RbxValue = String; + +#[derive(Debug, Clone)] struct RbxInstance { + /// Maps to the `Name` property on Instance. + pub name: String, + + /// Maps to the `ClassName` property on Instance. pub class_name: String, + + /// Maps to the `Parent` property on Instance. pub parent: Option, - pub properties: HashMap, + + /// Contains all other properties of an Instance. + pub properties: HashMap, } struct RbxSession { - pub partition_paths: HashMap, + pub partitions: HashMap, + + /// The RbxInstance that represents each partition. pub partition_instances: HashMap, + + /// The in-memory files associated with each partition. pub partition_files: HashMap, + + /// The store of all instances in the session. pub instances: HashMap, + // pub instances_by_route: HashMap, } -fn file_to_instance(file_item: &FileItem) -> RbxInstance { +fn file_to_instance(file_item: &FileItem, partition: &Partition) -> (Id, HashMap) { match file_item { - &FileItem::File { ref contents } => { + &FileItem::File { ref contents, ref route } => { let mut properties = HashMap::new(); properties.insert("Value".to_string(), contents.clone()); - RbxInstance { + let primary_id = get_id(); + let mut instances = HashMap::new(); + + instances.insert(primary_id, RbxInstance { + name: route.name(partition).to_string(), class_name: "StringValue".to_string(), parent: None, properties, - } + }); + + (primary_id, instances) }, - &FileItem::Directory { ref children } => { - RbxInstance { + &FileItem::Directory { ref children, ref route } => { + let primary_id = get_id(); + let mut instances = HashMap::new(); + + instances.insert(primary_id, RbxInstance { + name: route.name(partition).to_string(), class_name: "Folder".to_string(), parent: None, properties: HashMap::new(), + }); + + for child_file_item in children.values() { + let (child_id, mut child_instances) = file_to_instance(child_file_item, partition); + + child_instances.get_mut(&child_id).unwrap().parent = Some(primary_id); + + for (instance_id, instance) in child_instances.drain() { + instances.insert(instance_id, instance); + } } + + (primary_id, instances) } } } @@ -56,7 +99,7 @@ fn file_to_instance(file_item: &FileItem) -> RbxInstance { impl RbxSession { fn new() -> RbxSession { RbxSession { - partition_paths: HashMap::new(), + partitions: HashMap::new(), partition_instances: HashMap::new(), partition_files: HashMap::new(), instances: HashMap::new(), @@ -64,7 +107,7 @@ impl RbxSession { } fn load_files(&mut self) { - for partition_name in self.partition_paths.keys() { + for partition_name in self.partitions.keys() { let route = FileRoute { partition: partition_name.clone(), route: vec![], @@ -76,12 +119,25 @@ impl RbxSession { } } - fn read(&self, route: &FileRoute) -> Result { - let partition_path = self.partition_paths.get(&route.partition) - .ok_or(())?; - let path = route.to_path_buf(partition_path); + fn load_instances(&mut self) { + for (partition_name, file_item) in &self.partition_files { + let partition = self.partitions.get(partition_name).unwrap(); + let (root_id, mut instances) = file_to_instance(&file_item, partition); - println!("Read {:?}, path {}", route, path.display()); + // there has to be an std method for this + // oh well + for (instance_id, instance) in instances.drain() { + self.instances.insert(instance_id, instance); + } + + self.partition_instances.insert(partition_name.clone(), root_id); + } + } + + fn read(&self, route: &FileRoute) -> Result { + let partition_path = &self.partitions.get(&route.partition) + .ok_or(())?.path; + let path = route.to_path_buf(partition_path); let metadata = fs::metadata(path) .map_err(|_| ())?; @@ -96,8 +152,8 @@ impl RbxSession { } fn read_file(&self, route: &FileRoute) -> Result { - let partition_path = self.partition_paths.get(&route.partition) - .ok_or(())?; + let partition_path = &self.partitions.get(&route.partition) + .ok_or(())?.path; let path = route.to_path_buf(partition_path); let mut file = File::open(path) @@ -110,12 +166,13 @@ impl RbxSession { Ok(FileItem::File { contents, + route: route.clone(), }) } fn read_directory(&self, route: &FileRoute) -> Result { - let partition_path = self.partition_paths.get(&route.partition) - .ok_or(())?; + let partition_path = &self.partitions.get(&route.partition) + .ok_or(())?.path; let path = route.to_path_buf(partition_path); let reader = fs::read_dir(path) @@ -139,6 +196,7 @@ impl RbxSession { Ok(FileItem::Directory { children, + route: route.clone(), }) } } @@ -174,21 +232,40 @@ mod tests { let mut session = RbxSession::new(); - session.partition_paths.insert("agh".to_string(), root_dir.path().to_path_buf()); + let partition = Partition { + path: root_dir.path().to_path_buf(), + target: vec!["ReplicatedStorage".to_string()], + }; + + session.partitions.insert("agh".to_string(), partition); session.load_files(); assert_eq!(session.partition_files.len(), 1); - let folder = session.partition_files.values().nth(0).unwrap(); + { + let folder = session.partition_files.values().nth(0).unwrap(); - let children = match folder { - &FileItem::Directory { ref children } => children, - _ => panic!("Not a directory!"), - }; + let children = match folder { + &FileItem::Directory { ref children, .. } => children, + _ => panic!("Not a directory!"), + }; - assert_eq!(children.len(), 2); - assert!(children.get("foo.txt").is_some()); - assert!(children.get("bar.tsv").is_some()); + assert_eq!(children.len(), 2); + assert!(children.get("foo.txt").is_some()); + assert!(children.get("bar.tsv").is_some()); + } + + session.load_instances(); + + assert_eq!(session.instances.len(), 3); + assert_eq!(session.partition_instances.len(), 1); + + { + let folder_id = session.partition_instances.values().nth(0).unwrap(); + + let folder = session.instances.get(folder_id).unwrap(); + assert_eq!(folder.name, "ReplicatedStorage"); + } } }