mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Update protocol version to 6 and enhance API documentation
- Incremented the protocol version from 5 to 6 in the Atlas API documentation, reflecting the latest changes in server/plugin compatibility. - Added new API endpoints for live patch streaming and updated the server info response structure to include additional fields such as `unexpectedPlaceIds` and `gameId`. - Improved the WebSocket protocol documentation to align with the new versioning. - Enhanced metadata handling in the syncback rules and clarified the structure of meta files, including the introduction of new properties and adjustments to existing ones. These updates improve clarity and maintainability of the API documentation and ensure better alignment with the latest implementation changes.
This commit is contained in:
parent
f798f3f5e8
commit
200c5ca219
4 changed files with 127 additions and 113 deletions
|
|
@ -29,7 +29,7 @@ We are building a maintainable codebase. During audits, watch for duplicated log
|
||||||
|
|
||||||
The goal is clean, DRY code that is easy to reason about and hard to get wrong. If the same "compute meta path, check if name needs slugify, upsert/remove name field" sequence appears in 3+ places, that's a refactor candidate.
|
The goal is clean, DRY code that is easy to reason about and hard to get wrong. If the same "compute meta path, check if name needs slugify, upsert/remove name field" sequence appears in 3+ places, that's a refactor candidate.
|
||||||
|
|
||||||
**Protocol Version:** 5 | Server/plugin MAJOR.MINOR must match
|
**Protocol Version:** 6 | Server/plugin MAJOR.MINOR must match
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
|
@ -50,19 +50,25 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
||||||
| `/api/write` | POST | Two-way sync: plugin sends changes |
|
| `/api/write` | POST | Two-way sync: plugin sends changes |
|
||||||
| `/api/open/:id` | POST | Open script in editor (`{"cursor": [line, col]}`) |
|
| `/api/open/:id` | POST | Open script in editor (`{"cursor": [line, col]}`) |
|
||||||
| `/api/ref-patch/:id` | GET | Fix Ref properties after syncback |
|
| `/api/ref-patch/:id` | GET | Fix Ref properties after syncback |
|
||||||
|
| `/api/socket/:cursor` | GET | WebSocket upgrade for live patch streaming |
|
||||||
|
|
||||||
### Server Info Response (`GET /api/rojo`)
|
### Server Info Response (`GET /api/rojo`)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"serverVersion": "7.7.0-rc.1",
|
"serverVersion": "7.7.0-rc.1",
|
||||||
"protocolVersion": 5,
|
"serverFork": "atlas",
|
||||||
|
"protocolVersion": 6,
|
||||||
"sessionId": "uuid",
|
"sessionId": "uuid",
|
||||||
"projectName": "MyProject",
|
"projectName": "MyProject",
|
||||||
"expectedPlaceIds": [123456],
|
"expectedPlaceIds": [123456],
|
||||||
|
"unexpectedPlaceIds": [111111],
|
||||||
"rootInstanceId": "00000000-0000-0000-0000-000000000000",
|
"rootInstanceId": "00000000-0000-0000-0000-000000000000",
|
||||||
"syncSourceOnly": true,
|
"syncSourceOnly": true,
|
||||||
"visibleServices": ["Workspace", "ServerScriptService"]
|
"visibleServices": ["Workspace", "ServerScriptService"],
|
||||||
|
"placeId": 123456,
|
||||||
|
"gameId": 789012,
|
||||||
|
"ignoreHiddenServices": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -70,17 +76,18 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"sessionId": "uuid",
|
||||||
"removed": ["id-1"],
|
"removed": ["id-1"],
|
||||||
"added": { "id-2": { "id": "id-2", "className": "Folder", "name": "New", "properties": {}, "children": [] } },
|
"added": { "id-2": { "parent": "parent-id", "className": "Folder", "name": "New", "properties": {}, "children": [] } },
|
||||||
"updated": [{ "id": "id-3", "changedName": "Renamed", "changedProperties": { "Source": "print('hi')" } }]
|
"updated": [{ "id": "id-3", "changedName": "Renamed", "changedClassName": "Script", "changedProperties": { "Source": "print('hi')" }, "changedMetadata": {} }]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Format detection:** `Name/init.luau` → directory | `Name.luau` → file | neither → use `has_children` (`.lua` also supported)
|
**Format detection:** `Name/init.luau` → directory | `Name.luau` → file | neither → use `has_children` (`.lua` also supported)
|
||||||
|
|
||||||
## WebSocket Protocol (v5)
|
## WebSocket Protocol (v6)
|
||||||
|
|
||||||
**Connection:** `ws://host:port/api/rojo`
|
**Connection:** `ws://host:port/api/socket/:cursor`
|
||||||
|
|
||||||
1. Connect → Receive `ServerInfo` handshake → Send `{type = "Subscribe"}` → Receive patches
|
1. Connect → Receive `ServerInfo` handshake → Send `{type = "Subscribe"}` → Receive patches
|
||||||
|
|
||||||
|
|
@ -112,11 +119,15 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
pub struct ServeSession {
|
pub struct ServeSession {
|
||||||
session_id: Uuid,
|
session_id: SessionId,
|
||||||
tree_handle: Arc<Mutex<RojoTree>>,
|
tree: Arc<Mutex<RojoTree>>,
|
||||||
message_queue: MessageQueue,
|
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
||||||
project: Project,
|
root_project: Project,
|
||||||
vfs: Vfs,
|
vfs: Arc<Vfs>,
|
||||||
|
change_processor: ChangeProcessor,
|
||||||
|
start_time: Instant,
|
||||||
|
tree_mutation_sender: Sender<PatchSet>,
|
||||||
|
suppressed_paths: Arc<Mutex<HashMap<PathBuf, (usize, usize)>>>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -223,8 +234,8 @@ for _, update in patch.updated do Reconciler.hydrate(instanceMap:fromId(update.i
|
||||||
│ Sends PatchSet to tree_mutation_sender channel │
|
│ Sends PatchSet to tree_mutation_sender channel │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ ChangeProcessor::handle_tree_event() [src/change_processor.rs] │
|
│ ChangeProcessor::handle_tree_event() [src/change_processor.rs] │
|
||||||
│ │ For removed: fs::remove_file(instigating_source.path) │
|
│ │ For removed: fs::remove_file(path) + remove meta │
|
||||||
│ │ For updated: if "Source" property → fs::write(path, value) │
|
│ │ For updated: Source writes, renames, ClassName changes, properties │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
│ apply_patch_set() → Updates RojoTree │
|
│ apply_patch_set() → Updates RojoTree │
|
||||||
│ ▼ │
|
│ ▼ │
|
||||||
|
|
@ -510,54 +521,14 @@ true, false, 42, "string"
|
||||||
|
|
||||||
### Server-Side: ChangeProcessor (`src/change_processor.rs`)
|
### Server-Side: ChangeProcessor (`src/change_processor.rs`)
|
||||||
|
|
||||||
Receives patches from plugin via `tree_mutation_receiver` channel:
|
Receives patches from plugin via `tree_mutation_receiver` channel. Handles removals, Source writes, renames (with slugify/dedup/meta lifecycle), ClassName changes (script type transitions via file renames), and property writes to meta/model files.
|
||||||
|
|
||||||
```rust
|
**Capabilities:**
|
||||||
fn handle_tree_event(&self, patch_set: PatchSet) {
|
- **Removals:** Deletes file + associated meta file via `InstigatingSource::Path`
|
||||||
let mut tree = self.tree.lock().unwrap();
|
- **Source writes:** Updates script file contents
|
||||||
|
- **Renames:** Slugifies new name, deduplicates against siblings, renames file on disk, creates/updates/removes meta `name` field as needed. Handles both standalone files and init-file directories.
|
||||||
// Process removals
|
- **ClassName changes:** Script-to-script transitions (e.g. ModuleScript → Script) via file extension renames
|
||||||
for &id in &patch_set.removed_instances {
|
- **Property writes:** Attributes and other properties written to meta/model files via `syncback_updated_properties` in `api.rs`
|
||||||
if let Some(instance) = tree.get_instance(id) {
|
|
||||||
match &instance.metadata().instigating_source {
|
|
||||||
Some(InstigatingSource::Path(path)) => {
|
|
||||||
log::info!("Two-way sync: Removing file {}", path.display());
|
|
||||||
fs::remove_file(path)?;
|
|
||||||
}
|
|
||||||
Some(InstigatingSource::ProjectNode { .. }) => {
|
|
||||||
log::warn!("Cannot remove instance from project file");
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::warn!("Cannot remove instance without instigating source");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process updates
|
|
||||||
for update in &patch_set.updated_instances {
|
|
||||||
if let Some(instance) = tree.get_instance(update.id) {
|
|
||||||
// Currently only Source property is supported
|
|
||||||
if let Some(Variant::String(source)) = update.changed_properties.get("Source") {
|
|
||||||
match &instance.metadata().instigating_source {
|
|
||||||
Some(InstigatingSource::Path(path)) => {
|
|
||||||
log::info!("Two-way sync: Writing Source to {}", path.display());
|
|
||||||
fs::write(path, source)?;
|
|
||||||
}
|
|
||||||
_ => log::warn!("Cannot update instance without path source"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other property changes logged but not written
|
|
||||||
if update.changed_name.is_some() { log::warn!("Cannot rename instances yet"); }
|
|
||||||
if update.changed_class_name.is_some() { log::warn!("Cannot change ClassName yet"); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
apply_patch_set(&mut tree, patch_set); // Update DOM
|
|
||||||
self.message_queue.push_messages(&[applied_patch]); // Broadcast to plugins
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**InstigatingSource:** Every instance in RojoTree tracks where it came from:
|
**InstigatingSource:** Every instance in RojoTree tracks where it came from:
|
||||||
- `InstigatingSource::Path(PathBuf)` - Created from a file, can be written back
|
- `InstigatingSource::Path(PathBuf)` - Created from a file, can be written back
|
||||||
|
|
@ -620,15 +591,10 @@ end
|
||||||
| **Undo/redo triggers spurious changes** | Studio's ChangeHistoryService fires property changes during undo | Can cause unexpected writes or conflicts |
|
| **Undo/redo triggers spurious changes** | Studio's ChangeHistoryService fires property changes during undo | Can cause unexpected writes or conflicts |
|
||||||
| **Rapid changes interleave** | 200ms batch window may split logically related changes | Partial updates can leave inconsistent state |
|
| **Rapid changes interleave** | 200ms batch window may split logically related changes | Partial updates can leave inconsistent state |
|
||||||
| **Reparenting not supported** | `Parent` property changes to non-nil values are ignored | Moving instances in Studio doesn't sync |
|
| **Reparenting not supported** | `Parent` property changes to non-nil values are ignored | Moving instances in Studio doesn't sync |
|
||||||
| **Renaming not supported** | `changedName` logged but not written | Renaming in Studio doesn't sync |
|
|
||||||
| **ClassName changes ignored** | Would require file type change | Changing script type doesn't sync |
|
|
||||||
| **Only Source property writes** | Other properties logged but not written to filesystem | Only script content syncs reliably |
|
|
||||||
| **Project node instances immutable** | InstigatingSource::ProjectNode cannot be modified | Instances from project file can't be changed |
|
| **Project node instances immutable** | InstigatingSource::ProjectNode cannot be modified | Instances from project file can't be changed |
|
||||||
| **Duplicate-named siblings skipped** | Path ambiguity makes reliable sync impossible | Instances with same-name siblings ignored |
|
| **Duplicate-named siblings skipped** | Path ambiguity makes reliable sync impossible | Instances with same-name siblings ignored |
|
||||||
| **Ref properties complex** | Instance references may become invalid when instances move | ObjectValue.Value may not sync correctly |
|
| **Ref properties complex** | Instance references may become invalid when instances move | ObjectValue.Value may not sync correctly |
|
||||||
|
|
||||||
**Recommended workflow:** Use one-way sync (server → Studio) as primary. Use two-way sync only for script Source changes with careful attention to undo/redo behavior.
|
|
||||||
|
|
||||||
## Protocol Enforcement
|
## Protocol Enforcement
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ Used in `tree` and recursively for all children.
|
||||||
| `*.lua` | ModuleScript | — | Legacy |
|
| `*.lua` | ModuleScript | — | Legacy |
|
||||||
| `*.server.lua` | Script | Legacy | Legacy |
|
| `*.server.lua` | Script | Legacy | Legacy |
|
||||||
| `*.client.lua` | LocalScript | — | Legacy |
|
| `*.client.lua` | LocalScript | — | Legacy |
|
||||||
|
| `*.plugin.lua` | Script | Plugin | Legacy |
|
||||||
|
|
||||||
**Prefer `.luau`** — modern RunContext. `.lua` is legacy (still supported).
|
**Prefer `.luau`** — modern RunContext. `.lua` is legacy (still supported).
|
||||||
|
|
||||||
|
|
@ -199,6 +200,9 @@ Configure `syncbackRules` object for Roblox → filesystem sync:
|
||||||
| `syncCurrentCamera` | bool | `false` | Whether to sync `Workspace.CurrentCamera` |
|
| `syncCurrentCamera` | bool | `false` | Whether to sync `Workspace.CurrentCamera` |
|
||||||
| `syncUnscriptable` | bool | `true` | Whether to sync unscriptable properties |
|
| `syncUnscriptable` | bool | `true` | Whether to sync unscriptable properties |
|
||||||
| `ignoreReferents` | bool | `false` | Whether to skip Ref properties (e.g., `Model.PrimaryPart`) |
|
| `ignoreReferents` | bool | `false` | Whether to skip Ref properties (e.g., `Model.PrimaryPart`) |
|
||||||
|
| `createIgnoreDirPaths` | bool | `false` | Whether `/**` in ignore paths also matches the directory itself |
|
||||||
|
| `ignoreHiddenServices` | bool | — | Override root-level `ignoreHiddenServices` for syncback |
|
||||||
|
| `warnDuplicateNames` | bool | `false` | Warn on duplicate child names during syncback |
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
|
|
@ -212,42 +216,70 @@ Configure `syncbackRules` object for Roblox → filesystem sync:
|
||||||
"ignoreClasses": ["Terrain", "Camera"],
|
"ignoreClasses": ["Terrain", "Camera"],
|
||||||
"syncCurrentCamera": false,
|
"syncCurrentCamera": false,
|
||||||
"syncUnscriptable": true,
|
"syncUnscriptable": true,
|
||||||
"ignoreReferents": false
|
"ignoreReferents": false,
|
||||||
|
"createIgnoreDirPaths": false,
|
||||||
|
"warnDuplicateNames": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Meta Files
|
## Meta Files
|
||||||
|
|
||||||
Add metadata to file-based instances. Example: `MyScript.server.meta.json5` (for `MyScript.server.luau`)
|
Add metadata to file-based instances. Meta files use **camelCase keys** (no `$` prefix -- that's only for ProjectNode).
|
||||||
|
|
||||||
|
**Adjacent meta** (e.g. `MyScript.server.meta.json5` for `MyScript.server.luau`):
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"$className": "Script", // Override (rare)
|
"name": "Key:Script", // Override filename-derived instance name (slugify support)
|
||||||
"$properties": { "Disabled": false },
|
"id": "unique-id", // For Ref property linking
|
||||||
"$attributes": { "Priority": 1 },
|
"properties": { "Disabled": false },
|
||||||
"$ignoreUnknownInstances": true
|
"attributes": { "Priority": 1 },
|
||||||
|
"ignoreUnknownInstances": true
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Meta files CAN'T:** Override name, set `$path`, have children.
|
**Directory meta** (e.g. `init.meta.json5` inside a directory):
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"name": "My:Folder", // Override directory-derived instance name
|
||||||
|
"className": "Configuration", // Override class (directory meta only)
|
||||||
|
"id": "unique-id",
|
||||||
|
"properties": { "Anchored": true },
|
||||||
|
"attributes": { "Health": 100 },
|
||||||
|
"ignoreUnknownInstances": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key differences from ProjectNode:** Meta uses `className`, `properties`, `attributes` (camelCase, no `$` prefix). Adjacent meta does NOT support `className`.
|
||||||
|
|
||||||
|
**`name` field (slugify support):** When an instance name contains forbidden filesystem characters (`: / ? * < > | " \`), syncback slugifies the filename (forbidden chars → `_`) and stores the real name in the `name` field. On forward sync, `name` overrides the filename-derived instance name. On collisions (e.g. `A/B` and `A_B` both slugify to `A_B`), `~1`, `~2` suffixes are appended and meta `name` fields distinguish them.
|
||||||
|
|
||||||
|
**Meta files CAN'T:** Set `$path`, have children.
|
||||||
|
|
||||||
## Model.json5 Format
|
## Model.json5 Format
|
||||||
|
|
||||||
Inline instance definition:
|
Inline instance definition. Like meta files, model files use **camelCase keys** (no `$` prefix).
|
||||||
|
|
||||||
```json5
|
```json5
|
||||||
{
|
{
|
||||||
"$className": "Tool",
|
"name": "What?Model", // Override filename-derived name (slugify support)
|
||||||
"$properties": { "CanBeDropped": false },
|
"className": "Tool",
|
||||||
"$attributes": { "Damage": 50 },
|
"id": "unique-id",
|
||||||
"Handle": {
|
"properties": { "CanBeDropped": false },
|
||||||
"$className": "Part",
|
"attributes": { "Damage": 50 },
|
||||||
"$properties": { "Size": [1, 1, 3] }
|
"children": {
|
||||||
|
"Handle": {
|
||||||
|
"className": "Part",
|
||||||
|
"properties": { "Size": [1, 1, 3] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Note:** Children are nested under a `"children"` key (not flattened like in ProjectNode).
|
||||||
|
|
||||||
## Property Formats
|
## Property Formats
|
||||||
|
|
||||||
### Shorthand (Recommended)
|
### Shorthand (Recommended)
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ syncback/
|
||||||
snapshot.rs - Convert instances → snapshots
|
snapshot.rs - Convert instances → snapshots
|
||||||
fs_snapshot.rs - Filesystem representation
|
fs_snapshot.rs - Filesystem representation
|
||||||
file_names.rs - Generate valid filenames
|
file_names.rs - Generate valid filenames
|
||||||
|
meta.rs - Meta file read/write helpers
|
||||||
property_filter.rs - Filter properties per syncback rules
|
property_filter.rs - Filter properties per syncback rules
|
||||||
ref_properties.rs - Handle Ref properties (cross-references)
|
ref_properties.rs - Handle Ref properties (cross-references)
|
||||||
hash/ - Content hashing for change detection
|
hash/ - Content hashing for change detection
|
||||||
|
|
@ -119,18 +120,33 @@ pub struct SyncbackRules {
|
||||||
/// Roblox tree paths to ignore (e.g., "ServerStorage/Secrets")
|
/// Roblox tree paths to ignore (e.g., "ServerStorage/Secrets")
|
||||||
pub ignore_trees: Vec<String>,
|
pub ignore_trees: Vec<String>,
|
||||||
|
|
||||||
/// Filesystem paths to ignore (e.g., "src/client/*")
|
/// Filesystem glob patterns to ignore (e.g., "src/client/*")
|
||||||
pub ignore_paths: Vec<PathBuf>,
|
pub ignore_paths: Vec<String>,
|
||||||
|
|
||||||
/// Properties to not sync back per class
|
/// Properties to not sync back per class
|
||||||
/// Example: {"BasePart": ["Color"], "Script": ["Disabled"]}
|
/// Example: {"BasePart": ["Color"], "Script": ["Disabled"]}
|
||||||
pub ignore_properties: HashMap<String, Vec<String>>,
|
pub ignore_properties: IndexMap<Ustr, Vec<Ustr>>,
|
||||||
|
|
||||||
|
/// Instance classes to skip entirely
|
||||||
|
pub ignore_classes: Vec<String>,
|
||||||
|
|
||||||
/// Whether to sync Workspace.CurrentCamera (default: false)
|
/// Whether to sync Workspace.CurrentCamera (default: false)
|
||||||
pub sync_current_camera: bool,
|
pub sync_current_camera: Option<bool>,
|
||||||
|
|
||||||
/// Whether to sync unscriptable properties (default: true)
|
/// Whether to sync unscriptable properties (default: true)
|
||||||
pub sync_unscriptable: bool,
|
pub sync_unscriptable: Option<bool>,
|
||||||
|
|
||||||
|
/// Whether to skip Ref properties (default: false)
|
||||||
|
pub ignore_referents: Option<bool>,
|
||||||
|
|
||||||
|
/// Whether /** in ignore paths also matches the directory itself
|
||||||
|
pub create_ignore_dir_paths: Option<bool>,
|
||||||
|
|
||||||
|
/// Override root-level ignoreHiddenServices for syncback
|
||||||
|
pub ignore_hidden_services: Option<bool>,
|
||||||
|
|
||||||
|
/// Warn on duplicate child names during syncback
|
||||||
|
pub warn_duplicate_names: Option<bool>,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -207,6 +223,7 @@ pub struct SyncbackSnapshot<'sync> {
|
||||||
pub new: Ref, // New tree instance (Roblox file)
|
pub new: Ref, // New tree instance (Roblox file)
|
||||||
pub path: PathBuf, // Target filesystem path
|
pub path: PathBuf, // Target filesystem path
|
||||||
pub middleware: Option<Middleware>, // How to write (Lua, Rbxm, etc.)
|
pub middleware: Option<Middleware>, // How to write (Lua, Rbxm, etc.)
|
||||||
|
pub needs_meta_name: bool, // Filesystem name differs from instance name
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -333,9 +350,9 @@ stats.record_duplicate_name(path, name)
|
||||||
stats.record_rbxm_fallback(path, reason)
|
stats.record_rbxm_fallback(path, reason)
|
||||||
stats.record_unknown_class(class_name)
|
stats.record_unknown_class(class_name)
|
||||||
stats.record_unknown_property(class, property)
|
stats.record_unknown_property(class, property)
|
||||||
stats.has_issues() // Any warnings?
|
stats.has_issues() // Any warnings?
|
||||||
stats.has_unknown_types() // Unknown classes/properties?
|
stats.has_unknown_types() // Unknown classes/properties?
|
||||||
stats.log_summary() // Log final report
|
stats.log_summary() // Log final report
|
||||||
```
|
```
|
||||||
|
|
||||||
**Logged at end of syncback:**
|
**Logged at end of syncback:**
|
||||||
|
|
@ -364,7 +381,7 @@ print('hello')
|
||||||
```json5
|
```json5
|
||||||
// src/MyScript.server.meta.json5 (for MyScript.server.luau)
|
// src/MyScript.server.meta.json5 (for MyScript.server.luau)
|
||||||
{
|
{
|
||||||
"$properties": {
|
"properties": {
|
||||||
"Disabled": false
|
"Disabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -407,12 +424,7 @@ Filesystem:
|
||||||
4. **Write permission** - Can't write to file
|
4. **Write permission** - Can't write to file
|
||||||
- Error reported, syncback continues for other files
|
- Error reported, syncback continues for other files
|
||||||
|
|
||||||
**Error collection:**
|
**Error propagation:** Errors are propagated via `anyhow::bail!`, not collected in stats. Fatal errors abort the syncback; non-fatal issues are logged as warnings.
|
||||||
```rust
|
|
||||||
stats.record_error(&format!("Failed to sync {}: {}", path.display(), error));
|
|
||||||
```
|
|
||||||
|
|
||||||
All errors collected and reported at end.
|
|
||||||
|
|
||||||
## Integration with Plugin
|
## Integration with Plugin
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -294,7 +294,8 @@ Prefer JSON5 over JSON for comments and trailing commas:
|
||||||
### Loading a Project (Rust)
|
### Loading a Project (Rust)
|
||||||
```rust
|
```rust
|
||||||
use librojo::Project;
|
use librojo::Project;
|
||||||
let project = Project::load_from_slice(path, contents)?;
|
let project = Project::load_fuzzy(&vfs, path)?;
|
||||||
|
// or: Project::load_exact(path)?;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building Snapshots (Rust)
|
### Building Snapshots (Rust)
|
||||||
|
|
@ -312,26 +313,29 @@ Reconciler.applyPatch(instanceMap, patch)
|
||||||
## File Organization
|
## File Organization
|
||||||
|
|
||||||
```
|
```
|
||||||
src/ - Rust server/CLI
|
src/ - Rust server/CLI
|
||||||
cli/ - Command implementations
|
cli/ - Command implementations
|
||||||
snapshot/ - Core snapshot system
|
snapshot/ - Core snapshot system
|
||||||
snapshot_middleware/ - File type handlers
|
snapshot_middleware/ - File type handlers
|
||||||
syncback/ - Roblox → filesystem
|
syncback/ - Roblox → filesystem
|
||||||
web/ - HTTP/WebSocket server
|
web/ - HTTP/WebSocket server
|
||||||
|
change_processor.rs - Two-way sync filesystem writer
|
||||||
|
|
||||||
plugin/src/ - Lua Studio plugin
|
plugin/src/ - Lua Studio plugin
|
||||||
App/ - Roact UI components
|
App/ - Roact UI components
|
||||||
Reconciler/ - Patch application logic
|
Reconciler/ - Patch application logic
|
||||||
Config.lua - Version and protocol config
|
ChangeBatcher/ - Two-way sync patch creation
|
||||||
|
Config.lua - Version and protocol config
|
||||||
|
|
||||||
rbx-dom/ - Git submodule (separate repo)
|
rbx-dom/ - Git submodule (separate repo)
|
||||||
rbx_binary/ - Binary format (rbxm/rbxl)
|
rbx_binary/ - Binary format (rbxm/rbxl)
|
||||||
rbx_xml/ - XML format (rbxmx/rbxlx)
|
rbx_xml/ - XML format (rbxmx/rbxlx)
|
||||||
rbx_dom_weak/ - Instance representation
|
rbx_dom_weak/ - Instance representation
|
||||||
rbx_reflection/ - Property metadata
|
rbx_reflection/ - Property metadata
|
||||||
|
rbx_reflection_database/ - Reflection DB
|
||||||
|
|
||||||
tests/ - Rust integration tests
|
tests/ - Rust integration tests
|
||||||
rojo-test/ - Test fixtures and utilities
|
rojo-test/ - Test fixtures and utilities
|
||||||
```
|
```
|
||||||
|
|
||||||
## Anti-Patterns to Avoid
|
## Anti-Patterns to Avoid
|
||||||
|
|
@ -349,7 +353,7 @@ rojo-test/ - Test fixtures and utilities
|
||||||
|
|
||||||
## Version Information
|
## Version Information
|
||||||
|
|
||||||
- **Protocol version:** 5 (`plugin/src/Config.lua`)
|
- **Protocol version:** 6 (`plugin/src/Config.lua`)
|
||||||
- **Server/Plugin must match:** MAJOR.MINOR (e.g., 7.7.x works with 7.7.y)
|
- **Server/Plugin must match:** MAJOR.MINOR (e.g., 7.7.x works with 7.7.y)
|
||||||
- **MSRV:** Rust 1.88
|
- **MSRV:** Rust 1.88
|
||||||
- **License:** MPL-2.0 (rbx-dom uses MIT)
|
- **License:** MPL-2.0 (rbx-dom uses MIT)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue