Work on instance loading a little bit

This commit is contained in:
Lucien Greathouse 2018-04-12 23:49:58 -07:00
parent e9bc66e5e3
commit d0f821add4
5 changed files with 158 additions and 43 deletions

View file

@ -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. * **Roblox Instance**: An actual Roblox object derived from `Instance` on the client.
## Initialization ## 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` * name: `String`
* class_name: `String` * class_name: `String`
* parent: `Option<Id>` * parent: `Option<Id>`
* properties: `HashMap<String, RbxValue>` * properties: `HashMap<String, RbxValue>`
* route: `Route` (does not have to serialize) * file_route: `FileRoute`
* id: `Id` * id: `Id`
* rbx_referent: `Option<RbxReferent>` * rbx_referent: `Option<RbxReferent>`
The server should keep these book-keeping maps: The server should keep these book-keeping maps:
* `HashMap<Id, RbxInstance`, to serve as an index * partitions: `HashMap<String, Partition>`, to read partitions from disk
* `HashMap<Route, Id>`, to manage filesystem changes * partition_instances: `HashMap<String, Id>`, to track partition instances
* `HashMap<Id, Route>`, to manage changes from the client * partition_files: `HashMap<String, VfsItem>`, to serve as an in-memory filesystem
* `HashMap<Route, String>`, to disambiguate and debounce changes written to the filesystem * instances: `HashMap<Id, RbxInstance>`, to serve as an index
* `HashMap<RbxReferent, Id>`, when loading `rbxmx` format models from disk * instances_by_route: `HashMap<FileRoute, Id>`, to handle filesystem changes
As a future extension, the server should also track:
* `referent_to_id`: `HashMap<RbxReferent, Id>`, when loading `rbxmx` format models from disk.
`Partition` is defined as:
* path: `PathBuf`
* target: `Vec<String>`
On initial sync, the client should receive: On initial sync, the client should receive:
@ -38,14 +47,13 @@ On initial sync, the client should receive:
## File Changes ## File Changes
Whenever a file is changed, the server should: 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. * 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. * 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. * Else, clear the value from the map.
* Pass the `VfsItem` through the middleware chain to a tree of `RbxInstance` objects. * Pass the `VfsItem` through the middleware chain to create 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 `FileRoute` to `Id`.
* Check the existing map from `Route` to `Id`.
* If an existing instance exists with the root's `Id`, reuse it and replace the root's `Id`. * If an existing instance exists with the root's `Id`, reuse it and replace the root's `Id`.
* Else, generate a new ID. * Else, generate a new ID.
* For each child instance: * For each child instance:
@ -69,7 +77,7 @@ Those changes should come in three flavors:
* Deleted a known instance (need `id`) * Deleted a known instance (need `id`)
* Created a new instance (need `clientId`) * 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. In order to correctly serialize changes received from the client, some care needs to be taken.

View file

@ -1,5 +1,7 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use partition::Partition;
// TODO: Add lifetime, switch to using Cow<'a, str> instead of String? It's // TODO: Add lifetime, switch to using Cow<'a, str> instead of String? It's
// possible that it would be too cumbersome! // possible that it would be too cumbersome!
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
@ -34,4 +36,21 @@ impl FileRoute {
result 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()
}
}
} }

View file

@ -13,6 +13,7 @@ extern crate tempfile;
pub mod commands; pub mod commands;
pub mod file_route; pub mod file_route;
pub mod id; pub mod id;
pub mod partition;
pub mod pathext; pub mod pathext;
pub mod project; pub mod project;
pub mod rbx_session; pub mod rbx_session;

10
server/src/partition.rs Normal file
View file

@ -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<String>,
}

View file

@ -3,52 +3,95 @@ use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::fs::{self, File}; use std::fs::{self, File};
use id::{Id}; use id::{Id, get_id};
use file_route::FileRoute; use file_route::FileRoute;
use partition::Partition;
/// Represents a file or directory that has been read from the filesystem. /// Represents a file or directory that has been read from the filesystem.
// TODO: Keep track of file Path or FileRoute?
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum FileItem { enum FileItem {
File { File {
contents: String, contents: String,
route: FileRoute,
}, },
Directory { Directory {
children: HashMap<String, FileItem>, children: HashMap<String, FileItem>,
route: FileRoute,
}, },
} }
// TODO: Switch to enum to represent more value types
type RbxValue = String;
#[derive(Debug, Clone)]
struct RbxInstance { struct RbxInstance {
/// Maps to the `Name` property on Instance.
pub name: String,
/// Maps to the `ClassName` property on Instance.
pub class_name: String, pub class_name: String,
/// Maps to the `Parent` property on Instance.
pub parent: Option<Id>, pub parent: Option<Id>,
pub properties: HashMap<String, String>,
/// Contains all other properties of an Instance.
pub properties: HashMap<String, RbxValue>,
} }
struct RbxSession { struct RbxSession {
pub partition_paths: HashMap<String, PathBuf>, pub partitions: HashMap<String, Partition>,
/// The RbxInstance that represents each partition.
pub partition_instances: HashMap<String, Id>, pub partition_instances: HashMap<String, Id>,
/// The in-memory files associated with each partition.
pub partition_files: HashMap<String, FileItem>, pub partition_files: HashMap<String, FileItem>,
/// The store of all instances in the session.
pub instances: HashMap<Id, RbxInstance>, pub instances: HashMap<Id, RbxInstance>,
// pub instances_by_route: HashMap<FileRoute, Id>,
} }
fn file_to_instance(file_item: &FileItem) -> RbxInstance { fn file_to_instance(file_item: &FileItem, partition: &Partition) -> (Id, HashMap<Id, RbxInstance>) {
match file_item { match file_item {
&FileItem::File { ref contents } => { &FileItem::File { ref contents, ref route } => {
let mut properties = HashMap::new(); let mut properties = HashMap::new();
properties.insert("Value".to_string(), contents.clone()); 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(), class_name: "StringValue".to_string(),
parent: None, parent: None,
properties, properties,
} });
(primary_id, instances)
}, },
&FileItem::Directory { ref children } => { &FileItem::Directory { ref children, ref route } => {
RbxInstance { 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(), class_name: "Folder".to_string(),
parent: None, parent: None,
properties: HashMap::new(), 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 { impl RbxSession {
fn new() -> RbxSession { fn new() -> RbxSession {
RbxSession { RbxSession {
partition_paths: HashMap::new(), partitions: HashMap::new(),
partition_instances: HashMap::new(), partition_instances: HashMap::new(),
partition_files: HashMap::new(), partition_files: HashMap::new(),
instances: HashMap::new(), instances: HashMap::new(),
@ -64,7 +107,7 @@ impl RbxSession {
} }
fn load_files(&mut self) { fn load_files(&mut self) {
for partition_name in self.partition_paths.keys() { for partition_name in self.partitions.keys() {
let route = FileRoute { let route = FileRoute {
partition: partition_name.clone(), partition: partition_name.clone(),
route: vec![], route: vec![],
@ -76,12 +119,25 @@ impl RbxSession {
} }
} }
fn read(&self, route: &FileRoute) -> Result<FileItem, ()> { fn load_instances(&mut self) {
let partition_path = self.partition_paths.get(&route.partition) for (partition_name, file_item) in &self.partition_files {
.ok_or(())?; let partition = self.partitions.get(partition_name).unwrap();
let path = route.to_path_buf(partition_path); 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<FileItem, ()> {
let partition_path = &self.partitions.get(&route.partition)
.ok_or(())?.path;
let path = route.to_path_buf(partition_path);
let metadata = fs::metadata(path) let metadata = fs::metadata(path)
.map_err(|_| ())?; .map_err(|_| ())?;
@ -96,8 +152,8 @@ impl RbxSession {
} }
fn read_file(&self, route: &FileRoute) -> Result<FileItem, ()> { fn read_file(&self, route: &FileRoute) -> Result<FileItem, ()> {
let partition_path = self.partition_paths.get(&route.partition) let partition_path = &self.partitions.get(&route.partition)
.ok_or(())?; .ok_or(())?.path;
let path = route.to_path_buf(partition_path); let path = route.to_path_buf(partition_path);
let mut file = File::open(path) let mut file = File::open(path)
@ -110,12 +166,13 @@ impl RbxSession {
Ok(FileItem::File { Ok(FileItem::File {
contents, contents,
route: route.clone(),
}) })
} }
fn read_directory(&self, route: &FileRoute) -> Result<FileItem, ()> { fn read_directory(&self, route: &FileRoute) -> Result<FileItem, ()> {
let partition_path = self.partition_paths.get(&route.partition) let partition_path = &self.partitions.get(&route.partition)
.ok_or(())?; .ok_or(())?.path;
let path = route.to_path_buf(partition_path); let path = route.to_path_buf(partition_path);
let reader = fs::read_dir(path) let reader = fs::read_dir(path)
@ -139,6 +196,7 @@ impl RbxSession {
Ok(FileItem::Directory { Ok(FileItem::Directory {
children, children,
route: route.clone(),
}) })
} }
} }
@ -174,21 +232,40 @@ mod tests {
let mut session = RbxSession::new(); 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(); session.load_files();
assert_eq!(session.partition_files.len(), 1); 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 { let children = match folder {
&FileItem::Directory { ref children } => children, &FileItem::Directory { ref children, .. } => children,
_ => panic!("Not a directory!"), _ => panic!("Not a directory!"),
}; };
assert_eq!(children.len(), 2); assert_eq!(children.len(), 2);
assert!(children.get("foo.txt").is_some()); assert!(children.get("foo.txt").is_some());
assert!(children.get("bar.tsv").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");
}
} }
} }