From f232b9904c4c25a57f8acd333593d79a0e6f56bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:30:07 +0000 Subject: [PATCH 1/2] Initial plan From 7f96ec7cbb49e586d068a90d548009ff446163f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 16 Sep 2025 12:37:29 +0000 Subject: [PATCH 2/2] Implement complete RoExport Python program for Roblox script export Co-authored-by: N3uralCreativity <180445421+N3uralCreativity@users.noreply.github.com> --- .gitignore | 80 +++++++++++++ README.md | 241 +++++++++++++++++++++++++++++++++++++- examples/README.md | 58 +++++++++ examples/example_usage.py | 140 ++++++++++++++++++++++ requirements.txt | 2 + roexport.py | 9 ++ roexport/__init__.py | 12 ++ roexport/cli.py | 140 ++++++++++++++++++++++ roexport/exporter.py | 225 +++++++++++++++++++++++++++++++++++ roexport/parser.py | 154 ++++++++++++++++++++++++ setup.py | 60 ++++++++++ 11 files changed, 1120 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 examples/README.md create mode 100755 examples/example_usage.py create mode 100644 requirements.txt create mode 100755 roexport.py create mode 100644 roexport/__init__.py create mode 100644 roexport/cli.py create mode 100644 roexport/exporter.py create mode 100644 roexport/parser.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70e7331 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Examples and test outputs +examples/output/ +test_output/ +*.rbxm +*.rbxl + +# Temporary files +/tmp/ \ No newline at end of file diff --git a/README.md b/README.md index 1c02bcf..4de6a72 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,241 @@ # RoExport -A Functionnal Scripts exporter from Roblox Studio Files (.rbxm) to .luau files + +A clean and efficient Python tool for exporting Roblox Studio files (.rbxm and .rbxl) to .lua files that can be used in any IDE or text editor. + +## Features + +- **Clean Export**: Converts Roblox Studio files to standard .lua files +- **IDE Compatible**: Exported files work with any IDE (VS Code, IntelliJ, Sublime Text, etc.) +- **Preserves Structure**: Maintains folder hierarchy from your Roblox project +- **Multiple Script Types**: Supports Scripts, LocalScripts, and ModuleScripts +- **Smart Naming**: Automatically names files based on script type (.server.lua, .client.lua, .lua) +- **No Dependencies**: Uses only Python standard library +- **Command Line Interface**: Easy to use from terminal or integrate into workflows +- **Export Summary**: Generates overview of exported files + +## Quick Start + +### Installation + +```bash +# Clone the repository +git clone https://github.com/N3uralCreativity/RoExport.git +cd RoExport + +# Install the package +pip install -e . +``` + +### Basic Usage + +```bash +# Export a Roblox file to lua files +roexport my_game.rbxm + +# Specify custom output directory +roexport my_game.rbxm exported_scripts/ + +# Export with flat structure (no subdirectories) +roexport my_game.rbxm --flat + +# Export without headers +roexport my_game.rbxm --no-headers +``` + +## Usage Examples + +### Command Line + +```bash +# Basic export +roexport game.rbxm + +# Export to specific directory +roexport game.rbxm output/ + +# Flat structure export +roexport game.rbxm scripts/ --flat + +# Quiet export without summary +roexport game.rbxm --no-summary + +# Verbose output +roexport game.rbxm --verbose +``` + +### Python API + +```python +from roexport import RobloxExporter + +# Create exporter +exporter = RobloxExporter( + preserve_hierarchy=True, # Maintain folder structure + add_headers=True # Add informational headers +) + +# Export file +exported_files = exporter.export_with_summary("game.rbxm", "output/") + +print(f"Exported {len(exported_files)} files") +``` + +### Advanced Python Usage + +```python +from roexport import RobloxFileParser, RobloxExporter + +# Parse file to inspect scripts first +parser = RobloxFileParser() +scripts = parser.parse_file("game.rbxm") + +# Filter scripts by type +module_scripts = parser.get_scripts_by_type("ModuleScript") +local_scripts = parser.get_scripts_by_type("LocalScript") + +# Get script counts +counts = parser.get_script_count() +print(f"Found {counts['ModuleScript']} ModuleScripts") + +# Export with custom settings +exporter = RobloxExporter(preserve_hierarchy=False) +exporter.export_scripts(module_scripts, "modules/") +``` + +## File Naming Convention + +RoExport uses smart naming conventions for exported files: + +- **ModuleScript** → `ScriptName.lua` +- **LocalScript** → `ScriptName.client.lua` +- **Script** → `ScriptName.server.lua` + +## Output Structure + +### With Hierarchy (default) +``` +exported_scripts/ +├── ServerScriptService/ +│ ├── GameManager.server.lua +│ └── Utils/ +│ └── MathHelper.lua +├── StarterPlayer/ +│ └── StarterPlayerScripts/ +│ └── ClientMain.client.lua +└── EXPORT_SUMMARY.md +``` + +### Flat Structure (`--flat`) +``` +exported_scripts/ +├── GameManager.server.lua +├── MathHelper.lua +├── ClientMain.client.lua +└── EXPORT_SUMMARY.md +``` + +## Command Line Options + +| Option | Description | +|--------|-------------| +| `input` | Path to .rbxm or .rbxl file (required) | +| `output` | Output directory (optional, defaults to input filename) | +| `--flat` | Export in flat structure without subdirectories | +| `--no-headers` | Don't add informational headers to files | +| `--no-summary` | Don't generate export summary file | +| `--verbose, -v` | Enable verbose output | +| `--version` | Show version information | + +## Export Headers + +By default, exported files include informational headers: + +```lua +-- ================================================== +-- Script Name: GameManager +-- Script Type: Script +-- Parent Path: ServerScriptService +-- Exported by RoExport +-- ================================================== + +-- Your script content here +game.Players.PlayerAdded:Connect(function(player) + print("Player joined:", player.Name) +end) +``` + +## Development + +### Setting up Development Environment + +```bash +# Clone repository +git clone https://github.com/N3uralCreativity/RoExport.git +cd RoExport + +# Install in development mode +pip install -e . + +# Run tests (if available) +python -m pytest + +# Run linting (if available) +python -m flake8 roexport/ +``` + +### Project Structure + +``` +RoExport/ +├── roexport/ # Main package +│ ├── __init__.py # Package initialization +│ ├── parser.py # Roblox file parser +│ ├── exporter.py # Script exporter +│ └── cli.py # Command line interface +├── roexport.py # Main entry point +├── setup.py # Package setup +├── requirements.txt # Dependencies +├── README.md # Documentation +└── LICENSE # Apache 2.0 License +``` + +## Supported File Types + +- `.rbxm` - Roblox Model files +- `.rbxl` - Roblox Place files + +## Requirements + +- Python 3.7 or higher +- No external dependencies (uses only Python standard library) + +## License + +This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. + +## Contributing + +1. Fork the repository +2. Create your feature branch (`git checkout -b feature/amazing-feature`) +3. Commit your changes (`git commit -m 'Add amazing feature'`) +4. Push to the branch (`git push origin feature/amazing-feature`) +5. Open a Pull Request + +## Support + +If you encounter any issues or have questions: + +1. Check the [Issues](https://github.com/N3uralCreativity/RoExport/issues) page +2. Create a new issue with detailed information about your problem +3. Include sample files if possible (without sensitive content) + +## Changelog + +### Version 1.0.0 +- Initial release +- Support for .rbxm and .rbxl files +- Command line interface +- Python API +- Hierarchical and flat export modes +- Smart file naming +- Export summaries diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..f5fd260 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,58 @@ +# Examples + +This directory contains example usage of RoExport. + +## Quick Test + +Since we don't include actual .rbxm files in the repository, you can test RoExport with your own Roblox Studio files: + +1. Export a model or place from Roblox Studio as `.rbxm` or `.rbxl` +2. Place it in this directory +3. Run the examples below + +## Command Line Examples + +```bash +# Basic export +roexport your_game.rbxm + +# Export to specific directory +roexport your_game.rbxm output/ + +# Flat structure +roexport your_game.rbxm output/ --flat + +# No headers +roexport your_game.rbxm output/ --no-headers --no-summary + +# Verbose mode +roexport your_game.rbxm --verbose +``` + +## Python API Examples + +Check out `example_usage.py` for Python API examples. + +## Sample Output Structure + +After running RoExport on a typical Roblox game, you might see: + +``` +output/ +├── ServerScriptService/ +│ ├── GameManager.server.lua +│ ├── DataManager.server.lua +│ └── Modules/ +│ ├── PlayerData.lua +│ └── GameConfig.lua +├── StarterPlayer/ +│ └── StarterPlayerScripts/ +│ ├── ClientMain.client.lua +│ └── GUI/ +│ └── MenuHandler.client.lua +├── ReplicatedStorage/ +│ └── Shared/ +│ ├── RemoteEvents.lua +│ └── Constants.lua +└── EXPORT_SUMMARY.md +``` \ No newline at end of file diff --git a/examples/example_usage.py b/examples/example_usage.py new file mode 100755 index 0000000..2259e5b --- /dev/null +++ b/examples/example_usage.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +""" +Example usage of RoExport Python API +""" + +import os +import sys +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from roexport import RobloxExporter, RobloxFileParser + + +def basic_export_example(rbxm_file: str): + """Basic export example""" + print("=== Basic Export Example ===") + + # Create exporter with default settings + exporter = RobloxExporter() + + # Export the file + output_dir = "output_basic" + exported_files = exporter.export_with_summary(rbxm_file, output_dir) + + print(f"Exported {len(exported_files)} files to {output_dir}/") + + # Show script counts + counts = exporter.parser.get_script_count() + for script_type, count in counts.items(): + if count > 0: + print(f" {script_type}: {count}") + + +def advanced_parsing_example(rbxm_file: str): + """Advanced parsing and filtering example""" + print("\n=== Advanced Parsing Example ===") + + # Parse file first + parser = RobloxFileParser() + scripts = parser.parse_file(rbxm_file) + + print(f"Found {len(scripts)} total scripts") + + # Filter by type + module_scripts = [s for s in scripts if s.script_type == "ModuleScript"] + local_scripts = [s for s in scripts if s.script_type == "LocalScript"] + server_scripts = [s for s in scripts if s.script_type == "Script"] + + print(f"ModuleScripts: {len(module_scripts)}") + print(f"LocalScripts: {len(local_scripts)}") + print(f"Server Scripts: {len(server_scripts)}") + + # Export different types to different directories + exporter = RobloxExporter(preserve_hierarchy=False, add_headers=True) + + if module_scripts: + exporter.export_scripts(module_scripts, "output_modules") + print("Exported ModuleScripts to output_modules/") + + if local_scripts: + exporter.export_scripts(local_scripts, "output_client") + print("Exported LocalScripts to output_client/") + + if server_scripts: + exporter.export_scripts(server_scripts, "output_server") + print("Exported Server Scripts to output_server/") + + +def custom_settings_example(rbxm_file: str): + """Example with custom settings""" + print("\n=== Custom Settings Example ===") + + # Create exporter with custom settings + exporter = RobloxExporter( + preserve_hierarchy=False, # Flat structure + add_headers=False # No headers + ) + + # Export with custom settings + output_dir = "output_custom" + exported_files = exporter.export_file(rbxm_file, output_dir) + + print(f"Exported {len(exported_files)} files (flat, no headers) to {output_dir}/") + + +def inspect_scripts_example(rbxm_file: str): + """Example of inspecting scripts before export""" + print("\n=== Script Inspection Example ===") + + parser = RobloxFileParser() + scripts = parser.parse_file(rbxm_file) + + print("Script details:") + for i, script in enumerate(scripts[:5]): # Show first 5 scripts + print(f" {i+1}. {script.name} ({script.script_type})") + print(f" Path: {script.parent_path or 'Root'}") + print(f" Filename: {script.get_filename()}") + print(f" Content length: {len(script.source)} characters") + print() + + if len(scripts) > 5: + print(f" ... and {len(scripts) - 5} more scripts") + + +def main(): + """Main example function""" + # Check if we have a test file + test_files = [f for f in os.listdir('.') if f.endswith(('.rbxm', '.rbxl'))] + + if not test_files: + print("No .rbxm or .rbxl files found in examples directory.") + print("Please add a Roblox Studio file to test with.") + print("\nTo create a test file:") + print("1. Open Roblox Studio") + print("2. Create some scripts in your game") + print("3. File > Export Selection... or File > Save to File...") + print("4. Save as .rbxm or .rbxl in this directory") + return + + # Use the first test file found + test_file = test_files[0] + print(f"Using test file: {test_file}") + + try: + # Run examples + basic_export_example(test_file) + advanced_parsing_example(test_file) + custom_settings_example(test_file) + inspect_scripts_example(test_file) + + print("\n=== Examples Complete ===") + print("Check the output_* directories for exported files!") + + except Exception as e: + print(f"Error running examples: {e}") + import traceback + traceback.print_exc() + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1c5595d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# RoExport dependencies +# No external dependencies required - uses only Python standard library \ No newline at end of file diff --git a/roexport.py b/roexport.py new file mode 100755 index 0000000..683f727 --- /dev/null +++ b/roexport.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +""" +RoExport - Main entry point script +""" + +from roexport.cli import main + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/roexport/__init__.py b/roexport/__init__.py new file mode 100644 index 0000000..a9f32d3 --- /dev/null +++ b/roexport/__init__.py @@ -0,0 +1,12 @@ +""" +RoExport - A Python library for exporting Roblox Studio files to .lua files +""" + +__version__ = "1.0.0" +__author__ = "N3uralCreativity" +__description__ = "Export Roblox Studio files (.rbxm) to .lua files usable in any IDE" + +from .exporter import RobloxExporter +from .parser import RobloxFileParser + +__all__ = ["RobloxExporter", "RobloxFileParser"] \ No newline at end of file diff --git a/roexport/cli.py b/roexport/cli.py new file mode 100644 index 0000000..38ab596 --- /dev/null +++ b/roexport/cli.py @@ -0,0 +1,140 @@ +""" +Command-line interface for RoExport +""" + +import argparse +import sys +import os +import logging +from typing import Optional +from .exporter import RobloxExporter +from . import __version__ + + +def setup_logging(verbose: bool = False): + """Setup logging configuration""" + level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=level, + format='%(levelname)s: %(message)s' + ) + + +def validate_input_file(file_path: str) -> bool: + """Validate that the input file exists and has correct extension""" + if not os.path.exists(file_path): + print(f"Error: Input file '{file_path}' not found") + return False + + if not file_path.lower().endswith(('.rbxm', '.rbxl')): + print(f"Error: Input file must be a .rbxm or .rbxl file") + return False + + return True + + +def main(): + """Main CLI entry point""" + parser = argparse.ArgumentParser( + description='Export Roblox Studio files to .lua files', + prog='roexport' + ) + + parser.add_argument( + 'input', + help='Path to the .rbxm or .rbxl file to export' + ) + + parser.add_argument( + 'output', + nargs='?', + help='Output directory for .lua files (default: same as input file name)' + ) + + parser.add_argument( + '--flat', + action='store_true', + help='Export files in flat structure (no subdirectories)' + ) + + parser.add_argument( + '--no-headers', + action='store_true', + help='Don\'t add informational headers to exported files' + ) + + parser.add_argument( + '--no-summary', + action='store_true', + help='Don\'t generate export summary file' + ) + + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + + parser.add_argument( + '--version', + action='version', + version=f'RoExport {__version__}' + ) + + args = parser.parse_args() + + # Setup logging + setup_logging(args.verbose) + + # Validate input file + if not validate_input_file(args.input): + sys.exit(1) + + # Determine output directory + if args.output: + output_dir = args.output + else: + # Use input filename (without extension) as output directory + base_name = os.path.splitext(os.path.basename(args.input))[0] + output_dir = base_name + + # Create exporter + exporter = RobloxExporter( + preserve_hierarchy=not args.flat, + add_headers=not args.no_headers + ) + + try: + print(f"Exporting '{args.input}' to '{output_dir}'...") + + if args.no_summary: + exported_files = exporter.export_file(args.input, output_dir) + else: + exported_files = exporter.export_with_summary(args.input, output_dir) + + if exported_files: + print(f"Successfully exported {len(exported_files)} scripts!") + + # Show summary + if not args.no_summary: + counts = exporter.parser.get_script_count() + print("\nScript counts:") + for script_type, count in counts.items(): + if count > 0: + print(f" {script_type}: {count}") + + print(f"\nFiles exported to: {os.path.abspath(output_dir)}") + else: + print("No scripts found to export.") + sys.exit(1) + + except Exception as e: + print(f"Error: {e}") + if args.verbose: + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/roexport/exporter.py b/roexport/exporter.py new file mode 100644 index 0000000..8e4b3e0 --- /dev/null +++ b/roexport/exporter.py @@ -0,0 +1,225 @@ +""" +Roblox script exporter for converting parsed scripts to .lua files +""" + +import os +import logging +from typing import List, Optional +from .parser import RobloxScript, RobloxFileParser + +logger = logging.getLogger(__name__) + + +class RobloxExporter: + """Exports Roblox scripts to .lua files""" + + def __init__(self, preserve_hierarchy: bool = True, add_headers: bool = True): + """ + Initialize the exporter + + Args: + preserve_hierarchy: Whether to preserve the original folder structure + add_headers: Whether to add informational headers to exported files + """ + self.preserve_hierarchy = preserve_hierarchy + self.add_headers = add_headers + self.parser = RobloxFileParser() + + def export_file(self, input_path: str, output_dir: str) -> List[str]: + """ + Export all scripts from a Roblox file to .lua files + + Args: + input_path: Path to the .rbxm or .rbxl file + output_dir: Directory to export .lua files to + + Returns: + List of exported file paths + """ + # Parse the input file + scripts = self.parser.parse_file(input_path) + + if not scripts: + logger.warning(f"No scripts found in {input_path}") + return [] + + # Create output directory + os.makedirs(output_dir, exist_ok=True) + + exported_files = [] + + for script in scripts: + exported_path = self._export_script(script, output_dir) + if exported_path: + exported_files.append(exported_path) + + logger.info(f"Exported {len(exported_files)} scripts to {output_dir}") + return exported_files + + def export_scripts(self, scripts: List[RobloxScript], output_dir: str) -> List[str]: + """ + Export a list of scripts to .lua files + + Args: + scripts: List of RobloxScript objects to export + output_dir: Directory to export .lua files to + + Returns: + List of exported file paths + """ + os.makedirs(output_dir, exist_ok=True) + + exported_files = [] + + for script in scripts: + exported_path = self._export_script(script, output_dir) + if exported_path: + exported_files.append(exported_path) + + return exported_files + + def _export_script(self, script: RobloxScript, output_dir: str) -> Optional[str]: + """Export a single script to a .lua file""" + + try: + if self.preserve_hierarchy: + # Create full path with hierarchy + full_path = script.get_full_path() + file_path = os.path.join(output_dir, full_path) + + # Create intermediate directories + dir_path = os.path.dirname(file_path) + if dir_path: + os.makedirs(dir_path, exist_ok=True) + else: + # Flat structure - just use filename + file_path = os.path.join(output_dir, script.get_filename()) + + # Prepare content + content = self._prepare_content(script) + + # Write to file + with open(file_path, 'w', encoding='utf-8') as f: + f.write(content) + + logger.debug(f"Exported {script.name} to {file_path}") + return file_path + + except Exception as e: + logger.error(f"Failed to export script {script.name}: {e}") + return None + + def _prepare_content(self, script: RobloxScript) -> str: + """Prepare the content for export""" + + content_parts = [] + + if self.add_headers: + # Add informational header + header = self._generate_header(script) + content_parts.append(header) + + # Add the actual script content + content_parts.append(script.source) + + return '\n'.join(content_parts) + + def _generate_header(self, script: RobloxScript) -> str: + """Generate an informational header for the script""" + + header_lines = [ + "-- " + "="*50, + f"-- Script Name: {script.name}", + f"-- Script Type: {script.script_type}", + ] + + if script.parent_path: + header_lines.append(f"-- Parent Path: {script.parent_path}") + + header_lines.extend([ + "-- Exported by RoExport", + "-- " + "="*50, + "" + ]) + + return '\n'.join(header_lines) + + def create_init_files(self, output_dir: str, scripts: List[RobloxScript]): + """ + Create init.lua files for ModuleScript directories + + Args: + output_dir: The output directory + scripts: List of exported scripts + """ + if not self.preserve_hierarchy: + return + + # Find all directories that contain ModuleScripts + module_dirs = set() + for script in scripts: + if script.script_type == "ModuleScript" and script.parent_path: + module_dirs.add(os.path.join(output_dir, script.parent_path)) + + # Create init.lua files + for dir_path in module_dirs: + init_path = os.path.join(dir_path, "init.lua") + if not os.path.exists(init_path): + with open(init_path, 'w', encoding='utf-8') as f: + f.write("-- Auto-generated init.lua\n") + f.write("-- This directory contains ModuleScripts\n") + f.write("return {}\n") + + def generate_summary(self, scripts: List[RobloxScript], output_dir: str): + """Generate a summary file of exported scripts""" + + summary_path = os.path.join(output_dir, "EXPORT_SUMMARY.md") + + with open(summary_path, 'w', encoding='utf-8') as f: + f.write("# RoExport Summary\n\n") + + # Count by type + counts = {} + for script in scripts: + counts[script.script_type] = counts.get(script.script_type, 0) + 1 + + f.write("## Script Counts\n\n") + for script_type, count in counts.items(): + f.write(f"- {script_type}: {count}\n") + + f.write("\n## Exported Files\n\n") + + # Group by type + by_type = {} + for script in scripts: + if script.script_type not in by_type: + by_type[script.script_type] = [] + by_type[script.script_type].append(script) + + for script_type, type_scripts in by_type.items(): + f.write(f"### {script_type}\n\n") + for script in type_scripts: + path = script.get_full_path() if self.preserve_hierarchy else script.get_filename() + f.write(f"- `{path}`\n") + f.write("\n") + + logger.info(f"Generated summary at {summary_path}") + + def export_with_summary(self, input_path: str, output_dir: str) -> List[str]: + """ + Export scripts and generate summary + + Args: + input_path: Path to the .rbxm or .rbxl file + output_dir: Directory to export .lua files to + + Returns: + List of exported file paths + """ + exported_files = self.export_file(input_path, output_dir) + + if exported_files: + self.create_init_files(output_dir, self.parser.scripts) + self.generate_summary(self.parser.scripts, output_dir) + + return exported_files \ No newline at end of file diff --git a/roexport/parser.py b/roexport/parser.py new file mode 100644 index 0000000..276e33f --- /dev/null +++ b/roexport/parser.py @@ -0,0 +1,154 @@ +""" +Roblox file parser for extracting scripts from .rbxm files +""" + +import xml.etree.ElementTree as ET +import os +import logging +from typing import Dict, List, Tuple, Optional + +logger = logging.getLogger(__name__) + + +class RobloxScript: + """Represents a Roblox script with its metadata""" + + def __init__(self, name: str, source: str, script_type: str, parent_path: str = ""): + self.name = name + self.source = source + self.script_type = script_type # Script, LocalScript, ModuleScript + self.parent_path = parent_path + + def get_filename(self) -> str: + """Generate appropriate filename for the script""" + # Clean the name for filesystem + clean_name = "".join(c for c in self.name if c.isalnum() or c in (' ', '-', '_')).rstrip() + if not clean_name: + clean_name = "script" + + # Add appropriate extension + if self.script_type == "ModuleScript": + return f"{clean_name}.lua" + elif self.script_type == "LocalScript": + return f"{clean_name}.client.lua" + else: # Regular Script + return f"{clean_name}.server.lua" + + def get_full_path(self) -> str: + """Get the full path including parent hierarchy""" + if self.parent_path: + return os.path.join(self.parent_path, self.get_filename()) + return self.get_filename() + + +class RobloxFileParser: + """Parser for Roblox Studio files (.rbxm and .rbxl)""" + + def __init__(self): + self.scripts: List[RobloxScript] = [] + + def parse_file(self, file_path: str) -> List[RobloxScript]: + """ + Parse a Roblox file and extract all scripts + + Args: + file_path: Path to the .rbxm or .rbxl file + + Returns: + List of RobloxScript objects + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + if not file_path.lower().endswith(('.rbxm', '.rbxl')): + raise ValueError("File must be a .rbxm or .rbxl file") + + try: + tree = ET.parse(file_path) + root = tree.getroot() + + self.scripts = [] + self._parse_element(root, "") + + logger.info(f"Parsed {len(self.scripts)} scripts from {file_path}") + return self.scripts + + except ET.ParseError as e: + raise ValueError(f"Invalid XML format in file {file_path}: {e}") + except Exception as e: + raise RuntimeError(f"Error parsing file {file_path}: {e}") + + def _parse_element(self, element: ET.Element, parent_path: str): + """Recursively parse XML elements to find scripts""" + + # Check if this element is a script + class_name = element.get('class', '') + if class_name in ['Script', 'LocalScript', 'ModuleScript']: + script = self._extract_script(element, class_name, parent_path) + if script: + self.scripts.append(script) + + # Build path for children + name = self._get_element_name(element) + if name and class_name not in ['Script', 'LocalScript', 'ModuleScript']: + current_path = os.path.join(parent_path, name) if parent_path else name + else: + current_path = parent_path + + # Recursively parse children + for child in element: + self._parse_element(child, current_path) + + def _extract_script(self, element: ET.Element, script_type: str, parent_path: str) -> Optional[RobloxScript]: + """Extract script information from an XML element""" + + name = self._get_element_name(element) + if not name: + name = f"unnamed_{script_type.lower()}" + + # Find the Source property + source = "" + for prop in element.findall('.//Properties'): + for child in prop: + if child.get('name') == 'Source': + source = child.text or "" + break + + # Also check direct string properties + if not source: + for string_elem in element.findall('.//string[@name="Source"]'): + source = string_elem.text or "" + break + + if source.strip(): # Only include scripts with actual content + return RobloxScript(name, source, script_type, parent_path) + + return None + + def _get_element_name(self, element: ET.Element) -> str: + """Extract the name of an element""" + + # Try to find name in Properties + for prop in element.findall('.//Properties'): + for child in prop: + if child.get('name') == 'Name': + return child.text or "" + + # Try direct string property + for string_elem in element.findall('.//string[@name="Name"]'): + return string_elem.text or "" + + # Fallback to referent or class + return element.get('referent', element.get('class', '')) + + def get_scripts_by_type(self, script_type: str) -> List[RobloxScript]: + """Get all scripts of a specific type""" + return [script for script in self.scripts if script.script_type == script_type] + + def get_script_count(self) -> Dict[str, int]: + """Get count of scripts by type""" + counts = {"Script": 0, "LocalScript": 0, "ModuleScript": 0} + for script in self.scripts: + if script.script_type in counts: + counts[script.script_type] += 1 + return counts \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f56b860 --- /dev/null +++ b/setup.py @@ -0,0 +1,60 @@ +""" +Setup script for RoExport +""" + +from setuptools import setup, find_packages +import os + +# Read the README file +with open("README.md", "r", encoding="utf-8") as fh: + long_description = fh.read() + +# Read version from __init__.py +def get_version(): + with open("roexport/__init__.py", "r", encoding="utf-8") as f: + for line in f: + if line.startswith("__version__"): + return line.split("=")[1].strip().strip('"') + return "1.0.0" + +version_info = { + "__version__": get_version(), + "__author__": "N3uralCreativity", + "__description__": "Export Roblox Studio files (.rbxm) to .lua files usable in any IDE" +} + +setup( + name="roexport", + version=version_info["__version__"], + author=version_info["__author__"], + description=version_info["__description__"], + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/N3uralCreativity/RoExport", + packages=find_packages(), + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Topic :: Software Development :: Tools", + "Topic :: Games/Entertainment", + ], + python_requires=">=3.7", + entry_points={ + "console_scripts": [ + "roexport=roexport.cli:main", + ], + }, + keywords="roblox export lua script development", + project_urls={ + "Bug Reports": "https://github.com/N3uralCreativity/RoExport/issues", + "Source": "https://github.com/N3uralCreativity/RoExport", + }, +) \ No newline at end of file