diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c26bc7795c..5f4954c851 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ Always reference these instructions first and fallback to search or bash command - **CRITICAL**: Some components require Microsoft.DotNet.RemoteExecutor package that may fail due to network issues with Azure DevOps feeds. This is NORMAL and documented. - Build individual core projects (RECOMMENDED approach): - `dotnet restore Content.Shared/Content.Shared.csproj` -- takes 2 seconds - - `dotnet restore Content.Server/Content.Server.csproj` -- takes 2 seconds + - `dotnet restore Content.Server/Content.Server.csproj` -- takes 2 seconds - `dotnet restore Content.Client/Content.Client.csproj` -- takes 2 seconds - `dotnet build Content.Shared/Content.Shared.csproj --configuration DebugOpt --no-restore` -- takes 60-90 seconds. NEVER CANCEL. Set timeout to 120+ seconds. - `dotnet build Content.Server/Content.Server.csproj --configuration DebugOpt --no-restore` -- takes 30-45 seconds. NEVER CANCEL. Set timeout to 90+ seconds. @@ -48,7 +48,7 @@ Always reference these instructions first and fallback to search or bash command - **WORKAROUND**: Build individual core projects instead of full solution - **IMPACT**: Some tools (YAMLLinter, full test suite) may not work, but core game components work fine -### Build Configuration Issues +### Build Configuration Issues - **Use DebugOpt configuration** for most development work - **Use Tools configuration** for running development tools - **Never use default Debug** configuration as it may have performance issues @@ -62,7 +62,7 @@ Always reference these instructions first and fallback to search or bash command - **Content.Tests**: Unit tests for shared components - **Content.IntegrationTests**: Integration tests (may fail due to dependency issues) -### Additional Components +### Additional Components - **Content.Tools**: Development and content creation tools - **Content.YAMLLinter**: YAML validation tool (may fail due to dependency issues) - **RobustToolbox**: Game engine (git submodule) @@ -95,7 +95,7 @@ Always reference these instructions first and fallback to search or bash command ### Changelog Documentation in Pull Requests - **REQUIRED**: Include changelog entries in PR descriptions for player-visible changes - **FORMAT**: Use the following template in PR descriptions: -- **AUTHOR**: Set author Copilot (AI) +- **AUTHOR**: Set author Copilot AI ``` :cl: Author - add: Добавлено веселье. @@ -105,7 +105,7 @@ Always reference these instructions first and fallback to search or bash command ``` - **CHANGE TYPES**: - `add`: New features, content, or functionality - - `remove`: Removed features, content, or functionality + - `remove`: Removed features, content, or functionality - `tweak`: Modified existing features or balance changes - `fix`: Bug fixes or corrections - **AUTOMATION**: Changelog entries are automatically processed and added to game changelogs after PR merge @@ -125,7 +125,7 @@ git submodule update --init --recursive # Build core (run after changes) dotnet build Content.Shared/Content.Shared.csproj --configuration DebugOpt --no-restore -dotnet build Content.Server/Content.Server.csproj --configuration DebugOpt --no-restore +dotnet build Content.Server/Content.Server.csproj --configuration DebugOpt --no-restore dotnet build Content.Client/Content.Client.csproj --configuration DebugOpt --no-restore # Run (for testing) @@ -139,7 +139,7 @@ timeout 30s dotnet run --project Content.Server --configuration DebugOpt --no-bu ### Build Timing Expectations - **NEVER CANCEL** builds - they can take 30-90 seconds per component - Content.Shared: 60-90 seconds (largest component) -- Content.Server: 30-45 seconds +- Content.Server: 30-45 seconds - Content.Client: 30-45 seconds - Set timeouts to 120+ seconds for builds to avoid premature cancellation - **Total build time**: 2-4 minutes for all core components diff --git a/.github/scripts/README.md b/.github/scripts/README.md deleted file mode 100644 index 322c4edc47..0000000000 --- a/.github/scripts/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# GitHub Workflow Scripts - -This directory contains scripts used by GitHub Actions workflows. - -## XAML Preview System - -### xaml-preview.py - -Enhanced Python script that processes XAML files and generates comprehensive previews for pull request comments. - -#### Features - -- **Enhanced XAML Analysis**: Deep parsing of XAML structure with hierarchy analysis -- **Layout Complexity Assessment**: Analyzes nesting depth, control counts, and layout patterns -- **Visual Structure Diagrams**: ASCII tree diagrams with emoji icons for different control types -- **UI Control Inventory**: Detailed categorization of containers and controls -- **File Metadata**: Shows file size, line count, namespaces, and complexity metrics -- **Change Detection**: Handles added, modified, and removed files with appropriate formatting -- **Error Handling**: Gracefully handles malformed XAML or missing files - -### xaml_mockup_generator.py - -Visual mockup generator that creates basic layout preview images from XAML files. - -#### Features - -- **Visual Layout Generation**: Creates PNG mockups showing UI structure -- **Control-Specific Styling**: Different visual styles for buttons, labels, containers -- **Hierarchical Rendering**: Shows nested container relationships -- **Size Intelligence**: Attempts to respect explicit sizing attributes -- **Fallback Support**: Works independently if main preview script fails - -### Integration - -These scripts work together in the `.github/workflows/xaml-preview.yml` workflow: - -1. **Trigger**: Activates on pull requests that modify `.xaml` files -2. **Analysis**: Enhanced XAML structure parsing and complexity analysis -3. **Image Generation**: Creates visual mockups for added/modified files -4. **Preview Generation**: Produces comprehensive markdown previews -5. **Comment Management**: Posts/updates PR comments with complete previews -6. **Artifact Upload**: Makes generated images available as downloadable artifacts - -### Usage - -```bash -# Enhanced text preview -python3 xaml-preview.py --modified "file1.xaml file2.xaml" --added "file3.xaml" --removed "file4.xaml" - -# Visual mockup generation -python3 xaml_mockup_generator.py input.xaml output.png -``` - -### Enhanced Output Format - -The enhanced preview includes: - -**For each XAML file:** -- 📊 **Analysis Summary**: Root element, complexity level, nesting depth, file size -- 📎 **Visual Mockup**: Download link to generated PNG layout preview -- 🎨 **Structure Diagram**: ASCII tree with emoji icons showing UI hierarchy -- 📋 **Source Code**: Collapsible section with syntax-highlighted XAML content -- 🔍 **Detailed Metrics**: Container types, control inventory, layout patterns - -**Overall Summary:** -- Quick navigation for multiple files -- Change type breakdown (added/modified/removed counts) -- Artifact download information -- Enhanced footer with feature explanation - -### Control Icon Legend - -The structure diagrams use intuitive emoji icons: -- 🪟 Windows (Window, FancyWindow) -- 📦 Containers (BoxContainer, VBoxContainer, HBoxContainer) -- 📂 Split Containers -- 📜 Scroll Containers -- 🔘 Buttons -- 🏷️ Labels -- 📝 Text Inputs (TextEdit, LineEdit) -- 📄 Rich Text Labels -- ▢ Generic Controls - -This enhanced system provides developers with immediate visual feedback on XAML changes, including both structural analysis and basic visual previews, without requiring local builds. \ No newline at end of file diff --git a/.github/scripts/xaml-preview.py b/.github/scripts/xaml-preview.py deleted file mode 100755 index bba580834b..0000000000 --- a/.github/scripts/xaml-preview.py +++ /dev/null @@ -1,512 +0,0 @@ -#!/usr/bin/env python3 -""" -XAML Preview Generator -Processes XAML files and generates formatted previews for PR comments. -Enhanced version with visual hierarchy and better analysis. -""" - -import os -import sys -import xml.etree.ElementTree as ET -import argparse -from pathlib import Path -from typing import List, Dict, Optional, Any -from dataclasses import dataclass -import re - - -@dataclass -class ControlInfo: - """Information about a UI control parsed from XAML.""" - name: str - element_type: str - attributes: Dict[str, str] - children: List['ControlInfo'] - text_content: Optional[str] = None - namespace: Optional[str] = None - - def has_layout_properties(self) -> bool: - """Check if control has layout-related properties.""" - layout_props = {'Width', 'Height', 'MinWidth', 'MinHeight', 'MaxWidth', 'MaxHeight', - 'HorizontalAlignment', 'VerticalAlignment', 'Margin', 'Padding', - 'HorizontalExpand', 'VerticalExpand'} - return any(prop in self.attributes for prop in layout_props) - - def get_size_info(self) -> str: - """Get size and layout information as a formatted string.""" - size_parts = [] - if 'Width' in self.attributes: - size_parts.append(f"W:{self.attributes['Width']}") - if 'Height' in self.attributes: - size_parts.append(f"H:{self.attributes['Height']}") - if 'MinWidth' in self.attributes: - size_parts.append(f"MinW:{self.attributes['MinWidth']}") - if 'MinHeight' in self.attributes: - size_parts.append(f"MinH:{self.attributes['MinHeight']}") - - align_parts = [] - if 'HorizontalAlignment' in self.attributes: - align_parts.append(f"HA:{self.attributes['HorizontalAlignment']}") - if 'VerticalAlignment' in self.attributes: - align_parts.append(f"VA:{self.attributes['VerticalAlignment']}") - - all_parts = size_parts + align_parts - return " | ".join(all_parts) if all_parts else "" - - -def parse_xaml_structure(file_path: str) -> Dict[str, Any]: - """Parse XAML file and extract detailed structural information.""" - try: - tree = ET.parse(file_path) - root = tree.getroot() - - # Extract basic information - info = { - 'root_element': root.tag, - 'attributes': dict(root.attrib), - 'children_count': len(list(root)), - 'has_content': bool(root.text and root.text.strip()), - 'namespaces': {}, - 'controls': [], - 'file_size': os.path.getsize(file_path), - 'line_count': 0, - 'structure': None, - 'layout_analysis': {}, - 'ui_complexity': 'Simple' - } - - # Count lines - with open(file_path, 'r', encoding='utf-8') as f: - info['line_count'] = len(f.readlines()) - - # Extract namespaces - for key, value in root.attrib.items(): - if key.startswith('xmlns'): - namespace_name = key.split(':', 1)[1] if ':' in key else 'default' - info['namespaces'][namespace_name] = value - - # Parse structure recursively - info['structure'] = parse_control_recursive(root) - - # Analyze layout complexity - info['layout_analysis'] = analyze_layout_complexity(info['structure']) - - # Determine UI complexity - total_controls = count_controls(info['structure']) - if total_controls > 20: - info['ui_complexity'] = 'Complex' - elif total_controls > 10: - info['ui_complexity'] = 'Moderate' - - # Extract all unique control types - extract_control_types(info['structure'], info['controls']) - - return info - - except ET.ParseError as e: - return { - 'error': f'XML Parse Error: {str(e)}', - 'file_size': os.path.getsize(file_path) if os.path.exists(file_path) else 0, - 'line_count': 0 - } - except Exception as e: - return { - 'error': f'Error: {str(e)}', - 'file_size': os.path.getsize(file_path) if os.path.exists(file_path) else 0, - 'line_count': 0 - } - - -def parse_control_recursive(element: ET.Element) -> ControlInfo: - """Recursively parse XML element into ControlInfo structure.""" - # Clean up element name - name = element.tag.split('}')[-1] if '}' in element.tag else element.tag - namespace = element.tag.split('}')[0][1:] if '}' in element.tag else None - - control = ControlInfo( - name=name, - element_type=name, - attributes=dict(element.attrib), - children=[], - text_content=element.text.strip() if element.text and element.text.strip() else None, - namespace=namespace - ) - - # Parse child elements - for child in element: - control.children.append(parse_control_recursive(child)) - - return control - - -def extract_control_types(control: ControlInfo, types_list: List[str]): - """Extract all unique control types from the structure.""" - if control.element_type not in types_list: - types_list.append(control.element_type) - - for child in control.children: - extract_control_types(child, types_list) - - -def count_controls(control: ControlInfo) -> int: - """Count total number of controls in the structure.""" - return 1 + sum(count_controls(child) for child in control.children) - - -def analyze_layout_complexity(structure: ControlInfo) -> Dict[str, Any]: - """Analyze the layout complexity and patterns.""" - analysis = { - 'total_controls': count_controls(structure), - 'max_depth': get_max_depth(structure), - 'container_types': [], - 'has_complex_layout': False, - 'layout_patterns': [] - } - - # Find container types - find_containers(structure, analysis['container_types']) - - # Check for complex layout patterns - if analysis['max_depth'] > 5: - analysis['has_complex_layout'] = True - analysis['layout_patterns'].append('Deep nesting detected') - - if len(analysis['container_types']) > 3: - analysis['layout_patterns'].append('Multiple container types') - - return analysis - - -def get_max_depth(control: ControlInfo, current_depth: int = 0) -> int: - """Get maximum nesting depth of the control structure.""" - if not control.children: - return current_depth - - return max(get_max_depth(child, current_depth + 1) for child in control.children) - - -def find_containers(control: ControlInfo, container_list: List[str]): - """Find all container control types.""" - container_types = {'BoxContainer', 'SplitContainer', 'ScrollContainer', 'GridContainer', - 'TabContainer', 'VBoxContainer', 'HBoxContainer', 'Control', 'Panel'} - - if control.element_type in container_types and control.element_type not in container_list: - container_list.append(control.element_type) - - for child in control.children: - find_containers(child, container_list) - - -def generate_structure_diagram(control: ControlInfo, indent: int = 0, max_depth: int = 6) -> str: - """Generate a visual ASCII diagram of the UI structure.""" - if indent > max_depth: - return " " * indent + "... (truncated)\n" - - # Create the visual representation - prefix = " " * indent - - # Choose appropriate icon for control type - icon = get_control_icon(control.element_type) - - # Build the line - line = f"{prefix}{icon} {control.element_type}" - - # Add important attributes - important_attrs = [] - if 'Name' in control.attributes: - important_attrs.append(f"Name=\"{control.attributes['Name']}\"") - if 'Text' in control.attributes: - text = control.attributes['Text'][:20] + "..." if len(control.attributes['Text']) > 20 else control.attributes['Text'] - important_attrs.append(f"Text=\"{text}\"") - - # Add size information - size_info = control.get_size_info() - if size_info: - important_attrs.append(f"[{size_info}]") - - if important_attrs: - line += f" ({', '.join(important_attrs)})" - - line += "\n" - - # Add children - result = line - for child in control.children: - result += generate_structure_diagram(child, indent + 1, max_depth) - - return result - - -def get_control_icon(control_type: str) -> str: - """Get an appropriate icon/symbol for the control type.""" - icons = { - 'Window': '🪟', - 'FancyWindow': '🪟', - 'BoxContainer': '📦', - 'VBoxContainer': '📦', - 'HBoxContainer': '📦', - 'SplitContainer': '📂', - 'ScrollContainer': '📜', - 'GridContainer': '⊞', - 'TabContainer': '📑', - 'Button': '🔘', - 'Label': '🏷️', - 'TextEdit': '📝', - 'LineEdit': '📝', - 'RichTextLabel': '📄', - 'Panel': '▢', - 'Control': '▢', - 'Separator': '━', - 'VSeparator': '│', - 'HSeparator': '─', - 'ProgressBar': '█', - 'CheckBox': '☐', - 'OptionButton': '◯', - 'ItemList': '📋', - 'Tree': '🌳', - 'TextureRect': '🖼️', - 'NinePatchRect': '🖼️', - } - return icons.get(control_type, '▢') - - -def format_file_info(file_path: str, info: Dict, change_type: str) -> str: - """Format file information for display with enhanced visual preview.""" - icon_map = { - 'added': '✨', - 'modified': '📝', - 'removed': '🗑️' - } - - icon = icon_map.get(change_type, '📄') - relative_path = file_path - - if 'error' in info: - return f"## {icon} {change_type.title()}: `{relative_path}`\n\n⚠️ **Error processing file:** {info['error']}\n\n" - - # File size formatting - size = info['file_size'] - if size > 1024 * 1024: - size_str = f"{size / (1024 * 1024):.1f} MB" - elif size > 1024: - size_str = f"{size / 1024:.1f} KB" - else: - size_str = f"{size} bytes" - - # Build summary with enhanced information - summary_parts = [] - - # Clean up root element name for better readability - root_element = info.get('root_element', 'Unknown') - if '}' in root_element: - root_element = root_element.split('}')[-1] - summary_parts.append(f"**Root Element:** `{root_element}`") - - # Enhanced control information - layout_analysis = info.get('layout_analysis', {}) - total_controls = layout_analysis.get('total_controls', len(info.get('controls', []))) - summary_parts.append(f"**UI Complexity:** {info.get('ui_complexity', 'Unknown')} ({total_controls} controls)") - - # Layout depth information - max_depth = layout_analysis.get('max_depth', 0) - if max_depth > 0: - summary_parts.append(f"**Nesting Depth:** {max_depth} levels") - - summary_parts.append(f"**File Size:** {size_str} ({info['line_count']} lines)") - - # Container information - container_types = layout_analysis.get('container_types', []) - if container_types: - container_list = ', '.join(f"`{ctrl}`" for ctrl in container_types[:5]) - if len(container_types) > 5: - container_list += f" and {len(container_types) - 5} more" - summary_parts.append(f"**Layout Containers:** {container_list}") - - # Control types used - if info.get('controls'): - controls_list = ', '.join(f"`{ctrl}`" for ctrl in info['controls'][:8]) - if len(info['controls']) > 8: - controls_list += f" and {len(info['controls']) - 8} more" - summary_parts.append(f"**UI Controls:** {controls_list}") - - # Layout patterns - layout_patterns = layout_analysis.get('layout_patterns', []) - if layout_patterns: - summary_parts.append(f"**Layout Notes:** {', '.join(layout_patterns)}") - - # Show interesting namespaces - namespaces = info.get('namespaces', {}) - if namespaces and len(namespaces) > 1: - ns_list = [] - for ns, uri in namespaces.items(): - if ns != 'default' and 'spacestation14.io' not in uri: - ns_list.append(f"`{ns}`") - if ns_list: - summary_parts.append(f"**Custom Namespaces:** {', '.join(ns_list[:3])}") - - summary = '\n'.join(f"- {part}" for part in summary_parts) - - # Check if mockup image exists - mockup_section = "" - if change_type in ['added', 'modified']: - mockup_filename = os.path.basename(file_path).replace('.xaml', '_mockup.png') - mockup_path = f"xaml-previews/{mockup_filename}" - if os.path.exists(mockup_path): - # Get the workflow run URL for artifact download - run_id = os.environ.get('GITHUB_RUN_ID', 'unknown') - repo = os.environ.get('GITHUB_REPOSITORY', 'space-sunrise/sunrise-station') - - mockup_section = f""" -### 🖼️ Visual Mockup - -> **Note:** This is a simplified visual representation showing the basic layout structure. -> The actual UI may look different with proper styling and content. - -📎 **Download mockup image:** [`{mockup_filename}`](https://github.com/{repo}/actions/runs/{run_id}/artifacts) (Look for "xaml-previews" artifact) - -*Mockup generated automatically - shows basic UI layout and structure* -""" - - # Generate structure diagram - structure_diagram = "" - if 'structure' in info and info['structure']: - structure_diagram = f""" -### 🎨 UI Structure Preview - -``` -{generate_structure_diagram(info['structure']).rstrip()} -``` -""" - - # Read file content for preview (reduced size) - content_preview = "" - try: - with open(file_path, 'r', encoding='utf-8') as f: - lines = f.readlines() - preview_lines = lines[:20] # Show first 20 lines - content_preview = ''.join(preview_lines) - if len(lines) > 20: - content_preview += f"\n... (showing first 20 of {len(lines)} lines)" - except Exception as e: - content_preview = f"Error reading file: {str(e)}" - - return f"""## {icon} {change_type.title()}: `{relative_path}` - -{summary} -{mockup_section} -{structure_diagram} -
📋 Click to view XAML source - -```xml -{content_preview} -``` - -
- -""" - - -def process_xaml_files(modified_files: List[str], added_files: List[str], removed_files: List[str]) -> str: - """Process all XAML files and generate enhanced preview content.""" - - # Filter to only include XAML files - modified_xaml = [f for f in modified_files if f.endswith('.xaml')] - added_xaml = [f for f in added_files if f.endswith('.xaml')] - removed_xaml = [f for f in removed_files if f.endswith('.xaml')] - - preview_content = "# 🎨 XAML Preview Bot\n\n" - - total_files = len(modified_xaml) + len(added_xaml) + len(removed_xaml) - - if total_files == 0: - return preview_content + "No XAML files were changed in this PR.\n" - - # Enhanced summary - summary_parts = [] - if added_xaml: - summary_parts.append(f"**{len(added_xaml)} added**") - if modified_xaml: - summary_parts.append(f"**{len(modified_xaml)} modified**") - if removed_xaml: - summary_parts.append(f"**{len(removed_xaml)} removed**") - - preview_content += f"Found **{total_files}** XAML file(s) changed: {', '.join(summary_parts)}\n\n" - - # Add quick navigation if there are many files - if total_files > 3: - preview_content += "### Quick Navigation\n" - all_files = [(f, 'added') for f in added_xaml] + [(f, 'modified') for f in modified_xaml] + [(f, 'removed') for f in removed_xaml] - for file_path, change_type in all_files: - icon = {'added': '✨', 'modified': '📝', 'removed': '🗑️'}[change_type] - file_name = os.path.basename(file_path) - preview_content += f"- {icon} [{file_name}](#{change_type}-{file_name.lower().replace('.', '').replace(' ', '-')})\n" - preview_content += "\n---\n\n" - - # Process added files - for file_path in added_xaml: - if os.path.exists(file_path): - info = parse_xaml_structure(file_path) - preview_content += format_file_info(file_path, info, 'added') - else: - preview_content += f"## ✨ Added: `{file_path}`\n\n⚠️ **File not found in current checkout**\n\n" - - # Process modified files - for file_path in modified_xaml: - if os.path.exists(file_path): - info = parse_xaml_structure(file_path) - preview_content += format_file_info(file_path, info, 'modified') - else: - preview_content += f"## 📝 Modified: `{file_path}`\n\n⚠️ **File not found in current checkout**\n\n" - - # Process removed files - for file_path in removed_xaml: - preview_content += f"## 🗑️ Removed: `{file_path}`\n\n*This XAML file was deleted from the codebase.*\n\n" - - # Add enhanced footer - preview_content += "\n---\n\n" - preview_content += "### 🤖 About This Preview\n\n" - preview_content += "This enhanced preview shows the UI structure and layout analysis of your XAML changes. " - preview_content += "The structure diagram uses icons to represent different control types and shows the hierarchy " - preview_content += "to help you understand the layout without building locally.\n\n" - - # Check if any mockups were generated - mockup_count = 0 - if os.path.exists('xaml-previews'): - mockup_count = len([f for f in os.listdir('xaml-previews') if f.endswith('.png')]) - - if mockup_count > 0: - run_id = os.environ.get('GITHUB_RUN_ID', 'unknown') - repo = os.environ.get('GITHUB_REPOSITORY', 'space-sunrise/sunrise-station') - preview_content += f"**📎 {mockup_count} visual mockup(s) generated** - " - preview_content += f"Download from [workflow artifacts](https://github.com/{repo}/actions/runs/{run_id}/artifacts) " - preview_content += "(look for 'xaml-previews' artifact)\n\n" - - preview_content += "*Preview automatically generated by XAML Preview Bot*" - - return preview_content - - -def main(): - parser = argparse.ArgumentParser(description='Generate XAML file previews') - parser.add_argument('--modified', default='', help='Space-separated list of modified files') - parser.add_argument('--added', default='', help='Space-separated list of added files') - parser.add_argument('--removed', default='', help='Space-separated list of removed files') - - args = parser.parse_args() - - # Parse file lists - modified_files = [f.strip() for f in args.modified.split() if f.strip()] - added_files = [f.strip() for f in args.added.split() if f.strip()] - removed_files = [f.strip() for f in args.removed.split() if f.strip()] - - # Generate preview content - preview_content = process_xaml_files(modified_files, added_files, removed_files) - - # Output for GitHub Actions - print("PREVIEW_CONTENT< Dict: - # Simplified version for standalone use - try: - tree = ET.parse(file_path) - root = tree.getroot() - - def parse_control_recursive(element: ET.Element) -> ControlInfo: - name = element.tag.split('}')[-1] if '}' in element.tag else element.tag - namespace = element.tag.split('}')[0][1:] if '}' in element.tag else None - - control = ControlInfo( - name=name, - element_type=name, - attributes=dict(element.attrib), - children=[], - text_content=element.text.strip() if element.text and element.text.strip() else None, - namespace=namespace - ) - - for child in element: - control.children.append(parse_control_recursive(child)) - - return control - - return {'structure': parse_control_recursive(root)} - except Exception as e: - return {'error': str(e)} - - -class XamlMockupGenerator: - def __init__(self, width: int = 800, height: int = 600): - self.width = width - self.height = height - self.colors = { - 'background': '#2e3440', - 'panel': '#3b4252', - 'button': '#5e81ac', - 'text': '#eceff4', - 'accent': '#88c0d0', - 'border': '#4c566a', - 'highlight': '#8fbcbb' - } - - def generate_mockup(self, xaml_file: str, output_file: str) -> bool: - """Generate a visual mockup of the XAML file.""" - try: - # Parse the XAML structure - info = parse_xaml_structure(xaml_file) - if 'error' in info: - print(f"Error parsing XAML: {info['error']}") - return False - - structure = info.get('structure') - if not structure: - print("No structure found in XAML") - return False - - # Create image - image = Image.new('RGB', (self.width, self.height), self.colors['background']) - draw = ImageDraw.Draw(image) - - # Try to load a font - try: - font = ImageFont.truetype("DejaVuSans.ttf", 12) - title_font = ImageFont.truetype("DejaVuSans-Bold.ttf", 16) - except: - try: - font = ImageFont.truetype("arial.ttf", 12) - title_font = ImageFont.truetype("arialbd.ttf", 16) - except: - font = ImageFont.load_default() - title_font = ImageFont.load_default() - - # Draw title - title = f"XAML Mockup: {os.path.basename(xaml_file)}" - draw.text((10, 10), title, fill=self.colors['text'], font=title_font) - - # Draw the UI structure - self._draw_control(draw, structure, 10, 40, self.width - 20, self.height - 50, font) - - # Save the image - image.save(output_file, 'PNG') - return True - - except Exception as e: - print(f"Error generating mockup: {e}") - return False - - def _draw_control(self, draw: ImageDraw.ImageDraw, control: ControlInfo, - x: int, y: int, width: int, height: int, font: ImageFont.ImageFont, - level: int = 0) -> int: - """Draw a control and its children, returns the Y position after drawing.""" - if level > 10: # Prevent infinite recursion - return y - - # Determine control type and style - control_style = self._get_control_style(control.element_type) - - # Calculate dimensions - control_height = control_style['height'] - padding = max(2, 8 - level) - - # Draw the control background - if control_style['draw_background']: - draw.rectangle([x, y, x + width, y + control_height], - fill=control_style['background'], - outline=control_style['border']) - - # Draw control label - label_text = self._get_control_label(control) - if label_text: - text_color = control_style.get('text_color', self.colors['text']) - # Limit text length to fit - max_chars = max(10, (width - 20) // 8) - if len(label_text) > max_chars: - label_text = label_text[:max_chars - 3] + "..." - draw.text((x + padding, y + padding), label_text, fill=text_color, font=font) - - # Draw children for container controls - current_y = y + control_height + padding - available_height = height - (current_y - y) - - if control.children and available_height > 20: - child_height = max(20, available_height // max(1, len(control.children))) - - for i, child in enumerate(control.children): - if current_y >= y + height - 10: # Stop if we're out of space - break - - child_y = current_y - remaining_height = y + height - child_y - child_actual_height = min(child_height, remaining_height) - - if child_actual_height > 10: - current_y = self._draw_control(draw, child, - x + padding * 2, child_y, - width - padding * 4, child_actual_height, - font, level + 1) - current_y += padding - - return max(current_y, y + control_height) - - def _get_control_style(self, control_type: str) -> Dict: - """Get visual style for a control type.""" - styles = { - 'Window': { - 'height': 25, - 'background': self.colors['panel'], - 'border': self.colors['border'], - 'draw_background': True, - 'text_color': self.colors['text'] - }, - 'FancyWindow': { - 'height': 25, - 'background': self.colors['panel'], - 'border': self.colors['accent'], - 'draw_background': True, - 'text_color': self.colors['text'] - }, - 'Button': { - 'height': 20, - 'background': self.colors['button'], - 'border': self.colors['border'], - 'draw_background': True, - 'text_color': self.colors['text'] - }, - 'Label': { - 'height': 15, - 'background': None, - 'border': None, - 'draw_background': False, - 'text_color': self.colors['text'] - }, - 'TextEdit': { - 'height': 18, - 'background': '#4c566a', - 'border': self.colors['border'], - 'draw_background': True, - 'text_color': self.colors['text'] - }, - 'LineEdit': { - 'height': 18, - 'background': '#4c566a', - 'border': self.colors['border'], - 'draw_background': True, - 'text_color': self.colors['text'] - }, - 'BoxContainer': { - 'height': 15, - 'background': None, - 'border': '#434c5e', - 'draw_background': False, - 'text_color': self.colors['highlight'] - }, - 'ScrollContainer': { - 'height': 15, - 'background': '#3b4252', - 'border': self.colors['border'], - 'draw_background': True, - 'text_color': self.colors['highlight'] - } - } - - # Default style - default_style = { - 'height': 15, - 'background': None, - 'border': '#434c5e', - 'draw_background': False, - 'text_color': self.colors['text'] - } - - return styles.get(control_type, default_style) - - def _get_control_label(self, control: ControlInfo) -> str: - """Get display label for a control.""" - # Priority: Name > Text > Type - if 'Name' in control.attributes: - return f"{control.element_type}: {control.attributes['Name']}" - elif 'Text' in control.attributes: - text = control.attributes['Text'] - # Clean up localization keys - if text.startswith('{Loc'): - text = text.replace('{Loc ', '').replace("'", '').replace('}', '') - return f"{control.element_type}: {text}" - else: - return control.element_type - - -def main(): - if len(sys.argv) < 2: - print("Usage: python3 xaml_mockup_generator.py [output-file]") - return - - xaml_file = sys.argv[1] - output_file = sys.argv[2] if len(sys.argv) > 2 else xaml_file.replace('.xaml', '_mockup.png') - - if not os.path.exists(xaml_file): - print(f"XAML file not found: {xaml_file}") - return - - generator = XamlMockupGenerator() - if generator.generate_mockup(xaml_file, output_file): - print(f"Mockup generated: {output_file}") - else: - print("Failed to generate mockup") - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/.github/workflows/xaml-preview.yml b/.github/workflows/xaml-preview.yml deleted file mode 100644 index 1217387fe5..0000000000 --- a/.github/workflows/xaml-preview.yml +++ /dev/null @@ -1,112 +0,0 @@ -name: XAML Preview - -on: - pull_request_target: - paths: - - '**.xaml' - -jobs: - preview: - name: Generate XAML Previews - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4.2.2 - - - name: Get changed files - id: files - uses: Ana06/get-changed-files@v2.3.0 - with: - format: 'space-delimited' - filter: | - **.xaml - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install Pillow - - - name: Generate XAML previews - id: preview - run: | - # Check if any XAML files were actually changed - xaml_files="" - for file in ${{ steps.files.outputs.modified }} ${{ steps.files.outputs.added }} ${{ steps.files.outputs.removed }}; do - if [[ "$file" == *.xaml ]]; then - xaml_files="$xaml_files $file" - fi - done - - if [ -z "$xaml_files" ]; then - echo "No XAML files changed, skipping preview generation" - echo "skip_preview=true" >> $GITHUB_OUTPUT - exit 0 - fi - - echo "Processing XAML files: $xaml_files" - - # Create output directory for images - mkdir -p xaml-previews - - # Generate mockup images for existing XAML files - for file in ${{ steps.files.outputs.modified }} ${{ steps.files.outputs.added }}; do - if [[ "$file" == *.xaml ]] && [[ -f "$file" ]]; then - echo "Generating mockup for $file" - python3 ./.github/scripts/xaml_mockup_generator.py "$file" "xaml-previews/$(basename "$file" .xaml)_mockup.png" || echo "Failed to generate mockup for $file" - fi - done - - # Generate text preview - python3 ./.github/scripts/xaml-preview.py \ - --modified "${{ steps.files.outputs.modified }}" \ - --added "${{ steps.files.outputs.added }}" \ - --removed "${{ steps.files.outputs.removed }}" \ - >> $GITHUB_OUTPUT - - - name: Upload mockup images - if: steps.preview.outputs.skip_preview != 'true' - uses: actions/upload-artifact@v4 - with: - name: xaml-previews - path: xaml-previews/ - retention-days: 7 - - - name: Find existing comment - uses: peter-evans/find-comment@v1 - id: fc - with: - issue-number: ${{ github.event.number }} - comment-author: 'github-actions[bot]' - body-includes: 🎨 XAML Preview Bot - - - name: Create comment if it doesn't exist - if: steps.fc.outputs.comment-id == '' && steps.preview.outputs.skip_preview != 'true' - uses: peter-evans/create-or-update-comment@v1 - with: - issue-number: ${{ github.event.number }} - body: | - ${{ steps.preview.outputs.PREVIEW_CONTENT }} - - - name: Update comment if it exists - if: steps.fc.outputs.comment-id != '' && steps.preview.outputs.skip_preview != 'true' - uses: peter-evans/create-or-update-comment@v1 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - edit-mode: replace - body: | - ${{ steps.preview.outputs.PREVIEW_CONTENT }} - - - name: Update comment to read that it has been edited - if: steps.fc.outputs.comment-id != '' && steps.preview.outputs.skip_preview != 'true' - uses: peter-evans/create-or-update-comment@v1 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - edit-mode: append - body: | - - Edit: preview updated after ${{ github.event.pull_request.head.sha }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1d5e725a93..78866a96f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -# XAML Preview Bot generated files -xaml-previews/ - ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. diff --git a/Content.Client/Administration/Managers/ClientAdminManager.cs b/Content.Client/Administration/Managers/ClientAdminManager.cs index 0f740c8104..3f072691de 100644 --- a/Content.Client/Administration/Managers/ClientAdminManager.cs +++ b/Content.Client/Administration/Managers/ClientAdminManager.cs @@ -15,6 +15,7 @@ namespace Content.Client.Administration.Managers [Dependency] private readonly IPlayerManager _player = default!; [Dependency] private readonly IClientNetManager _netMgr = default!; [Dependency] private readonly IClientConGroupController _conGroup = default!; + [Dependency] private readonly IClientConsoleHost _host = default!; [Dependency] private readonly IResourceManager _res = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterface = default!; @@ -86,12 +87,12 @@ namespace Content.Client.Administration.Managers private void UpdateMessageRx(MsgUpdateAdminStatus message) { _availableCommands.Clear(); - var host = IoCManager.Resolve(); // Anything marked as Any we'll just add even if the server doesn't know about it. - foreach (var (command, instance) in host.AvailableCommands) + foreach (var (command, instance) in _host.AvailableCommands) { - if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue; + if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) + continue; _availableCommands.Add(command); } diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs deleted file mode 100644 index 0de38ce234..0000000000 --- a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Content.Shared.Administration.Logs; -using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; - -namespace Content.Client.Administration.UI.CustomControls; - -public sealed class AdminLogLabel : RichTextLabel -{ - public AdminLogLabel(ref SharedAdminLog log, HSeparator separator) - { - Log = log; - Separator = separator; - - SetMessage($"{log.Date:HH:mm:ss}: {log.Message}"); - OnVisibilityChanged += VisibilityChanged; - } - - public SharedAdminLog Log { get; } - - public HSeparator Separator { get; } - - private void VisibilityChanged(Control control) - { - Separator.Visible = Visible; - } - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - OnVisibilityChanged -= VisibilityChanged; - } -} diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs index c7fbf6c2dc..8027a00c54 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListControl.xaml.cs @@ -1,16 +1,15 @@ using System.Linq; +using System.Text.RegularExpressions; using Content.Client.Administration.Systems; using Content.Client.UserInterface.Controls; using Content.Client.Verbs.UI; using Content.Shared.Administration; using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Input; -using Robust.Shared.Utility; namespace Content.Client.Administration.UI.CustomControls; @@ -96,13 +95,26 @@ public sealed partial class PlayerListControl : BoxContainer private void FilterList() { _sortedPlayerList.Clear(); + + Regex filterRegex; + // There is no neat way to handle invalid regex being submitted other than + // catching and ignoring the exception which gets thrown when it's invalid. + try + { + filterRegex = new Regex(FilterLineEdit.Text, RegexOptions.IgnoreCase); + } + catch (ArgumentException) + { + return; + } + foreach (var info in _playerList) { var displayName = $"{info.CharacterName} ({info.Username})"; if (info.IdentityName != info.CharacterName) displayName += $" [{info.IdentityName}]"; if (!string.IsNullOrEmpty(FilterLineEdit.Text) - && !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant())) + && !filterRegex.IsMatch(displayName)) continue; _sortedPlayerList.Add(info); } diff --git a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs index cd6a56ea71..f62a6c71e4 100644 --- a/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs +++ b/Content.Client/Administration/UI/CustomControls/PlayerListEntry.xaml.cs @@ -1,10 +1,8 @@ using Content.Client.Stylesheets; using Content.Shared.Administration; using Robust.Client.AutoGenerated; -using Robust.Client.GameObjects; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Utility; namespace Content.Client.Administration.UI.CustomControls; diff --git a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml index cd93ffeb0a..c646e380d4 100644 --- a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml +++ b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml @@ -1,5 +1,6 @@  + xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls" + xmlns:ui="clr-namespace:Content.Client.Options.UI"> @@ -52,6 +53,13 @@ diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs index 66d875b0f2..ce190464d2 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs +++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs @@ -26,6 +26,10 @@ public sealed partial class LatheMenu : DefaultWindow public event Action? OnServerListButtonPressed; public event Action? RecipeQueueAction; + public event Action? QueueDeleteAction; + public event Action? QueueMoveUpAction; + public event Action? QueueMoveDownAction; + public event Action? DeleteFabricatingAction; public List> Recipes = new(); @@ -50,12 +54,21 @@ public sealed partial class LatheMenu : DefaultWindow }; AmountLineEdit.OnTextChanged += _ => { + if (int.TryParse(AmountLineEdit.Text, out var amount)) + { + if (amount > LatheSystem.MaxItemsPerRequest) + AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString(); + else if (amount < 0) + AmountLineEdit.Text = "0"; + } + PopulateRecipes(); }; FilterOption.OnItemSelected += OnItemSelected; ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a); + DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke(); } public void SetEntity(EntityUid uid) @@ -223,22 +236,27 @@ public sealed partial class LatheMenu : DefaultWindow /// Populates the build queue list with all queued items /// /// - public void PopulateQueueList(IReadOnlyCollection> queue) + public void PopulateQueueList(IReadOnlyCollection queue) { QueueList.DisposeAllChildren(); var idx = 1; - foreach (var recipeProto in queue) + foreach (var batch in queue) { - var recipe = _prototypeManager.Index(recipeProto); - var queuedRecipeBox = new BoxContainer(); - queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal; + var recipe = _prototypeManager.Index(batch.Recipe); - queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe)); + var itemName = _lathe.GetRecipeName(batch.Recipe); + string displayText; + if (batch.ItemsRequested > 1) + displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested)); + else + displayText = Loc.GetString("lathe-menu-item-single", ("index", idx), ("name", itemName)); + + var queuedRecipeBox = new QueuedRecipeControl(displayText, idx - 1, GetRecipeDisplayControl(recipe)); + queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s); + queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s); + queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s); - var queuedRecipeLabel = new Label(); - queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}"; - queuedRecipeBox.AddChild(queuedRecipeLabel); QueueList.AddChild(queuedRecipeBox); idx++; } diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml new file mode 100644 index 0000000000..b1d4b496a1 --- /dev/null +++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml @@ -0,0 +1,35 @@ + + + + + diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs new file mode 100644 index 0000000000..c4ba9803b0 --- /dev/null +++ b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs @@ -0,0 +1,36 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Lathe.UI; + +[GenerateTypedNameReferences] +public sealed partial class QueuedRecipeControl : Control +{ + public Action? OnDeletePressed; + public Action? OnMoveUpPressed; + public Action? OnMoveDownPressed; + + public QueuedRecipeControl(string displayText, int index, Control displayControl) + { + RobustXamlLoader.Load(this); + + RecipeName.Text = displayText; + RecipeDisplayContainer.AddChild(displayControl); + + MoveUp.OnPressed += (_) => + { + OnMoveUpPressed?.Invoke(index); + }; + + MoveDown.OnPressed += (_) => + { + OnMoveDownPressed?.Invoke(index); + }; + + Delete.OnPressed += (_) => + { + OnDeletePressed?.Invoke(index); + }; + } +} diff --git a/Content.Client/Morgue/CrematoriumSystem.cs b/Content.Client/Morgue/CrematoriumSystem.cs new file mode 100644 index 0000000000..66eac263c2 --- /dev/null +++ b/Content.Client/Morgue/CrematoriumSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Morgue; + +namespace Content.Client.Morgue; + +public sealed class CrematoriumSystem : SharedCrematoriumSystem; diff --git a/Content.Client/Morgue/MorgueSystem.cs b/Content.Client/Morgue/MorgueSystem.cs new file mode 100644 index 0000000000..b8d2f109fb --- /dev/null +++ b/Content.Client/Morgue/MorgueSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Morgue; + +namespace Content.Client.Morgue; + +public sealed class MorgueSystem : SharedMorgueSystem; diff --git a/Content.Client/NPC/PathfindingSystem.cs b/Content.Client/NPC/PathfindingSystem.cs index 0c72a8f99f..dc8fd98433 100644 --- a/Content.Client/NPC/PathfindingSystem.cs +++ b/Content.Client/NPC/PathfindingSystem.cs @@ -20,6 +20,7 @@ namespace Content.Client.NPC [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IInputManager _inputManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IOverlayManager _overlayManager = default!; [Dependency] private readonly IResourceCache _cache = default!; [Dependency] private readonly NPCSteeringSystem _steering = default!; [Dependency] private readonly MapSystem _mapSystem = default!; @@ -30,17 +31,15 @@ namespace Content.Client.NPC get => _modes; set { - var overlayManager = IoCManager.Resolve(); - if (value == PathfindingDebugMode.None) { Breadcrumbs.Clear(); Polys.Clear(); - overlayManager.RemoveOverlay(); + _overlayManager.RemoveOverlay(); } - else if (!overlayManager.HasOverlay()) + else if (!_overlayManager.HasOverlay()) { - overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem)); + _overlayManager.AddOverlay(new PathfindingOverlay(EntityManager, _eyeManager, _inputManager, _mapManager, _cache, this, _mapSystem, _transformSystem)); } if ((value & PathfindingDebugMode.Steering) != 0x0) diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml index ca22d7206b..0fa5ee1754 100644 --- a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml +++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml @@ -9,6 +9,9 @@ +