All checks were successful
Build / build (push) Successful in 1m20s
## Major Reorganization
Reorganized all 57 TypeScript files from flat src/ directory into logical subdirectories for improved maintainability and discoverability.
## New Directory Structure
```
src/
├── core/ (4 files)
│ └── Foundation modules: defaultScene, gameConfig, debug, router
│
├── ship/ (10 files)
│ ├── Ship coordination and subsystems
│ └── input/ - VR controller and keyboard input
│
├── levels/ (10 files)
│ ├── config/ - Level schema, serialization, deserialization
│ ├── generation/ - Level generator and editor
│ └── ui/ - Level selector
│
├── environment/ (11 files)
│ ├── asteroids/ - Rock factory and explosions
│ ├── celestial/ - Suns, planets, textures
│ ├── stations/ - Star base loading
│ └── background/ - Stars, mirror, radar
│
├── ui/ (9 files)
│ ├── hud/ - Scoreboard and status screen
│ ├── screens/ - Login, settings, preloader
│ └── widgets/ - Discord integration
│
├── replay/ (7 files)
│ ├── Replay system components
│ └── recording/ - Physics recording and storage
│
├── game/ (3 files)
│ └── Game systems: stats, progression, demo
│
├── services/ (2 files)
│ └── External integrations: auth, social
│
└── utils/ (5 files)
└── Shared utilities and helpers
```
## Changes Made
### File Moves (57 files)
- Core modules: 4 files → core/
- Ship system: 10 files → ship/ + ship/input/
- Level system: 10 files → levels/ (+ 3 subdirs)
- Environment: 11 files → environment/ (+ 4 subdirs)
- UI components: 9 files → ui/ (+ 3 subdirs)
- Replay system: 7 files → replay/ + replay/recording/
- Game systems: 3 files → game/
- Services: 2 files → services/
- Utilities: 5 files → utils/
### Import Path Updates
- Updated ~200 import statements across all files
- Fixed relative paths based on new directory structure
- Fixed case-sensitive import issues (physicsRecorder, physicsStorage)
- Ensured consistent lowercase filenames for imports
## Benefits
1. **Easy Navigation** - Related code grouped together
2. **Clear Boundaries** - Logical separation of concerns
3. **Scalability** - Easy pattern for adding new features
4. **Discoverability** - Find ship code in /ship, levels in /levels, etc.
5. **Maintainability** - Isolated modules easier to update
6. **No Circular Dependencies** - Clean dependency graph maintained
## Testing
- All TypeScript compilation errors resolved
- Build succeeds with new structure
- Import paths verified and corrected
- Case-sensitivity issues fixed
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
618 lines
21 KiB
TypeScript
618 lines
21 KiB
TypeScript
import { Scene, Vector3, Quaternion, AbstractMesh } from "@babylonjs/core";
|
|
import debugLog from "../../core/debug";
|
|
import { PhysicsStorage } from "./physicsStorage";
|
|
import { LevelConfig } from "../../levels/config/levelConfig";
|
|
|
|
/**
|
|
* Represents the physics state of a single object at a point in time
|
|
*/
|
|
export interface PhysicsObjectState {
|
|
id: string;
|
|
position: [number, number, number];
|
|
rotation: [number, number, number, number]; // Quaternion (x, y, z, w)
|
|
linearVelocity: [number, number, number];
|
|
angularVelocity: [number, number, number];
|
|
mass: number;
|
|
restitution: number;
|
|
}
|
|
|
|
/**
|
|
* Snapshot of all physics objects at a specific time
|
|
*/
|
|
export interface PhysicsSnapshot {
|
|
timestamp: number; // Physics time in milliseconds
|
|
frameNumber: number; // Sequential frame counter
|
|
objects: PhysicsObjectState[];
|
|
}
|
|
|
|
/**
|
|
* Recording metadata
|
|
*/
|
|
export interface RecordingMetadata {
|
|
startTime: number;
|
|
endTime: number;
|
|
frameCount: number;
|
|
recordingDuration: number; // milliseconds
|
|
physicsUpdateRate: number; // Hz
|
|
levelConfig?: LevelConfig; // Full scene state at recording time
|
|
}
|
|
|
|
/**
|
|
* Complete recording with metadata and snapshots
|
|
*/
|
|
export interface PhysicsRecording {
|
|
metadata: RecordingMetadata;
|
|
snapshots: PhysicsSnapshot[];
|
|
}
|
|
|
|
/**
|
|
* Physics state recorder that continuously captures physics state
|
|
* - Ring buffer mode: Always captures last N seconds (low memory, quick export)
|
|
* - Long recording mode: Saves to IndexedDB for 2-10 minute recordings
|
|
*/
|
|
export class PhysicsRecorder {
|
|
private _scene: Scene;
|
|
private _isEnabled: boolean = false;
|
|
private _isLongRecording: boolean = false;
|
|
|
|
// Ring buffer for continuous recording
|
|
private _ringBuffer: PhysicsSnapshot[] = [];
|
|
private _maxRingBufferFrames: number = 216; // 30 seconds at 7.2 Hz
|
|
private _ringBufferIndex: number = 0;
|
|
|
|
// Long recording storage
|
|
private _longRecording: PhysicsSnapshot[] = [];
|
|
private _longRecordingStartTime: number = 0;
|
|
|
|
// Frame tracking
|
|
private _frameNumber: number = 0;
|
|
private _startTime: number = 0;
|
|
private _physicsUpdateRate: number = 7.2; // Hz (estimated)
|
|
|
|
// Performance tracking
|
|
private _captureTimeAccumulator: number = 0;
|
|
private _captureCount: number = 0;
|
|
|
|
// IndexedDB storage
|
|
private _storage: PhysicsStorage | null = null;
|
|
|
|
// Auto-save to IndexedDB
|
|
private _autoSaveEnabled: boolean = true;
|
|
private _autoSaveBuffer: PhysicsSnapshot[] = [];
|
|
private _autoSaveInterval: number = 10000; // Save every 10 seconds
|
|
private _lastAutoSaveTime: number = 0;
|
|
private _currentSessionId: string = "";
|
|
private _levelConfig: LevelConfig | null = null;
|
|
|
|
constructor(scene: Scene, levelConfig?: LevelConfig) {
|
|
this._scene = scene;
|
|
this._levelConfig = levelConfig || null;
|
|
|
|
// Initialize IndexedDB storage
|
|
this._storage = new PhysicsStorage();
|
|
this._storage.initialize().catch(error => {
|
|
debugLog("PhysicsRecorder: Failed to initialize storage", error);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Start the ring buffer recorder (always capturing last 30 seconds)
|
|
* Also starts auto-save to IndexedDB
|
|
*/
|
|
public startRingBuffer(): void {
|
|
if (this._isEnabled) {
|
|
debugLog("PhysicsRecorder: Ring buffer already running");
|
|
return;
|
|
}
|
|
|
|
this._isEnabled = true;
|
|
this._startTime = performance.now();
|
|
this._lastAutoSaveTime = performance.now();
|
|
this._frameNumber = 0;
|
|
|
|
// Create unique session ID for this recording
|
|
this._currentSessionId = `session-${Date.now()}`;
|
|
|
|
// Hook into physics update observable
|
|
this._scene.onAfterPhysicsObservable.add(() => {
|
|
if (this._isEnabled) {
|
|
this.captureFrame();
|
|
this.checkAutoSave();
|
|
}
|
|
});
|
|
|
|
debugLog("PhysicsRecorder: Recording started (ring buffer + auto-save to IndexedDB)");
|
|
debugLog(`PhysicsRecorder: Session ID: ${this._currentSessionId}`);
|
|
}
|
|
|
|
/**
|
|
* Stop the ring buffer recorder
|
|
*/
|
|
public stopRingBuffer(): void {
|
|
this._isEnabled = false;
|
|
debugLog("PhysicsRecorder: Ring buffer stopped");
|
|
}
|
|
|
|
/**
|
|
* Start a long-term recording (saves all frames to memory)
|
|
*/
|
|
public startLongRecording(): void {
|
|
if (this._isLongRecording) {
|
|
debugLog("PhysicsRecorder: Long recording already in progress");
|
|
return;
|
|
}
|
|
|
|
this._isLongRecording = true;
|
|
this._longRecording = [];
|
|
this._longRecordingStartTime = performance.now();
|
|
|
|
debugLog("PhysicsRecorder: Long recording started");
|
|
}
|
|
|
|
/**
|
|
* Stop long-term recording
|
|
*/
|
|
public stopLongRecording(): void {
|
|
if (!this._isLongRecording) {
|
|
debugLog("PhysicsRecorder: No long recording in progress");
|
|
return;
|
|
}
|
|
|
|
this._isLongRecording = false;
|
|
const duration = ((performance.now() - this._longRecordingStartTime) / 1000).toFixed(1);
|
|
debugLog(`PhysicsRecorder: Long recording stopped (${duration}s, ${this._longRecording.length} frames)`);
|
|
}
|
|
|
|
/**
|
|
* Capture current physics state of all objects
|
|
*/
|
|
private captureFrame(): void {
|
|
const captureStart = performance.now();
|
|
|
|
const timestamp = performance.now() - this._startTime;
|
|
const objects: PhysicsObjectState[] = [];
|
|
|
|
// Get all physics-enabled meshes AND transform nodes
|
|
const physicsMeshes = this._scene.meshes.filter(mesh => mesh.physicsBody !== null && mesh.physicsBody !== undefined);
|
|
const physicsTransformNodes = this._scene.transformNodes.filter(node => node.physicsBody !== null && node.physicsBody !== undefined);
|
|
const allPhysicsObjects = [...physicsMeshes, ...physicsTransformNodes];
|
|
|
|
for (const mesh of allPhysicsObjects) {
|
|
const body = mesh.physicsBody;
|
|
|
|
// Double-check body still exists and has transformNode (can be disposed between filter and here)
|
|
if (!body || !body.transformNode) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
// Get position
|
|
const pos = body.transformNode.position;
|
|
|
|
// Get rotation as quaternion
|
|
let quat = body.transformNode.rotationQuaternion;
|
|
if (!quat) {
|
|
// Convert Euler to Quaternion if needed
|
|
const rot = body.transformNode.rotation;
|
|
quat = Quaternion.FromEulerAngles(rot.x, rot.y, rot.z);
|
|
}
|
|
|
|
// Get velocities
|
|
const linVel = body.getLinearVelocity();
|
|
const angVel = body.getAngularVelocity();
|
|
|
|
// Get mass
|
|
const mass = body.getMassProperties().mass;
|
|
|
|
// Get restitution (from shape material if available)
|
|
let restitution = 0;
|
|
if (body.shape && (body.shape as any).material) {
|
|
restitution = (body.shape as any).material.restitution || 0;
|
|
}
|
|
|
|
objects.push({
|
|
id: mesh.id,
|
|
position: [
|
|
parseFloat(pos.x.toFixed(3)),
|
|
parseFloat(pos.y.toFixed(3)),
|
|
parseFloat(pos.z.toFixed(3))
|
|
],
|
|
rotation: [
|
|
parseFloat(quat.x.toFixed(4)),
|
|
parseFloat(quat.y.toFixed(4)),
|
|
parseFloat(quat.z.toFixed(4)),
|
|
parseFloat(quat.w.toFixed(4))
|
|
],
|
|
linearVelocity: [
|
|
parseFloat(linVel.x.toFixed(3)),
|
|
parseFloat(linVel.y.toFixed(3)),
|
|
parseFloat(linVel.z.toFixed(3))
|
|
],
|
|
angularVelocity: [
|
|
parseFloat(angVel.x.toFixed(3)),
|
|
parseFloat(angVel.y.toFixed(3)),
|
|
parseFloat(angVel.z.toFixed(3))
|
|
],
|
|
mass: parseFloat(mass.toFixed(2)),
|
|
restitution: parseFloat(restitution.toFixed(2))
|
|
});
|
|
} catch (error) {
|
|
// Physics body was disposed during capture, skip this object
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const snapshot: PhysicsSnapshot = {
|
|
timestamp: parseFloat(timestamp.toFixed(1)),
|
|
frameNumber: this._frameNumber,
|
|
objects
|
|
};
|
|
|
|
// Add to ring buffer (circular overwrite)
|
|
this._ringBuffer[this._ringBufferIndex] = snapshot;
|
|
this._ringBufferIndex = (this._ringBufferIndex + 1) % this._maxRingBufferFrames;
|
|
|
|
// Add to long recording if active
|
|
if (this._isLongRecording) {
|
|
this._longRecording.push(snapshot);
|
|
}
|
|
|
|
// Add to auto-save buffer if enabled
|
|
if (this._autoSaveEnabled) {
|
|
this._autoSaveBuffer.push(snapshot);
|
|
}
|
|
|
|
this._frameNumber++;
|
|
|
|
// Track performance
|
|
const captureTime = performance.now() - captureStart;
|
|
this._captureTimeAccumulator += captureTime;
|
|
this._captureCount++;
|
|
|
|
// Log average capture time every 100 frames
|
|
if (this._captureCount % 100 === 0) {
|
|
const avgTime = (this._captureTimeAccumulator / this._captureCount).toFixed(3);
|
|
debugLog(`PhysicsRecorder: Average capture time: ${avgTime}ms (${objects.length} objects)`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if it's time to auto-save to IndexedDB
|
|
*/
|
|
private checkAutoSave(): void {
|
|
if (!this._autoSaveEnabled || !this._storage) {
|
|
return;
|
|
}
|
|
|
|
const now = performance.now();
|
|
const timeSinceLastSave = now - this._lastAutoSaveTime;
|
|
|
|
// Save every 10 seconds
|
|
if (timeSinceLastSave >= this._autoSaveInterval && this._autoSaveBuffer.length > 0) {
|
|
this.performAutoSave();
|
|
this._lastAutoSaveTime = now;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save buffered snapshots to IndexedDB
|
|
*/
|
|
private async performAutoSave(): Promise<void> {
|
|
if (!this._storage || this._autoSaveBuffer.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Copy buffer and clear it immediately to avoid blocking next frame
|
|
const snapshotsToSave = [...this._autoSaveBuffer];
|
|
this._autoSaveBuffer = [];
|
|
|
|
// Use the LevelConfig passed to constructor
|
|
const levelConfig = this._levelConfig || undefined;
|
|
|
|
// Create a recording from the buffered snapshots
|
|
const metadata: RecordingMetadata = {
|
|
startTime: snapshotsToSave[0].timestamp,
|
|
endTime: snapshotsToSave[snapshotsToSave.length - 1].timestamp,
|
|
frameCount: snapshotsToSave.length,
|
|
recordingDuration: snapshotsToSave[snapshotsToSave.length - 1].timestamp - snapshotsToSave[0].timestamp,
|
|
physicsUpdateRate: this._physicsUpdateRate,
|
|
levelConfig // Include complete scene state
|
|
};
|
|
|
|
const recording: PhysicsRecording = {
|
|
metadata,
|
|
snapshots: snapshotsToSave
|
|
};
|
|
|
|
try {
|
|
// Save to IndexedDB with session ID as name
|
|
await this._storage.saveRecording(this._currentSessionId, recording);
|
|
|
|
const duration = (metadata.recordingDuration / 1000).toFixed(1);
|
|
const configSize = levelConfig ? `with scene state (${JSON.stringify(levelConfig).length} bytes)` : 'without scene state';
|
|
debugLog(`PhysicsRecorder: Auto-saved ${snapshotsToSave.length} frames (${duration}s) ${configSize} to IndexedDB`);
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error during auto-save", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export last N seconds from ring buffer
|
|
*/
|
|
public exportRingBuffer(seconds: number = 30): PhysicsRecording {
|
|
const maxFrames = Math.min(
|
|
Math.floor(seconds * this._physicsUpdateRate),
|
|
this._maxRingBufferFrames
|
|
);
|
|
|
|
// Extract frames from ring buffer (handling circular nature)
|
|
const snapshots: PhysicsSnapshot[] = [];
|
|
const startIndex = (this._ringBufferIndex - maxFrames + this._maxRingBufferFrames) % this._maxRingBufferFrames;
|
|
|
|
for (let i = 0; i < maxFrames; i++) {
|
|
const index = (startIndex + i) % this._maxRingBufferFrames;
|
|
const snapshot = this._ringBuffer[index];
|
|
if (snapshot) {
|
|
snapshots.push(snapshot);
|
|
}
|
|
}
|
|
|
|
// Sort by frame number to ensure correct order
|
|
snapshots.sort((a, b) => a.frameNumber - b.frameNumber);
|
|
|
|
const metadata: RecordingMetadata = {
|
|
startTime: snapshots[0]?.timestamp || 0,
|
|
endTime: snapshots[snapshots.length - 1]?.timestamp || 0,
|
|
frameCount: snapshots.length,
|
|
recordingDuration: (snapshots[snapshots.length - 1]?.timestamp || 0) - (snapshots[0]?.timestamp || 0),
|
|
physicsUpdateRate: this._physicsUpdateRate
|
|
};
|
|
|
|
return {
|
|
metadata,
|
|
snapshots
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Export long recording
|
|
*/
|
|
public exportLongRecording(): PhysicsRecording {
|
|
if (this._longRecording.length === 0) {
|
|
debugLog("PhysicsRecorder: No long recording data to export");
|
|
return {
|
|
metadata: {
|
|
startTime: 0,
|
|
endTime: 0,
|
|
frameCount: 0,
|
|
recordingDuration: 0,
|
|
physicsUpdateRate: this._physicsUpdateRate
|
|
},
|
|
snapshots: []
|
|
};
|
|
}
|
|
|
|
const metadata: RecordingMetadata = {
|
|
startTime: this._longRecording[0].timestamp,
|
|
endTime: this._longRecording[this._longRecording.length - 1].timestamp,
|
|
frameCount: this._longRecording.length,
|
|
recordingDuration: this._longRecording[this._longRecording.length - 1].timestamp - this._longRecording[0].timestamp,
|
|
physicsUpdateRate: this._physicsUpdateRate
|
|
};
|
|
|
|
return {
|
|
metadata,
|
|
snapshots: this._longRecording
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Download recording as JSON file
|
|
*/
|
|
public downloadRecording(recording: PhysicsRecording, filename: string = "physics-recording"): void {
|
|
const json = JSON.stringify(recording, null, 2);
|
|
const blob = new Blob([json], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = `${filename}-${Date.now()}.json`;
|
|
link.click();
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
const sizeMB = (blob.size / 1024 / 1024).toFixed(2);
|
|
const duration = (recording.metadata.recordingDuration / 1000).toFixed(1);
|
|
debugLog(`PhysicsRecorder: Downloaded ${filename} (${sizeMB} MB, ${duration}s, ${recording.metadata.frameCount} frames)`);
|
|
}
|
|
|
|
/**
|
|
* Get recording statistics
|
|
*/
|
|
public getStats(): {
|
|
isRecording: boolean;
|
|
isLongRecording: boolean;
|
|
ringBufferFrames: number;
|
|
ringBufferDuration: number;
|
|
longRecordingFrames: number;
|
|
longRecordingDuration: number;
|
|
averageCaptureTime: number;
|
|
} {
|
|
const ringBufferDuration = this._ringBuffer.length > 0
|
|
? (this._ringBuffer[this._ringBuffer.length - 1]?.timestamp || 0) - (this._ringBuffer[0]?.timestamp || 0)
|
|
: 0;
|
|
|
|
const longRecordingDuration = this._longRecording.length > 0
|
|
? this._longRecording[this._longRecording.length - 1].timestamp - this._longRecording[0].timestamp
|
|
: 0;
|
|
|
|
return {
|
|
isRecording: this._isEnabled,
|
|
isLongRecording: this._isLongRecording,
|
|
ringBufferFrames: this._ringBuffer.filter(s => s !== undefined).length,
|
|
ringBufferDuration: ringBufferDuration / 1000, // Convert to seconds
|
|
longRecordingFrames: this._longRecording.length,
|
|
longRecordingDuration: longRecordingDuration / 1000, // Convert to seconds
|
|
averageCaptureTime: this._captureCount > 0 ? this._captureTimeAccumulator / this._captureCount : 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Clear long recording data
|
|
*/
|
|
public clearLongRecording(): void {
|
|
this._longRecording = [];
|
|
this._isLongRecording = false;
|
|
debugLog("PhysicsRecorder: Long recording data cleared");
|
|
}
|
|
|
|
/**
|
|
* Save current long recording to IndexedDB
|
|
*/
|
|
public async saveLongRecordingToStorage(name: string): Promise<string | null> {
|
|
if (!this._storage) {
|
|
debugLog("PhysicsRecorder: Storage not initialized");
|
|
return null;
|
|
}
|
|
|
|
const recording = this.exportLongRecording();
|
|
if (recording.snapshots.length === 0) {
|
|
debugLog("PhysicsRecorder: No recording data to save");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const recordingId = await this._storage.saveRecording(name, recording);
|
|
debugLog(`PhysicsRecorder: Saved to IndexedDB with ID: ${recordingId}`);
|
|
return recordingId;
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error saving to IndexedDB", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save ring buffer to IndexedDB
|
|
*/
|
|
public async saveRingBufferToStorage(name: string, seconds: number = 30): Promise<string | null> {
|
|
if (!this._storage) {
|
|
debugLog("PhysicsRecorder: Storage not initialized");
|
|
return null;
|
|
}
|
|
|
|
const recording = this.exportRingBuffer(seconds);
|
|
if (recording.snapshots.length === 0) {
|
|
debugLog("PhysicsRecorder: No ring buffer data to save");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const recordingId = await this._storage.saveRecording(name, recording);
|
|
debugLog(`PhysicsRecorder: Saved ring buffer to IndexedDB with ID: ${recordingId}`);
|
|
return recordingId;
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error saving ring buffer to IndexedDB", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load a recording from IndexedDB
|
|
*/
|
|
public async loadRecordingFromStorage(recordingId: string): Promise<PhysicsRecording | null> {
|
|
if (!this._storage) {
|
|
debugLog("PhysicsRecorder: Storage not initialized");
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return await this._storage.loadRecording(recordingId);
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error loading from IndexedDB", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* List all recordings in IndexedDB
|
|
*/
|
|
public async listStoredRecordings(): Promise<Array<{
|
|
id: string;
|
|
name: string;
|
|
timestamp: number;
|
|
duration: number;
|
|
frameCount: number;
|
|
}>> {
|
|
if (!this._storage) {
|
|
debugLog("PhysicsRecorder: Storage not initialized");
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
return await this._storage.listRecordings();
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error listing recordings", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a recording from IndexedDB
|
|
*/
|
|
public async deleteStoredRecording(recordingId: string): Promise<boolean> {
|
|
if (!this._storage) {
|
|
debugLog("PhysicsRecorder: Storage not initialized");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
await this._storage.deleteRecording(recordingId);
|
|
return true;
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error deleting recording", error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get storage statistics
|
|
*/
|
|
public async getStorageStats(): Promise<{
|
|
recordingCount: number;
|
|
totalSegments: number;
|
|
estimatedSizeMB: number;
|
|
} | null> {
|
|
if (!this._storage) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
return await this._storage.getStats();
|
|
} catch (error) {
|
|
debugLog("PhysicsRecorder: Error getting storage stats", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispose of recorder resources
|
|
*/
|
|
public async dispose(): Promise<void> {
|
|
// Save any remaining buffered data before disposing
|
|
if (this._autoSaveBuffer.length > 0) {
|
|
debugLog(`PhysicsRecorder: Saving ${this._autoSaveBuffer.length} remaining frames before disposal`);
|
|
await this.performAutoSave();
|
|
}
|
|
|
|
this.stopRingBuffer();
|
|
this.stopLongRecording();
|
|
this._ringBuffer = [];
|
|
this._longRecording = [];
|
|
this._autoSaveBuffer = [];
|
|
|
|
if (this._storage) {
|
|
this._storage.close();
|
|
}
|
|
}
|
|
}
|