mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-22 21:41:37 +00:00
Enhance documentation and update syncback features
- Updated the `audit.md` documentation to clarify the syncback process and the structure of the `FsSnapshot`. - Revised the `atlas-api.mdc` to reflect changes in the Git metadata handling, including the introduction of a separate endpoint for fetching Git metadata. - Improved the `atlas-dedup.mdc` to include handling for the reserved name "init" during slugification. - Enhanced the `atlas-syncback.mdc` to document the new `SyncbackResult` structure and its integration with the syncback process. - Updated various sections across multiple documentation files to ensure consistency and clarity regarding the project's architecture and API endpoints. These changes aim to improve the overall understanding of the system and its components, facilitating better usage and integration.
This commit is contained in:
parent
652e3b2f72
commit
e8dc9b09b9
8 changed files with 284 additions and 120 deletions
|
|
@ -43,7 +43,7 @@ One-shot command: reads a Roblox binary/XML file, diffs it against the existing
|
|||
CLI Entry
|
||||
└── src/cli/syncback.rs Entry point. Loads project, reads .rbxl/.rbxm,
|
||||
creates ServeSession for existing tree, calls
|
||||
syncback_loop(), writes FsSnapshot to disk.
|
||||
syncback_loop(), writes result.fs_snapshot to disk.
|
||||
|
||||
Syncback Core
|
||||
├── src/syncback/mod.rs Main orchestration. syncback_loop() runs the full
|
||||
|
|
@ -60,8 +60,9 @@ Syncback Core
|
|||
│ .meta.json5 paths for scripts (strips .server/.client
|
||||
│ suffixes from stem).
|
||||
├── src/syncback/property_filter.rs Property filtering. filter_properties() removes Name,
|
||||
│ Parent, Source (scripts), Value (StringValue), Contents
|
||||
│ (LocalizationTable), Ref/UniqueId, unscriptable props.
|
||||
│ Parent, Source, Value, Contents via reflection DB
|
||||
│ (DoesNotSerialize). Explicitly filters Ref/UniqueId
|
||||
│ by variant type. Unscriptable props if configured.
|
||||
├── src/syncback/ref_properties.rs Ref property resolution. collect_referents() finds all
|
||||
│ Ref properties. link_referents() writes Rojo_Ref_*
|
||||
│ (path-based) and Rojo_Id attributes to instances.
|
||||
|
|
@ -289,10 +290,12 @@ Patch Application (Plugin)
|
|||
│ Used for confirmation dialog to show what will change.
|
||||
├── plugin/src/Reconciler/matching.lua Recursive change-count scoring algorithm. matchChildren()
|
||||
│ pairs virtual instances with Studio instances during
|
||||
│ hydration. Signature: (virtualChildren, studioChildren,
|
||||
│ virtualInstances). One of 3 parallel matching impls that
|
||||
│ must produce identical pairings (see also src/snapshot/
|
||||
│ matching.rs and src/syncback/matching.rs).
|
||||
│ hydration. Signature: (session, virtualChildren,
|
||||
│ studioChildren, virtualInstances, parentVirtualId?,
|
||||
│ parentStudioInstance?, _depth?). One of 3 parallel
|
||||
│ matching impls that must produce identical pairings
|
||||
│ (see also src/snapshot/matching.rs and src/syncback/
|
||||
│ matching.rs).
|
||||
├── plugin/src/Reconciler/trueEquals.lua Shared value equality. Fuzzy floats (epsilon 0.0001),
|
||||
│ Color3 via RGB ints, CFrame/Vector3 component-wise, NaN
|
||||
│ handling, nil/null-ref equivalence. Used by matching.lua
|
||||
|
|
@ -457,7 +460,7 @@ Plugin Shared Modules
|
|||
├── plugin/src/strict.lua Module export wrapper (strict mode enforcement).
|
||||
└── plugin/src/DiffUtil.lua Diff utilities for comparing instance data.
|
||||
|
||||
Integration Tests (tests/tests/)
|
||||
Integration Tests (entry point: tests/end_to_end.rs)
|
||||
├── tests/tests/build.rs Build tests (gen_build_tests! macro).
|
||||
├── tests/tests/serve.rs Forward-sync serve tests.
|
||||
├── tests/tests/connected_mode.rs Connected mode tests.
|
||||
|
|
@ -474,6 +477,7 @@ Integration Tests (tests/tests/)
|
|||
├── tests/tests/git_sync_defaults.rs Git-based sync default selection tests.
|
||||
│ Hash computation parity, default selection logic,
|
||||
│ auto-staging flow, no-git-repo fallback.
|
||||
├── tests/tests/scripts_only_pruning.rs Scripts-only mode pruning tests.
|
||||
└── tests/tests/matching_fixtures.rs Matching algorithm fixture tests across all three
|
||||
implementations (Rust syncback, Rust forward, Lua plugin).
|
||||
```
|
||||
|
|
@ -784,7 +788,7 @@ The matching algorithm exists in **3 parallel implementations** that must produc
|
|||
|
||||
The confirmation dialog uses git metadata for smart default selections (`PatchTree.lua` + `src/git.rs`). Verify:
|
||||
|
||||
- **Server-side `compute_git_metadata()`**: Does it correctly identify changed files vs HEAD? Does the two-phase tree lock (snapshot paths briefly, release before git subprocesses) prevent deadlocks?
|
||||
- **Server-side `compute_git_metadata()`**: Does it correctly identify changed files vs HEAD? Does the three-phase approach (1: run git commands for changed files with no lock, 2: briefly lock tree to resolve paths to Refs, 3: run `git cat-file --batch-check` with no lock) prevent deadlocks?
|
||||
- **Hash computation parity**: Server uses `compute_blob_sha1()` in `git.rs` and plugin uses `SHA1.luau` -- both must produce identical hashes for the same content using `SHA1("blob <byte_len>\0<content>")` git blob format.
|
||||
- **Default selection logic** (`PatchTree.lua`): File has no git changes → default "pull". File has git changes AND is script AND Studio Source matches committed hash → default "push". Otherwise → `nil` (user must decide). Verify these rules are applied correctly.
|
||||
- **Auto-staging (`stageIds`)**: After confirmation, `api.rs` receives `stageIds` and calls `git_add`. Push-accepted items are always staged. Pull-accepted items are only staged if auto-selected (`defaultSelection ~= nil`). Manually-chosen pulls are left unstaged. Is this flow correct?
|
||||
|
|
|
|||
|
|
@ -24,7 +24,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.
|
||||
|
||||
**Protocol Version:** 6 | Server/plugin MAJOR.MINOR must match
|
||||
**Protocol Version:** 6 | Plugin WebSocket requires exact version match; live syncback uses MAJOR.MINOR
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -47,12 +47,17 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
|||
| `/api/ref-patch/:id` | GET | Fix Ref properties after syncback |
|
||||
| `/api/syncback` | POST | Live syncback: full Roblox → filesystem sync |
|
||||
| `/api/socket/:cursor` | GET | WebSocket upgrade for live patch streaming |
|
||||
| `/api/git-metadata` | GET | Git metadata (changedIds, scriptCommittedHashes, newFileIds) |
|
||||
| `/api/validate-tree` | GET | Tree freshness check (test infra) |
|
||||
| `/api/mcp/stream` | GET | WebSocket upgrade for MCP plugin stream |
|
||||
| `/api/mcp/syncback` | POST | MCP syncback (runs inline, returns stats without server restart) |
|
||||
| `/mcp` | POST | MCP JSON-RPC endpoint (initialize, tools/list, tools/call) |
|
||||
|
||||
### Server Info Response (`GET /api/rojo`)
|
||||
|
||||
```json
|
||||
{
|
||||
"serverVersion": "7.7.0-rc.1",
|
||||
"serverVersion": "8.5.0",
|
||||
"serverFork": "atlas",
|
||||
"protocolVersion": 6,
|
||||
"sessionId": "uuid",
|
||||
|
|
@ -61,24 +66,36 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
|
|||
"unexpectedPlaceIds": [111111],
|
||||
"rootInstanceId": "00000000-0000-0000-0000-000000000000",
|
||||
"syncSourceOnly": true,
|
||||
"syncScriptsOnly": false,
|
||||
"visibleServices": ["Workspace", "ServerScriptService"],
|
||||
"placeId": 123456,
|
||||
"gameId": 789012,
|
||||
"ignoreHiddenServices": true,
|
||||
"gitMetadata": {
|
||||
"changedIds": ["ref-1", "ref-2"],
|
||||
"scriptCommittedHashes": {
|
||||
"ref-1": ["a1b2c3...40-char-sha1-hex", "d4e5f6...staged-sha1"]
|
||||
}
|
||||
}
|
||||
"gitMetadata": null
|
||||
}
|
||||
```
|
||||
|
||||
**`gitMetadata`** is always `null` in the `/api/rojo` response. Git metadata is fetched separately via `GET /api/git-metadata`.
|
||||
|
||||
### Git Metadata Endpoint (`GET /api/git-metadata`)
|
||||
|
||||
Returns `Option<GitMetadata>` (msgpack). `null` when not in a git repo. Called by the plugin after connect and on debounced refresh.
|
||||
|
||||
```json
|
||||
{
|
||||
"changedIds": ["ref-1", "ref-2"],
|
||||
"scriptCommittedHashes": {
|
||||
"ref-1": ["a1b2c3...40-char-sha1-hex", "d4e5f6...staged-sha1"]
|
||||
},
|
||||
"newFileIds": ["ref-3"]
|
||||
}
|
||||
```
|
||||
|
||||
**`gitMetadata`** is `null` when the project is not in a git repo. Contains:
|
||||
- `changedIds`: Instance Refs whose backing files have changes relative to HEAD (staged + unstaged + untracked)
|
||||
- `scriptCommittedHashes`: For changed script instances, 1-2 SHA1 hashes (HEAD version and staged version if different) computed using the git blob format (`"blob <size>\0<content>"`). Plugin checks if Studio `script.Source` matches ANY hash to default "Atlas".
|
||||
- `newFileIds`: Instance Refs whose backing files have never been committed. Used by the plugin to default new-file additions to "push".
|
||||
|
||||
Recomputed fresh on each `GET /api/rojo` connect. Uses a two-phase tree lock: snapshot path mappings briefly, then release before running git subprocesses.
|
||||
Uses a three-phase approach: (1) run git commands for changed files scoped to project prefixes (no lock), (2) briefly lock tree to resolve paths to Refs and class names, (3) run `git cat-file --batch-check` for blob hashes (no lock). 10-second timeout. Uses `project_prefixes` (relevant `$path` roots relative to repo root) to scope `git status`/`git diff` to project directories.
|
||||
|
||||
### Write Request (`POST /api/write`)
|
||||
|
||||
|
|
@ -195,29 +212,36 @@ Serve loop restarts (new session, ready for connections)
|
|||
|
||||
1. Connect → Receive `ServerInfo` handshake → Send `{type = "Subscribe"}` → Receive patches
|
||||
|
||||
### Patch Message (`ApplyPatch`)
|
||||
### Patch Message (SocketPacket)
|
||||
|
||||
```lua
|
||||
{
|
||||
type = "ApplyPatch",
|
||||
sessionId = "uuid",
|
||||
patch = {
|
||||
packetType = "messages",
|
||||
body = {
|
||||
messageCursor = 5,
|
||||
messages = {
|
||||
{
|
||||
removed = {"id-1", "id-2"},
|
||||
added = {
|
||||
["id-3"] = {
|
||||
id = "id-3", className = "Folder", name = "New",
|
||||
properties = {}, metadata = { ignoreUnknownInstances = false },
|
||||
children = {"child-1"},
|
||||
Id = "id-3", ClassName = "Folder", Name = "New",
|
||||
Properties = {}, Metadata = { ignoreUnknownInstances = false },
|
||||
Children = {"child-1"}, Parent = "parent-id",
|
||||
},
|
||||
},
|
||||
updated = {
|
||||
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Format:** MessagePack (~30% smaller than JSON, ~2x faster)
|
||||
**Note:** `added` Instance fields use **PascalCase** (`Id`, `Parent`, `Name`, `ClassName`, `Properties`, `Children`, `Metadata`). `updated` InstanceUpdate fields use **camelCase** (`id`, `changedName`, `changedClassName`, `changedProperties`, `changedMetadata`).
|
||||
|
||||
**Format:** MessagePack (~30% smaller than JSON, ~2x faster). Plugin handler: `["messages"] = function(messagesPacket)`, iterates `messagesPacket.messages` and merges into a combined patch via `PatchSet.assign`.
|
||||
|
||||
## Server ServeSession (`src/serve_session.rs`)
|
||||
|
||||
|
|
@ -228,16 +252,19 @@ pub struct ServeSession {
|
|||
message_queue: Arc<MessageQueue<AppliedPatchSet>>,
|
||||
root_project: Project,
|
||||
vfs: Arc<Vfs>,
|
||||
change_processor: ChangeProcessor,
|
||||
change_processor: Option<ChangeProcessor>,
|
||||
start_time: Instant,
|
||||
tree_mutation_sender: Sender<PatchSet>,
|
||||
suppressed_paths: Arc<Mutex<HashMap<PathBuf, (usize, usize)>>>,
|
||||
tree_mutation_sender: Option<Sender<PatchSet>>,
|
||||
suppressed_paths: Option<Arc<Mutex<HashMap<PathBuf, (usize, usize)>>>>,
|
||||
ref_path_index: Option<Arc<Mutex<RefPathIndex>>>,
|
||||
git_repo_root: Option<PathBuf>,
|
||||
initial_head_commit: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
**Lifecycle:** Create → Watch filesystem → Generate patches on changes → Broadcast to plugins → Shutdown
|
||||
|
||||
**Message Queue:** Batches changes (~100ms window), cursor system for reconnect recovery
|
||||
**Message Queue:** Batches changes (~200ms window), cursor system for reconnect recovery
|
||||
|
||||
**WebSocket loop:**
|
||||
```rust
|
||||
|
|
@ -256,11 +283,16 @@ ServeSession = {
|
|||
__status = "NotStarted", -- NotStarted → Connecting → Connected → Disconnected
|
||||
__apiContext = ApiContext,
|
||||
__twoWaySync = false,
|
||||
__syncSourceOnly = false,
|
||||
__syncScriptsOnly = false,
|
||||
__reconciler = Reconciler,
|
||||
__instanceMap = InstanceMap,
|
||||
__changeBatcher = ChangeBatcher,
|
||||
__confirmingPatch = nil,
|
||||
__isConfirming = false, -- Prevents stacking dialogs, changes during review
|
||||
__serverInfo = nil,
|
||||
__applyingPatch = false,
|
||||
__oneShotSyncDone = false,
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -367,6 +399,8 @@ InstanceMap = {
|
|||
pausedUpdateInstances = {}, -- Set of instances to ignore changes from
|
||||
instancesToSignal = {}, -- Instance → RBXScriptConnection(s)
|
||||
onInstanceChanged = fn, -- Callback: (instance, propertyName) → void
|
||||
onInstanceInserted = fn, -- Callback: when a new instance is inserted (deferred Ref resolution)
|
||||
scriptsOnlyTempIds = {}, -- Temp IDs for scripts-only mode new script additions
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -421,6 +455,7 @@ ChangeBatcher = {
|
|||
__pendingPropertyChanges = {}, -- Instance → { PropertyName → true }
|
||||
__pendingUnresolvedRefs = {}, -- targetInstance → [{sourceInstance, propertyName}]
|
||||
__syncSourceOnly = false, -- Only sync Source property
|
||||
__syncScriptsOnly = false, -- Only sync LuaSourceContainer instances
|
||||
__paused = false, -- Pause during confirmation
|
||||
__renderSteppedConnection = conn, -- Frame loop connection
|
||||
__instanceMap = instanceMap,
|
||||
|
|
@ -737,7 +772,7 @@ Only **Updated** items get defaults. Added/Removed items remain `nil`.
|
|||
|
||||
The plugin uses a recursive change-count scoring algorithm (implemented in `matching.lua`) to pair incoming virtual instances with existing Studio instances during hydration.
|
||||
|
||||
**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, virtualInstances)`
|
||||
**Entry point:** `Matching.matchChildren(session, virtualChildren, studioChildren, virtualInstances, parentVirtualId?, parentStudioInstance?, _depth?)`
|
||||
|
||||
**Algorithm:**
|
||||
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
|
||||
|
|
@ -781,7 +816,7 @@ RUST_LOG=rojo::web=debug,rojo::serve_session=debug atlas serve
|
|||
Settings:set("logLevel", "Trace")
|
||||
```
|
||||
|
||||
Browser: `http://localhost:34872` → Network tab → WebSocket messages
|
||||
Browser: `http://localhost:34873` → Network tab → WebSocket messages
|
||||
|
||||
## Key Files
|
||||
|
||||
|
|
@ -794,4 +829,33 @@ Browser: `http://localhost:34872` → Network tab → WebSocket messages
|
|||
| `src/cli/serve.rs` - Live syncback handler | `plugin/src/Reconciler/` - Patch application |
|
||||
| `src/change_processor.rs` - FS writer | `plugin/src/Reconciler/matching.lua` - Matching |
|
||||
| `src/rojo_ref.rs` - Ref paths | `plugin/src/PatchTree.lua` - Default selections |
|
||||
| `src/git.rs` - Git integration | `plugin/src/App/init.lua` - Live syncback UI |
|
||||
| `src/git.rs` - Git integration (CLI-based) | `plugin/src/App/init.lua` - Live syncback UI |
|
||||
| `src/web/mcp.rs` - MCP JSON-RPC | `plugin/src/McpStream.lua` - MCP stream client |
|
||||
| `src/logging.rs` - File logging | `plugin/src/McpTools/` - Studio tool dispatch |
|
||||
|
||||
## MCP Integration
|
||||
|
||||
AI agent-facing JSON-RPC layer exposed during `atlas serve`. Agents call `POST /mcp`; the server handles some tools directly and dispatches others to the plugin over the MCP stream WebSocket.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
AI Agent → POST /mcp (JSON-RPC) → Server (McpState) → ws://host:port/api/mcp/stream → Plugin
|
||||
```
|
||||
|
||||
**McpState** (`src/web/mcp.rs`): Shared state for agent-plugin communication. Uses a watch channel (`command_tx`/`command_rx`) for commands and a oneshot channel (`result_tx`) for results. Tracks `plugin_stream_connected` and `plugin_config`.
|
||||
|
||||
**MCP tools:**
|
||||
|
||||
| Tool | Handled by | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `atlas_sync` | Server | Snapshot diff, sends sync command to plugin |
|
||||
| `get_script` | Server | Resolves id/fsPath to script, returns Source |
|
||||
| `syncback` | Plugin | Full Roblox → filesystem sync via `/api/mcp/syncback` |
|
||||
| `run_code` | Plugin | Execute Luau code in Studio |
|
||||
| `insert_model` | Plugin | Insert model from Creator Store |
|
||||
| `get_console_output` | Plugin | Return console output |
|
||||
| `get_studio_mode` | Plugin | Return Studio mode (Edit/Play) |
|
||||
| `start_stop_play` | Plugin | Start/stop Play mode |
|
||||
| `run_script_in_play_mode` | Plugin | Run code in a play session |
|
||||
|
||||
**Plugin side:** `McpStream.lua` connects to `ws://host:port/api/mcp/stream`, sends greeting with plugin config, handles incoming commands, and forwards results back. `McpTools/init.luau` dispatches Studio-side tools to individual handler files.
|
||||
|
|
|
|||
|
|
@ -71,7 +71,11 @@ Pure, stateless. Replaces forbidden filesystem characters with `_`.
|
|||
const FORBIDDEN_CHARS: [char; 9] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\'];
|
||||
const SLUGIFY_CHARS: [char; 10] = ['<', '>', ':', '"', '/', '|', '?', '*', '\\', '~'];
|
||||
const DANGEROUS_SUFFIXES: [&str; 8] = [".server", ".client", ".plugin", ".local", ".legacy", ".meta", ".model", ".project"];
|
||||
const INVALID_WINDOWS_NAMES: [&str; 22] = ["CON", "PRN", "AUX", "NUL", "COM1".."COM9", "LPT1".."LPT9"];
|
||||
const INVALID_WINDOWS_NAMES: [&str; 22] = [
|
||||
"CON", "PRN", "AUX", "NUL",
|
||||
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
|
||||
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9",
|
||||
];
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
|
@ -79,12 +83,13 @@ const INVALID_WINDOWS_NAMES: [&str; 22] = ["CON", "PRN", "AUX", "NUL", "COM1".."
|
|||
- Strips leading/trailing spaces and dots
|
||||
- Neutralizes Windows reserved names (`CON` → `CON_`)
|
||||
- Neutralizes dangerous suffixes (`.server` etc.) that would be misinterpreted as middleware extensions
|
||||
- Neutralizes reserved name `"init"` (case-insensitive) → `"init_"` to avoid collision with init file convention
|
||||
|
||||
**Key invariant:** `slugify_name` is idempotent. Applying it twice produces the same result.
|
||||
|
||||
### name_needs_slugify
|
||||
|
||||
Returns `true` if the name requires slugification. Checks for: `SLUGIFY_CHARS`, control characters, leading/trailing spaces, trailing dots, dangerous suffixes (`.server` etc.), Windows reserved names (`CON` etc.), and empty string. Used to decide whether metadata `name` field is needed.
|
||||
Returns `true` if the name requires slugification. Checks for: `SLUGIFY_CHARS`, control characters, leading/trailing spaces, trailing dots, dangerous suffixes (`.server` etc.), Windows reserved names (`CON` etc.), reserved name `"init"` (case-insensitive), and empty string. Used to decide whether metadata `name` field is needed.
|
||||
|
||||
### deduplicate_name_with_ext
|
||||
|
||||
|
|
@ -193,6 +198,7 @@ pub fn match_children(
|
|||
new_dom: &WeakDom, old_dom: &WeakDom,
|
||||
new_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
old_hashes: Option<&HashMap<Ref, Hash>>,
|
||||
session: &MatchingSession,
|
||||
) -> MatchResult
|
||||
```
|
||||
|
||||
|
|
@ -202,16 +208,21 @@ pub fn match_forward(
|
|||
snapshot_children: Vec<InstanceSnapshot>,
|
||||
tree_children: &[Ref],
|
||||
tree: &RojoTree,
|
||||
session: &MatchingSession,
|
||||
) -> ForwardMatchResult
|
||||
```
|
||||
|
||||
**Lua plugin:**
|
||||
```lua
|
||||
function Matching.matchChildren(
|
||||
virtualChildren: { string }, -- array of virtual instance IDs
|
||||
studioChildren: { Instance }, -- array of Studio Instance objects
|
||||
virtualInstances: { [string]: any } -- map of virtual ID → virtual instance data
|
||||
) -> { matched, unmatchedVirtual, unmatchedStudio }
|
||||
session: MatchingSession,
|
||||
virtualChildren: { string },
|
||||
studioChildren: { Instance },
|
||||
virtualInstances: VirtualInstances,
|
||||
parentVirtualId: string?,
|
||||
parentStudioInstance: Instance?,
|
||||
_depth: number?
|
||||
) -> MatchResult
|
||||
```
|
||||
|
||||
### Algorithm (All Three Implementations)
|
||||
|
|
|
|||
|
|
@ -7,14 +7,14 @@ alwaysApply: false
|
|||
|
||||
Built with Roact (React-like), handles WebSocket sync with server, patch application to Studio, and UI.
|
||||
|
||||
**Entry:** `plugin/src/init.server.lua` | **Protocol:** WebSocket v6 | **Must match server MAJOR.MINOR**
|
||||
**Entry:** `plugin/src/init.server.lua` | **Protocol:** WebSocket v6 | **Must match server exact version**
|
||||
|
||||
## Architecture
|
||||
|
||||
### Key Components
|
||||
|
||||
**1. UI Layer** (`App/`)
|
||||
- **init.lua** - Root component, manages connection state, auto-reconnect, sync reminders, playtest server auto-connect. Also renders `SyncbackConfirm` floating dialog (StudioPluginGui with `isEphemeral=true`, not a StatusPage file). Contains `performSyncback()` and `SYNCBACK_SERVICES` constant.
|
||||
- **init.lua** - Root component, manages connection state, auto-reconnect, playtest server auto-connect. Also renders `SyncbackConfirm` floating dialog (StudioPluginGui with `isEphemeral=true`, not a StatusPage file). Contains `performSyncback()` and `SYNCBACK_SERVICES` constant. Creates `McpStream` for MCP integration and handles `startMcpSync`, `handleMcpGetScript`, `collectAndPostSyncback` (to `/api/mcp/syncback`), dispatches tool calls via `McpTools.dispatch`. Note: sync reminder polling is currently disabled (returns immediately).
|
||||
- **Theme.lua** - Studio-matched color palette (auto light/dark)
|
||||
- **Page.lua** - Page wrapper component
|
||||
- **Components/** - Reusable UI components
|
||||
|
|
@ -25,9 +25,9 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
|||
- `Studio/` - Studio API wrappers (`StudioPluginAction`, `StudioPluginContext`, `StudioPluginGui`, `StudioToggleButton`, `StudioToolbarContext`, `StudioToolbar`)
|
||||
- General: `Header`, `TextInput`, `TextButton`, `Checkbox`, `Dropdown`, `Tag`, `IconButton`, `Tooltip`, `TouchRipple`, `VirtualScroller`, `ScrollingFrame`, `Spinner`, `BorderedContainer`, `EditableImage`, `SlicedImage`, `ClassIcon`
|
||||
- **StatusPages/** - Main UI pages
|
||||
- `NotConnected.lua` - Connection form (host/port input), "Syncback" button (Danger style)
|
||||
- `NotConnected.lua` - Connection form (host/port input), "Sync" button (Primary style), "Syncback" button (Danger style)
|
||||
- `Connecting.lua` - Loading screen
|
||||
- `Confirming.lua` - Patch confirmation dialog with push/pull/ignore per-node selections
|
||||
- `Confirming.lua` - Patch confirmation dialog with push/pull/ignore per-node selections. Accept button style=Primary/Neutral based on allSelected; text=Accept/OK based on hasChanges. Abort button only shown when hasChanges.
|
||||
- `Connected.lua` - Status display with patch history
|
||||
- `Error.lua` - Error display
|
||||
- `Settings/` - Settings UI (`init.lua`, `Setting.lua`)
|
||||
|
|
@ -39,8 +39,16 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
|||
- Protocol version checking
|
||||
- Confirmation flow with push/pull/ignore
|
||||
- One-shot mode support
|
||||
- `ApiContext:getGitMetadata()` fetches git metadata from `GET /api/git-metadata` (separate from `/api/rojo`)
|
||||
|
||||
**3. Reconciler** (`Reconciler/`)
|
||||
**3. MCP Integration** (`McpStream.lua`, `McpTools/`)
|
||||
- **McpStream.lua** - WebSocket client to `ws://host:port/api/mcp/stream`. `RECONNECT_INTERVAL = 3` seconds. Sends greeting with plugin config (`twoWaySync`, `oneShotSync`, `confirmationBehavior`, `placeId`). Handles commands: `sync`, `getScript`, `syncback`, and passthrough tools. Forwards Roblox console output via `ConsoleOutput.onLogMessage`.
|
||||
- **McpTools/** - Studio-side MCP tool dispatch
|
||||
- `init.luau` - Dispatcher for: `run_code`, `insert_model`, `get_console_output`, `get_studio_mode`, `start_stop_play`, `run_script_in_play_mode`
|
||||
- Tool handlers: `RunCode.luau`, `InsertModel.luau`, `GetConsoleOutput.luau`, `GetStudioMode.luau`, `StartStopPlay.luau`, `RunScriptInPlayMode.luau`
|
||||
- `Utils/` - `ConsoleOutput.luau`, `StudioModeState.luau`, `GameStopUtil.luau`, `DeepCopy.luau`
|
||||
|
||||
**4. Reconciler** (`Reconciler/`)
|
||||
- **applyPatch.lua** - Applies server patches to Studio tree
|
||||
- **reify.lua** - Creates instances from snapshots (parent last)
|
||||
- **hydrate.lua** - Updates InstanceMap with existing Studio instances
|
||||
|
|
@ -62,14 +70,14 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
|||
- **encodeProperty.lua** - Single property value encoding via RbxDom
|
||||
- **propertyFilter.lua** - Property type filtering (only `UniqueId` blocked; `Ref` is allowed)
|
||||
|
||||
**5. State Management**
|
||||
**6. State Management**
|
||||
- **Settings.lua** - Persistent plugin settings (prefixed `Atlas_` in plugin store)
|
||||
- **InstanceMap.lua** - Bidirectional Server ID <-> Studio Instance mapping with change detection
|
||||
- **PatchSet.lua** / **PatchTree.lua** - Patch representation and visualization tree (PatchTree.build accepts optional `gitMetadata` for smart default selections)
|
||||
- **Types.lua** - Type definitions and runtime validation
|
||||
- **ChangeMetadata.lua** - Change metadata tracking
|
||||
|
||||
**6. Utilities**
|
||||
**7. Utilities**
|
||||
- **Timer.lua** - Performance timing
|
||||
- **DiffUtil.lua** - Diff utilities
|
||||
- **Version.lua** - Version parsing and comparison
|
||||
|
|
@ -84,10 +92,10 @@ Built with Roact (React-like), handles WebSocket sync with server, patch applica
|
|||
- **strict.lua** - Strict module wrapper
|
||||
- **invariant.lua** - Assertion utilities
|
||||
|
||||
**7. Testing**
|
||||
**8. Testing**
|
||||
- Unit tests: `.spec.lua` files (TestEZ) throughout all modules
|
||||
- Integration tests: `integration/` directory (`confirmBehavior`, `oneShotSync`, `syncflow`, `twoWaySync`)
|
||||
- Stress tests: `.stress.spec.lua` files (applyPatch, diff, reify, encodeInstance, PatchSet)
|
||||
- Stress tests: `.stress.spec.lua` files (applyPatch, diff, reify, encodeInstance, PatchSet, InstanceMap)
|
||||
- Chaos tests: `chaos/` directory (`stressPatterns.spec.lua`)
|
||||
- Test utilities: `testUtils/` (`MockApiContext`, `PatchGenerator`, `LargeTreeGenerator`)
|
||||
- Test runner: `runTests.lua`
|
||||
|
|
@ -109,7 +117,8 @@ The plugin is built from `plugin.project.json` at the repo root:
|
|||
"Fmt": { "$path": "plugin/fmt" },
|
||||
"RbxDom": { "$path": "plugin/rbx_dom_lua" }
|
||||
},
|
||||
"Version": { "$path": "plugin/Version.txt" }
|
||||
"Version": { "$path": "plugin/Version.txt" },
|
||||
"UploadDetails": { "$path": "plugin/UploadDetails.json" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -123,7 +132,7 @@ Vendored packages live under `plugin/Packages/`. Custom modules (`Log`, `Http`,
|
|||
isDevBuild = false, -- Detected from ROJO_DEV_BUILD instance
|
||||
codename = "Epiphany",
|
||||
version = { major, minor, patch, metadata }, -- Parsed from Version.txt
|
||||
expectedServerVersionString = "7.7 or newer",
|
||||
expectedServerVersionString = "8.5.0", -- Dynamic from Version.txt (e.g. "%d.%d.%d")
|
||||
protocolVersion = 6, -- MUST match server
|
||||
defaultHost = "localhost",
|
||||
defaultPort = "34873",
|
||||
|
|
@ -170,41 +179,51 @@ Persistent settings stored with `Atlas_` prefix in Studio plugin store:
|
|||
3. Plugin sends: `{type = "Subscribe"}`
|
||||
4. Server sends patches on filesystem changes (MessagePack encoded)
|
||||
|
||||
**Patch structure:**
|
||||
**Packet structure (SocketPacket):**
|
||||
```lua
|
||||
{
|
||||
type = "ApplyPatch",
|
||||
sessionId = "uuid",
|
||||
patch = {
|
||||
packetType = "messages",
|
||||
body = {
|
||||
messageCursor = 5,
|
||||
messages = {
|
||||
{
|
||||
removed = {"id-1", "id-2"},
|
||||
added = {
|
||||
["id-3"] = {
|
||||
id = "id-3", className = "Folder", name = "New",
|
||||
properties = {}, metadata = { ignoreUnknownInstances = false },
|
||||
children = {"child-1"},
|
||||
Id = "id-3", ClassName = "Folder", Name = "New",
|
||||
Properties = {}, Metadata = { ignoreUnknownInstances = false },
|
||||
Children = {"child-1"}, Parent = "parent-id",
|
||||
},
|
||||
},
|
||||
updated = {
|
||||
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** `added` Instance fields use **PascalCase**; `updated` InstanceUpdate fields use **camelCase**. Plugin handler: `["messages"] = function(messagesPacket)`, iterates `messagesPacket.messages`.
|
||||
|
||||
## ServeSession (`ServeSession.lua`)
|
||||
|
||||
Manages the lifecycle of a connection to the server.
|
||||
|
||||
**Status flow:** `NotStarted` -> `Connecting` -> `Connected` -> `Disconnected`
|
||||
|
||||
**Key fields:** `__syncSourceOnly`, `__syncScriptsOnly` (from serverInfo), `__serverInfo`, `__applyingPatch`, `__oneShotSyncDone`, `enableSyncFallback`
|
||||
|
||||
**Key methods:**
|
||||
- `new(options)` - Creates session with `ApiContext` and `twoWaySync` flag
|
||||
- `start()` - Connects, computes initial patch, shows confirmation
|
||||
- `start()` - Connects, fetches git metadata via `apiContext:getGitMetadata()`, computes initial patch, shows confirmation
|
||||
- `stop()` - Disconnects and cleans up
|
||||
- `__onWebSocketMessage()` - Handles incoming patches
|
||||
- `__onWebSocketMessage()` - Handles incoming patches (packetType "messages")
|
||||
- `__applyPatch()` - Applies patches via Reconciler
|
||||
- `__confirmAndApplyInitialPatch()` - Handles user confirmation with push/pull/ignore
|
||||
- `__computeInitialPatch()` - Computes catch-up patch
|
||||
- `__connectScriptsOnlyWatchers()` - Connects `DescendantAdded` on visible services for scripts-only mode
|
||||
- `hookPrecommit()` / `hookPostcommit()` - Lifecycle hooks
|
||||
|
||||
**Confirmation flow:**
|
||||
|
|
@ -240,7 +259,7 @@ Manages the lifecycle of a connection to the server.
|
|||
|
||||
Pairs incoming virtual instances with existing Studio instances during hydration.
|
||||
|
||||
**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, virtualInstances)`
|
||||
**Entry point:** `Matching.matchChildren(session, virtualChildren, studioChildren, virtualInstances, parentVirtualId?, parentStudioInstance?, _depth?)`
|
||||
|
||||
**Algorithm:**
|
||||
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@ alwaysApply: true
|
|||
|-------|------|----------|-------------|
|
||||
| `name` | string | Yes* | Instance name (*optional if `default.project.json5`) |
|
||||
| `tree` | ProjectNode | Yes | Root instance tree structure |
|
||||
| `servePort` | number | No | Port for `atlas serve` (default: 34872) |
|
||||
| `$schema` | string | No | JSON schema URL for editor validation (preserved during syncback roundtrip) |
|
||||
| `servePort` | number | No | Port for `atlas serve` (default: 34873) |
|
||||
| `serveAddress` | string | No | Address to bind (default: `"127.0.0.1"`) |
|
||||
| `servePlaceIds` | number[] | No | Only allow sync to these place IDs |
|
||||
| `blockedPlaceIds` | number[] | No | Block sync to these place IDs |
|
||||
|
|
@ -34,7 +35,7 @@ alwaysApply: true
|
|||
"$schema": "https://raw.githubusercontent.com/UserGeneratedLLC/vscode-rojo/master/schemas/project.schema.json",
|
||||
"name": "MyProject",
|
||||
"tree": { /* ProjectNode */ },
|
||||
"servePort": 34872,
|
||||
"servePort": 34873,
|
||||
"serveAddress": "0.0.0.0",
|
||||
"servePlaceIds": [123456],
|
||||
"blockedPlaceIds": [111111],
|
||||
|
|
@ -195,7 +196,7 @@ Configure `syncbackRules` object for Roblox → filesystem sync:
|
|||
| `syncCurrentCamera` | bool | `false` | Whether to sync `Workspace.CurrentCamera` |
|
||||
| `syncUnscriptable` | bool | `false` | Whether to sync unscriptable properties |
|
||||
| `ignoreReferents` | bool | `false` | Whether to skip Ref properties (e.g., `Model.PrimaryPart`) |
|
||||
| `createIgnoreDirPaths` | bool | `false` | Whether `/**` in ignore paths also matches the directory itself |
|
||||
| `createIgnoreDirPaths` | bool | `true` | 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 |
|
||||
|
||||
|
|
@ -249,10 +250,12 @@ Add metadata to file-based instances. Meta files use **camelCase keys** (no `$`
|
|||
|
||||
**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`), `~2`, `~3` suffixes are appended and meta `name` fields distinguish them.
|
||||
**`name` field (slugify support):** When an instance name contains forbidden filesystem characters (`: / ? * < > | " \`), or is the reserved name `"init"` (case-insensitive, slugified to `"init_"`), 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`), `~2`, `~3` suffixes are appended and meta `name` fields distinguish them.
|
||||
|
||||
**`Rojo_Ref_*` attributes:** Meta files may also contain `Rojo_Ref_PropertyName` attributes written by syncback/two-way sync for Ref property linking. These are managed automatically and should not be hand-edited.
|
||||
|
||||
Meta files also support a `$schema` field (preserved during syncback roundtrip).
|
||||
|
||||
**Meta files CAN'T:** Set `$path`, have children.
|
||||
|
||||
## Model.json5 Format
|
||||
|
|
@ -266,16 +269,17 @@ Inline instance definition. Like meta files, model files use **camelCase keys**
|
|||
"id": "unique-id",
|
||||
"properties": { "CanBeDropped": false },
|
||||
"attributes": { "Damage": 50 },
|
||||
"children": {
|
||||
"Handle": {
|
||||
"children": [
|
||||
{
|
||||
"name": "Handle",
|
||||
"className": "Part",
|
||||
"properties": { "Size": [1, 1, 3] }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Children are nested under a `"children"` key (not flattened like in ProjectNode).
|
||||
**Note:** `children` is an **array** of child objects (each with a `name` field), not a map/object keyed by name. This differs from ProjectNode where children are flattened as keys.
|
||||
|
||||
## Property Formats
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ Transforms files → `InstanceSnapshot`. Main entry: `snapshot_from_vfs()`
|
|||
| `*.lua` | `lua.rs` | ModuleScript | Legacy |
|
||||
| `*.server.lua` | `lua.rs` | Script (RunContext.Legacy) | Legacy |
|
||||
| `*.client.lua` | `lua.rs` | LocalScript | Legacy |
|
||||
| `*.plugin.lua` | `lua.rs` | Script (RunContext.Plugin) | Legacy |
|
||||
| `*.rbxm` / `*.rbxmx` | `rbxm.rs` / `rbxmx.rs` | Binary/XML model tree | |
|
||||
| `*.model.json5` | `json_model.rs` | JSON instance | Preferred |
|
||||
| `*.model.json` | `json_model.rs` | JSON instance | Legacy |
|
||||
|
|
@ -84,6 +85,10 @@ Instance names with forbidden filesystem chars (`<>:"/\|?*~`) are handled by:
|
|||
- `upload.rs` - Upload to Roblox.com
|
||||
- `sourcemap.rs` - Generate sourcemaps
|
||||
- `syncback.rs` - Roblox → filesystem
|
||||
- `clone.rs` - Create project from Roblox place (init + syncback --download)
|
||||
- `completions.rs` - Shell completions (bash, zsh, fish, PowerShell, elvish) via `clap_complete`
|
||||
- `cursor.rs` - Open project directory in Cursor IDE
|
||||
- `studio.rs` - Open project in Roblox Studio via URL
|
||||
- `init.rs` / `fmt_project.rs` / `doc.rs` / `plugin.rs`
|
||||
|
||||
### 4. Syncback (`src/syncback/`)
|
||||
|
|
@ -102,9 +107,11 @@ Reverse: Roblox file → filesystem
|
|||
|
||||
### 5. Web Server (`src/web/`)
|
||||
|
||||
- `api.rs` - HTTP/WebSocket endpoints, including `POST /api/syncback` handler (`handle_api_syncback`)
|
||||
- `interface.rs` - Wire types: patches, `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload`, `ServerExitReason`
|
||||
- `api.rs` - HTTP/WebSocket endpoints, including `POST /api/syncback`, `GET /api/git-metadata`, MCP stream/syncback handlers
|
||||
- `interface.rs` - Wire types: patches, `SyncbackRequest`, `ServiceChunk`, `SyncbackPayload`, `ServerExitReason`, `GitMetadata`
|
||||
- `mod.rs` - `SyncbackSignal` (Mutex + Notify pattern), `LiveServer` (uses `tokio::select!` to wait for connections or syncback signal)
|
||||
- `mcp.rs` - MCP JSON-RPC endpoint (`POST /mcp`), `McpState`, tool definitions and handlers
|
||||
- `mcp_docs/` - MCP tool description markdown files
|
||||
- Protocol version enforcement
|
||||
|
||||
### 6. Project System (`src/project.rs`)
|
||||
|
|
@ -155,6 +162,9 @@ pub struct SyncbackRules {
|
|||
sync_current_camera: Option<bool>, // Default: false
|
||||
sync_unscriptable: Option<bool>, // Default: false
|
||||
ignore_referents: Option<bool>, // Skip Ref properties, default: false
|
||||
create_ignore_dir_paths: Option<bool>, // Default: true
|
||||
ignore_hidden_services: Option<bool>, // Override root-level ignoreHiddenServices
|
||||
warn_duplicate_names: Option<bool>, // Warn on duplicate child names
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -221,26 +231,28 @@ Review: `cargo insta review`
|
|||
|
||||
## Web API
|
||||
|
||||
**Default port:** 34873
|
||||
|
||||
**HTTP:**
|
||||
- `GET /` - Browser UI
|
||||
- `GET /api/rojo` - Server info (version, project name, protocol)
|
||||
- `POST /api/syncback` - Live syncback: full Roblox → filesystem sync (validates protocol, version, place ID)
|
||||
- `GET /api/git-metadata` - Git metadata (changedIds, scriptCommittedHashes, newFileIds)
|
||||
- `POST /api/syncback` - Live syncback: full Roblox → filesystem sync
|
||||
- `POST /api/mcp/syncback` - MCP syncback (inline, returns stats)
|
||||
- `POST /mcp` - MCP JSON-RPC endpoint
|
||||
|
||||
**WebSocket:** `ws://host:port/api/rojo`
|
||||
**WebSocket:** `ws://host:port/api/socket/:cursor` (live patches), `ws://host:port/api/mcp/stream` (MCP)
|
||||
|
||||
**Protocol v6:**
|
||||
**Protocol v6 (MessagePack):**
|
||||
```json
|
||||
// Handshake
|
||||
{"type": "ServerInfo", "protocolVersion": 6, "serverVersion": "7.7.0"}
|
||||
|
||||
// Patch
|
||||
{
|
||||
"type": "ApplyPatch",
|
||||
"sessionId": "<uuid>",
|
||||
"patch": {
|
||||
"removed": [id1, id2],
|
||||
"added": {id3: {...}},
|
||||
"updated": {id4: {changedProperties: {...}}}
|
||||
"packetType": "messages",
|
||||
"body": {
|
||||
"messageCursor": 5,
|
||||
"messages": [
|
||||
{ "removed": [], "added": {}, "updated": [] }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -278,7 +290,8 @@ pub struct SyncRule {
|
|||
**Load project:**
|
||||
```rust
|
||||
use librojo::Project;
|
||||
let project = Project::load_from_slice(path, contents)?;
|
||||
let project = Project::load_fuzzy(&vfs, path)?;
|
||||
// or: Project::load_exact(&vfs, path, fallback_name)?;
|
||||
```
|
||||
|
||||
**Build snapshot:**
|
||||
|
|
@ -314,11 +327,15 @@ to_writer(&mut buffer, &dom, dom.root())?;
|
|||
|
||||
## Key Dependencies
|
||||
|
||||
**Core:** rbx_binary, rbx_xml, rbx_dom_weak, memofs (local crate)
|
||||
**Async:** tokio, hyper, hyper-tungstenite
|
||||
**Serialization:** serde, json5, toml, yaml-rust2, csv
|
||||
**Core:** rbx_binary, rbx_xml, rbx_dom_weak, rbx_reflection, rbx_reflection_database, memofs (local crate)
|
||||
**Async:** tokio, hyper, hyper-util, http-body-util, hyper-tungstenite, reqwest, futures
|
||||
**Serialization:** serde, serde_json, json5, toml, yaml-rust2, csv, rmp-serde, serde_bytes, bincode
|
||||
**Hashing:** blake3, sha1
|
||||
**Logging:** log, tracing, tracing-appender, tracing-subscriber, time, dirs, flate2
|
||||
**CLI:** clap (with derive, env), clap_complete
|
||||
**Schema:** schemars
|
||||
**Float precision:** lexical-write-float (for roundtrip-safe f32/f64 formatting in JSON)
|
||||
**Utilities:** clap, anyhow, thiserror, crossbeam-channel, rayon, globset
|
||||
**Utilities:** anyhow, thiserror, crossbeam-channel, rayon, globset, uuid, walkdir, tempfile, pathdiff
|
||||
|
||||
## Module Organization
|
||||
|
||||
|
|
@ -362,7 +379,7 @@ RUST_LOG=rojo=debug rojo build
|
|||
|
||||
**Watch events:** `RUST_LOG=memofs=trace`
|
||||
|
||||
**Protocol issues:** Browser dev tools on `http://localhost:34872`
|
||||
**Protocol issues:** Browser dev tools on `http://localhost:34873`
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
|
|
@ -376,18 +393,27 @@ RUST_LOG=rojo=debug rojo build
|
|||
|
||||
```
|
||||
src/
|
||||
cli/ - Commands
|
||||
cli/ - Commands (serve, build, upload, syncback, sourcemap,
|
||||
clone, completions, cursor, studio, init, fmt_project,
|
||||
doc, plugin)
|
||||
snapshot/ - Core system
|
||||
matching.rs - Forward sync matching algorithm
|
||||
tree.rs - RojoTree (script_refs, known_paths(), path_to_ids)
|
||||
snapshot_middleware/ - File handlers
|
||||
syncback/ - Reverse sync
|
||||
matching.rs - Syncback matching algorithm
|
||||
dedup_suffix.rs - Dedup cleanup rules
|
||||
meta.rs - Meta file helpers
|
||||
web/ - HTTP/WebSocket (incl. SyncbackSignal, LiveServer)
|
||||
interface.rs - Wire types (patches, syncback requests)
|
||||
web/ - HTTP/WebSocket server
|
||||
api.rs - HTTP/WebSocket endpoints
|
||||
interface.rs - Wire types (patches, syncback requests, GitMetadata)
|
||||
mcp.rs - MCP JSON-RPC, McpState, tool handlers
|
||||
mcp_docs/ - MCP tool descriptions
|
||||
change_processor.rs - Two-way sync filesystem writer
|
||||
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
|
||||
git.rs - Git integration (CLI-based, changed files, SHA1, auto-staging)
|
||||
logging.rs - Structured logging, file output to ~/.atlas/logs/
|
||||
message_queue.rs - Patch message batching and cursor system
|
||||
roblox_api.rs - Roblox auth, place download, experience name lookup
|
||||
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex, Luau-style relative paths)
|
||||
variant_eq.rs - Property value comparison
|
||||
lib.rs - Public API
|
||||
|
|
@ -397,6 +423,7 @@ crates/
|
|||
memofs/ - Virtual filesystem
|
||||
|
||||
tests/
|
||||
end_to_end.rs - Integration test crate root
|
||||
rojo_test/ - Test utilities
|
||||
tests/ - Integration tests
|
||||
```
|
||||
|
|
|
|||
|
|
@ -85,9 +85,17 @@ pub fn syncback_loop(
|
|||
new_tree: WeakDom, // Roblox file contents
|
||||
project: &Project, // Project configuration
|
||||
incremental: bool, // Preserve structure vs rebuild
|
||||
) -> anyhow::Result<FsSnapshot>
|
||||
) -> anyhow::Result<SyncbackResult>
|
||||
|
||||
pub struct SyncbackResult {
|
||||
pub fs_snapshot: FsSnapshot,
|
||||
pub new_tree: WeakDom,
|
||||
pub instance_paths: HashMap<Ref, String>,
|
||||
}
|
||||
```
|
||||
|
||||
Also available: `syncback_loop_with_stats()` accepts `external_stats: Option<&SyncbackStats>` for callers that track stats externally.
|
||||
|
||||
## Syncback Modes
|
||||
|
||||
### Incremental Mode (`incremental: true`)
|
||||
|
|
@ -215,10 +223,11 @@ pub struct SyncbackRules {
|
|||
```
|
||||
|
||||
**Property filtering** (`property_filter.rs`):
|
||||
- Filters out `Name`, `Parent` (always)
|
||||
- Filters out `Name`, `Parent`, `Source`, `Value`, `Contents` via reflection DB (`DoesNotSerialize`), not explicit property lists
|
||||
- Explicitly filters `Variant::Ref` and `Variant::UniqueId` by variant type
|
||||
- Filters out properties in `ignore_properties`
|
||||
- Filters out unscriptable properties if `sync_unscriptable: false`
|
||||
- Filters out middleware-specific properties (e.g., `Script.Source` when writing to `.luau` file)
|
||||
- Middleware-specific filtering (e.g., `Script.Source` for `.luau` files) is handled in `snapshot.rs` via `filter_out_property`, not in `property_filter.rs`
|
||||
|
||||
### Hidden Services
|
||||
|
||||
|
|
@ -296,17 +305,14 @@ pub struct SyncbackSnapshot<'sync> {
|
|||
**Methods:**
|
||||
|
||||
```rust
|
||||
// Join child to current path
|
||||
snapshot.with_joined_path(new_ref, old_ref)?
|
||||
// Result: parent_path/ChildName.luau
|
||||
// Join child to current path (returns (Self, needs_meta_name, dedup_key))
|
||||
snapshot.with_joined_path(new_ref, old_ref, &taken_names)?
|
||||
|
||||
// Use base path
|
||||
snapshot.with_base_path(&base, new_ref, old_ref)?
|
||||
// Result: base/ChildName.luau
|
||||
// Use base path (same return type)
|
||||
snapshot.with_base_path(&base, new_ref, old_ref, &taken_names)?
|
||||
|
||||
// Explicit path
|
||||
snapshot.with_new_path(path, new_ref, old_ref)
|
||||
// Result: path (exactly as provided)
|
||||
```
|
||||
|
||||
**Filename generation** (`file_names.rs`):
|
||||
|
|
@ -366,7 +372,7 @@ fs_snapshot.merge(other) // Combine snapshots
|
|||
|
||||
// Apply to filesystem
|
||||
fs_snapshot.write_to_vfs(base_path, vfs)?;
|
||||
fs_snapshot.write_to_vfs_parallel(base_path, vfs)?; // Parallel writes (see below)
|
||||
fs_snapshot.write_to_vfs_parallel(base_path, vfs, git_cache)?; // Parallel writes; git_cache: Option<&GitIndexCache> skips unchanged files
|
||||
|
||||
// Post-process ref paths
|
||||
fs_snapshot.fix_ref_paths(&substitutions); // Replace placeholder paths with final relative paths
|
||||
|
|
@ -416,7 +422,7 @@ Forward sync also reads `Rojo_Target_*` attributes and resolves via `get_specifi
|
|||
- `collect_referents(dom, paths, final_paths)` - Finds all Ref properties, stores as (source, target_path) pairs
|
||||
- `tentative_fs_path(dom, ref)` - Computes pre-dedup path: slugified name + extension based on class/children
|
||||
- `tentative_fs_path_public()` - Public wrapper used in post-processing
|
||||
- `link_referents(dom, referents)` - Writes `Rojo_Ref_*` attributes to instances
|
||||
- `link_referents(links: RefLinks, dom: &mut WeakDom)` - Writes `Rojo_Ref_*` attributes to instances
|
||||
|
||||
**Ref path post-processing (in `mod.rs`):**
|
||||
|
||||
|
|
@ -577,6 +583,22 @@ diff place.rbxl rebuilt.rbxl
|
|||
- Globally optimal matching (Hungarian algorithm) for large ambiguous groups
|
||||
- ~~Matching cache (parent_ref -> match_assignments) for avoiding redundant scoring~~ (session-level caching now implemented via `MatchingSession`; per-parent invalidation cache still deferred)
|
||||
|
||||
## GitIndexCache Integration
|
||||
|
||||
`write_to_vfs_parallel()` accepts `git_cache: Option<&GitIndexCache>`. When provided (by `src/cli/syncback.rs` and `src/cli/serve.rs`), files whose SHA1 blob hash matches the git index are skipped. `GitIndexCache` is created via `GitIndexCache::new(project_root)` which runs `git ls-files --stage`.
|
||||
|
||||
## $schema Roundtrip
|
||||
|
||||
`.project.json5`, `.meta.json5`, and `.model.json5` files preserve a `$schema` field during syncback. Implemented via `schema: Option<String>` on `Project`, `AdjacentMetadata`, `DirectoryMetadata`, and `JsonModel` structs. Old instance metadata carries the schema through the pipeline.
|
||||
|
||||
## Reserved Name: `init`
|
||||
|
||||
`slugify_name()` appends `_` to `"init"` (case-insensitive): `"init"` -> `"init_"`. `name_needs_slugify()` returns `true` for `"init"`. This prevents collision with the init file convention (`init.luau`, `init.server.luau`, etc.).
|
||||
|
||||
## MCP Syncback
|
||||
|
||||
`POST /api/mcp/syncback` runs syncback inline and returns stats with a `syncback_complete` status. Unlike `/api/syncback` which restarts the serve loop, the MCP variant completes without server restart.
|
||||
|
||||
## Key Files to Reference
|
||||
|
||||
- `src/syncback/mod.rs` - Main orchestration loop, ref path post-processing
|
||||
|
|
|
|||
|
|
@ -30,9 +30,11 @@ Atlas bridges filesystem and Roblox Studio, enabling professional development to
|
|||
**Core capabilities:**
|
||||
- Filesystem-based development with live sync to Studio
|
||||
- Build/upload rbxl/rbxm files from command line
|
||||
- Syncback: pull instances from Roblox → filesystem (7.7.0+)
|
||||
- Syncback: pull instances from Roblox → filesystem
|
||||
- Two-way sync (plugin-based): live bidirectional sync during `atlas serve` sessions
|
||||
- Live syncback: one-click full sync from Studio to filesystem during `atlas serve` (no WebSocket connection required)
|
||||
- MCP integration: AI agent-facing JSON-RPC layer for programmatic sync, script access, and Studio tool dispatch
|
||||
- Scripts-only mode (`syncScriptsOnly`): sync only script instances, ignore everything else
|
||||
|
||||
### Two-Way Sync Strategy
|
||||
|
||||
|
|
@ -49,7 +51,7 @@ Atlas bridges filesystem and Roblox Studio, enabling professional development to
|
|||
|
||||
### Three Components
|
||||
|
||||
1. **Server/CLI (Rust)** - `src/`, commands: serve, build, upload, syncback, sourcemap
|
||||
1. **Server/CLI (Rust)** - `src/`, commands: serve, build, upload, syncback, sourcemap, clone, completions, cursor, studio
|
||||
2. **Studio Plugin (Lua)** - `plugin/`, Roact UI, WebSocket sync
|
||||
3. **rbx-dom (submodule)** - `rbx-dom/`, Roblox format libraries (binary/XML, types, reflection)
|
||||
|
||||
|
|
@ -314,7 +316,9 @@ Reconciler.applyPatch(instanceMap, patch)
|
|||
|
||||
```
|
||||
src/ - Rust server/CLI
|
||||
cli/ - Command implementations
|
||||
cli/ - Command implementations (serve, build, upload, syncback,
|
||||
sourcemap, clone, completions, cursor, studio, init,
|
||||
fmt_project, doc, plugin)
|
||||
snapshot/ - Core snapshot system
|
||||
matching.rs - Forward sync matching algorithm
|
||||
snapshot_middleware/ - File type handlers
|
||||
|
|
@ -323,9 +327,14 @@ src/ - Rust server/CLI
|
|||
dedup_suffix.rs - Dedup cleanup rules
|
||||
meta.rs - Meta file read/write helpers
|
||||
web/ - HTTP/WebSocket server
|
||||
api.rs - HTTP/WebSocket endpoints
|
||||
interface.rs - Wire types (patches, syncback requests)
|
||||
mcp.rs - MCP JSON-RPC endpoint, McpState, tool handlers
|
||||
mcp_docs/ - MCP tool description files
|
||||
change_processor.rs - Two-way sync filesystem writer
|
||||
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
|
||||
git.rs - Git integration (CLI-based, changed files, SHA1, auto-staging)
|
||||
logging.rs - Structured logging, file output to ~/.atlas/logs/, rotation
|
||||
roblox_api.rs - Roblox auth, place download, experience name lookup
|
||||
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex)
|
||||
variant_eq.rs - Property value comparison
|
||||
|
||||
|
|
@ -336,6 +345,8 @@ plugin/src/ - Lua Studio plugin
|
|||
trueEquals.lua - Shared value equality module
|
||||
ChangeBatcher/ - Two-way sync patch creation
|
||||
encodeService.lua - Service-level property encoding for live syncback
|
||||
McpStream.lua - MCP WebSocket client (ws://host:port/api/mcp/stream)
|
||||
McpTools/ - Studio-side MCP tool dispatch (run_code, insert_model, etc.)
|
||||
Config.lua - Version and protocol config
|
||||
PatchTree.lua - Patch visualization tree (git-based default selections)
|
||||
SHA1.luau - SHA1 hash for git blob format comparison
|
||||
|
|
@ -368,7 +379,9 @@ rojo-test/ - Test fixtures and utilities
|
|||
## Version Information
|
||||
|
||||
- **Protocol version:** 6 (`plugin/src/Config.lua`)
|
||||
- **Server/Plugin must match:** MAJOR.MINOR (e.g., 7.7.x works with 7.7.y)
|
||||
- **Version matching:** Plugin WebSocket connection requires **exact version match** (`ApiContext.lua`). Live syncback validation uses **MAJOR.MINOR** match (`api.rs`).
|
||||
- **Current version:** 8.5.0
|
||||
- **Default serve port:** 34873
|
||||
- **MSRV:** Rust 1.88
|
||||
- **License:** MPL-2.0 (rbx-dom uses MIT)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue