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/Administration/UI/Logs/AdminLogsControl.xaml.cs b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
index b5e42f93a5..49b3122c53 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
+++ b/Content.Client/Administration/UI/Logs/AdminLogsControl.xaml.cs
@@ -1,7 +1,9 @@
using System.Linq;
using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
using Content.Client._Sunrise.Administration.UI.CustomControls;
using Content.Client.Administration.UI.CustomControls;
+using Content.Client.Administration.UI.Logs.Entries;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Robust.Client.AutoGenerated;
@@ -39,6 +41,9 @@ public sealed partial class AdminLogsControl : Control
SelectAllPlayersButton.OnPressed += SelectAllPlayers;
SelectNoPlayersButton.OnPressed += SelectNoPlayers;
+ RenderRichTextButton.OnPressed += RenderRichTextChanged;
+ RemoveMarkupButton.OnPressed += RemoveMarkupChanged;
+
RoundSpinBox.IsValid = i => i > 0 && i <= CurrentRound;
RoundSpinBox.ValueChanged += RoundSpinBoxChanged;
RoundSpinBox.InitDefaultButtons();
@@ -51,13 +56,16 @@ public sealed partial class AdminLogsControl : Control
private int CurrentRound { get; set; }
+ private Regex LogSearchRegex { get; set; } = new("");
+
public int SelectedRoundId => RoundSpinBox.Value;
public string Search => LogSearch.Text;
private int ShownLogs { get; set; }
private int TotalLogs { get; set; }
private int RoundLogs { get; set; }
public bool IncludeNonPlayerLogs { get; set; }
-
+ private bool RenderRichText { get; set; }
+ private bool RemoveMarkup { get; set; }
public HashSet SelectedTypes { get; } = new();
public HashSet SelectedPlayers { get; } = new();
@@ -104,6 +112,19 @@ public sealed partial class AdminLogsControl : Control
private void LogSearchChanged(LineEditEventArgs args)
{
+ // This exception is thrown if the regex is invalid, which happens often, so we ignore it.
+ try
+ {
+ LogSearchRegex = new Regex(
+ "(" + LogSearch.Text + ")",
+ RegexOptions.IgnoreCase,
+ TimeSpan.FromSeconds(1));
+ }
+ catch (ArgumentException)
+ {
+ return;
+ }
+
UpdateLogs();
}
@@ -185,6 +206,26 @@ public sealed partial class AdminLogsControl : Control
UpdateLogs();
}
+ private void RenderRichTextChanged(ButtonEventArgs args)
+ {
+ RenderRichText = args.Button.Pressed;
+
+ RemoveMarkup = RemoveMarkup && !RenderRichText;
+ RemoveMarkupButton.Pressed = RemoveMarkup;
+
+ UpdateLogs();
+ }
+
+ private void RemoveMarkupChanged(ButtonEventArgs args)
+ {
+ RemoveMarkup = args.Button.Pressed;
+
+ RenderRichText = !RemoveMarkup && RenderRichText;
+ RenderRichTextButton.Pressed = RenderRichText;
+
+ UpdateLogs();
+ }
+
public void SetTypesSelection(HashSet selectedTypes, bool invert = false)
{
SelectedTypes.Clear();
@@ -243,16 +284,15 @@ public sealed partial class AdminLogsControl : Control
foreach (var child in LogsContainer.Children)
{
- if (child is not SunriseAdminLogLabel log)
- {
+ if (child is not AdminLogEntry log)
continue;
- }
child.Visible = ShouldShowLog(log);
- if (child.Visible)
- {
- ShownLogs++;
- }
+ if (!child.Visible)
+ continue;
+
+ log.RenderResults(LogSearchRegex, RenderRichText, RemoveMarkup);
+ ShownLogs++;
}
UpdateCount();
@@ -270,30 +310,30 @@ public sealed partial class AdminLogsControl : Control
button.Text.Contains(PlayerSearch.Text, StringComparison.OrdinalIgnoreCase);
}
- private bool LogMatchesPlayerFilter(SunriseAdminLogLabel label)
+ private bool LogMatchesPlayerFilter(AdminLogEntry entry)
{
- if (label.Log.Players.Length == 0)
+ if (entry.Log.Players.Length == 0)
return SelectedPlayers.Count == 0 || IncludeNonPlayerLogs;
- return SelectedPlayers.Overlaps(label.Log.Players);
+ return SelectedPlayers.Overlaps(entry.Log.Players);
}
- private bool ShouldShowLog(SunriseAdminLogLabel label)
+ private bool ShouldShowLog(AdminLogEntry entry)
{
// Check log type
- if (!SelectedTypes.Contains(label.Log.Type))
+ if (!SelectedTypes.Contains(entry.Log.Type))
return false;
// Check players
- if (!LogMatchesPlayerFilter(label))
+ if (!LogMatchesPlayerFilter(entry))
return false;
// Check impact
- if (!SelectedImpacts.Contains(label.Log.Impact))
+ if (!SelectedImpacts.Contains(entry.Log.Impact))
return false;
// Check search
- if (!label.Log.Message.Contains(LogSearch.Text, StringComparison.OrdinalIgnoreCase))
+ if (!LogSearchRegex.IsMatch(entry.Log.Message))
return false;
return true;
@@ -469,21 +509,11 @@ public sealed partial class AdminLogsControl : Control
for (var i = 0; i < span.Length; i++)
{
ref var log = ref span[i];
- var separator = new HSeparator();
- var label = new SunriseAdminLogLabel(ref log, separator);
- label.Visible = ShouldShowLog(label);
-
+ var entry = new AdminLogEntry(ref log);
TotalLogs++;
- if (label.Visible)
- {
- ShownLogs++;
- }
-
- LogsContainer.AddChild(label);
- LogsContainer.AddChild(separator);
+ LogsContainer.AddChild(entry);
}
-
- UpdateCount();
+ UpdateLogs();
}
public void SetLogs(List logs)
@@ -527,6 +557,7 @@ public sealed partial class AdminLogsControl : Control
SelectAllTypesButton.OnPressed -= SelectAllTypes;
SelectNoTypesButton.OnPressed -= SelectNoTypes;
+ IncludeNonPlayersButton.OnPressed -= IncludeNonPlayers;
IncludeNonPlayersButton.OnPressed -= IncludeNonPlayers;
SelectAllPlayersButton.OnPressed -= SelectAllPlayers;
SelectNoPlayersButton.OnPressed -= SelectNoPlayers;
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
index 1544b827ae..e674c6c833 100644
--- a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
+++ b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
@@ -1,6 +1,6 @@
using System.IO;
using System.Linq;
-using Content.Client.Administration.UI.CustomControls;
+using Content.Client.Administration.UI.Logs.Entries;
using Content.Client.Eui;
using Content.Shared.Administration.Logs;
using Content.Shared.Eui;
@@ -22,7 +22,7 @@ public sealed class AdminLogsEui : BaseEui
private const char CsvSeparator = ',';
private const string CsvQuote = "\"";
- private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message";
+ private const string CsvHeader = "Date,ID,PlayerID,Severity,Type,Message,CurTime";
private ISawmill _sawmill;
@@ -109,10 +109,10 @@ public sealed class AdminLogsEui : BaseEui
await writer.WriteLineAsync(CsvHeader);
foreach (var child in LogsControl.LogsContainer.Children)
{
- if (child is not AdminLogLabel logLabel || !child.Visible)
+ if (child is not AdminLogEntry entry || !child.Visible)
continue;
- var log = logLabel.Log;
+ var log = entry.Log;
// Date
// I swear to god if someone adds ,s or "s to the other fields...
@@ -138,6 +138,9 @@ public sealed class AdminLogsEui : BaseEui
await writer.WriteAsync(CsvQuote);
await writer.WriteAsync(log.Message.Replace(CsvQuote, CsvQuote + CsvQuote));
await writer.WriteAsync(CsvQuote);
+ await writer.WriteAsync(CsvSeparator);
+ // CurTime
+ await writer.WriteAsync(log.CurTime.ToString());
await writer.WriteLineAsync();
}
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml
new file mode 100644
index 0000000000..7733c2b1cb
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml.cs b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml.cs
new file mode 100644
index 0000000000..fc9f960b66
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntry.xaml.cs
@@ -0,0 +1,79 @@
+using System.Text.RegularExpressions;
+using Content.Client.Message;
+using Content.Shared.Administration.Logs;
+using Content.Shared.CCVar;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Configuration;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI.Logs.Entries;
+
+[GenerateTypedNameReferences]
+public sealed partial class AdminLogEntry : BoxContainer
+{
+ private readonly IConfigurationManager _cfgManager;
+
+ public SharedAdminLog Log { get; }
+
+ private readonly string _rawMessage;
+
+ public AdminLogEntry(ref SharedAdminLog log)
+ {
+ _cfgManager = IoCManager.Resolve();
+
+ RobustXamlLoader.Load(this);
+
+ Log = log;
+
+ _rawMessage = $"{log.Date:HH:mm:ss}: {log.Message}";
+ Message.SetMessage(_rawMessage);
+
+ DetailsHeading.OnToggled += DetailsToggled;
+ }
+
+ ///
+ /// Sets text to be highlighted from a search result, and renders rich text, or removes all rich text markup.
+ ///
+ public void RenderResults(Regex highlightRegex, bool renderRichText, bool removeMarkup)
+ {
+ var color = _cfgManager.GetCVar(CCVars.AdminLogsHighlightColor);
+ var formattedMessage = renderRichText
+ ? _rawMessage
+ : removeMarkup
+ ? FormattedMessage.RemoveMarkupPermissive(_rawMessage)
+ : FormattedMessage.EscapeText(_rawMessage);
+
+ // Want to avoid highlighting smaller strings
+ if (highlightRegex.ToString().Length > 4)
+ {
+ try
+ {
+ formattedMessage = highlightRegex.Replace(formattedMessage, $"[color={color}]$1[/color]", 3);
+ }
+ catch (RegexMatchTimeoutException)
+ {
+ // if we time out then don't bother highlighting results
+ }
+ }
+
+ if (!FormattedMessage.TryFromMarkup(formattedMessage, out var outputMessage))
+ return;
+
+ Message.SetMessage(outputMessage);
+ }
+
+ ///
+ /// We perform some extra calculations in the dropdown, so we want to render that only when
+ /// the dropdown is actually opened.
+ /// This also removes itself from the event listener so it doesn't trigger again.
+ ///
+ private void DetailsToggled(BaseButton.ButtonToggledEventArgs args)
+ {
+ if (!args.Pressed || DetailsBody.ChildCount > 0)
+ return;
+ DetailsBody.AddChild(new AdminLogEntryDetails(Log));
+ DetailsHeading.OnToggled -= DetailsToggled;
+ }
+}
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml
new file mode 100644
index 0000000000..23391046b9
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml.cs b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml.cs
new file mode 100644
index 0000000000..611626e544
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/Entries/AdminLogEntryDetails.xaml.cs
@@ -0,0 +1,98 @@
+using System.Linq;
+using Content.Client.Administration.Systems;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Client.UserInterface.Controls;
+using Content.Client.Verbs.UI;
+using Content.Shared.Administration.Logs;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+using Robust.Shared.Network;
+
+namespace Content.Client.Administration.UI.Logs.Entries;
+
+[GenerateTypedNameReferences]
+public sealed partial class AdminLogEntryDetails : BoxContainer
+{
+ private readonly AdminSystem _adminSystem;
+ private readonly IUserInterfaceManager _uiManager;
+ private readonly IEntityManager _entManager;
+
+ public AdminLogEntryDetails(SharedAdminLog log)
+ {
+ RobustXamlLoader.Load(this);
+ _entManager = IoCManager.Resolve();
+ _uiManager = IoCManager.Resolve();
+ _adminSystem = _entManager.System();
+
+ Type.Text = log.Type.ToString();
+ Impact.Text = log.Impact.ToString();
+
+ LocalTime.Text = $"{log.Date.ToLocalTime():HH:mm:ss}";
+ UTCTime.Text = $"{log.Date:HH:mm:ss}";
+ // TimeSpan and DateTime use different formatting string conventions for some completely logical reason
+ // that mere mortals such as myself will never be able to understand.
+ CurTime.Text = new TimeSpan(log.CurTime).ToString(@"hh\:mm\:ss");
+
+ PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
+ PlayerListContainer.GenerateItem += GenerateButton;
+ PopulateList(log.Players);
+ }
+
+ private void PopulateList(Guid[] players)
+ {
+ if (players.Length == 0)
+ return;
+
+ if (_adminSystem.PlayerList is not { } allPlayers || allPlayers.Count == 0)
+ return;
+
+ var listData = new List();
+ foreach (var playerGuid in players)
+ {
+ var netUserId = new NetUserId(playerGuid);
+
+ // Linq here is fine since this runs in response to admin input in the UI and
+ // this loop only tends to go through 1-4 iterations.
+ if (allPlayers.FirstOrDefault(player => player.SessionId == netUserId) is not { } playerInfo)
+ continue;
+
+ listData.Add(new PlayerListData(playerInfo));
+ }
+
+ if (listData.Count == 0)
+ return;
+
+ PlayerListContainer.PopulateList(listData);
+ }
+
+ private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
+ {
+ if (args == null || data is not PlayerListData { Info: var selectedPlayer })
+ return;
+
+ if (!(args.Function == EngineKeyFunctions.UIRightClick
+ || args.Function == EngineKeyFunctions.UIClick)
+ || selectedPlayer.NetEntity == null)
+ return;
+
+ _uiManager.GetUIController().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
+ args.Handle();
+ }
+
+ private void GenerateButton(ListData data, ListContainerButton button)
+ {
+ if (data is not PlayerListData { Info: var info })
+ return;
+
+ var entryLabel = new Label();
+ entryLabel.Text = $"{info.CharacterName} ({info.Username})";
+ var entry = new BoxContainer();
+ entry.AddChild(entryLabel);
+
+ button.AddChild(entry);
+ button.AddStyleClass(ListContainer.StyleClassListContainerButton);
+ }
+}
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
index de9ccbbf50..fa92a3e18f 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs
@@ -57,12 +57,43 @@ public sealed partial class ObjectsTab : Control
private void TeleportTo(NetEntity nent)
{
- _console.ExecuteCommand($"tpto {nent}");
+ var selection = _selections[ObjectTypeOptions.SelectedId];
+ switch (selection)
+ {
+ case ObjectsTabSelection.Grids:
+ {
+ // directly teleport to the entity
+ _console.ExecuteCommand($"tpto {nent}");
+ }
+ break;
+ case ObjectsTabSelection.Maps:
+ {
+ // teleport to the map, not to the map entity (which is in nullspace)
+ if (!_entityManager.TryGetEntity(nent, out var map) || !_entityManager.TryGetComponent(map, out var mapComp))
+ break;
+ _console.ExecuteCommand($"tp 0 0 {mapComp.MapId}");
+ break;
+ }
+ case ObjectsTabSelection.Stations:
+ {
+ // teleport to the station's largest grid, not to the station entity (which is in nullspace)
+ if (!_entityManager.TryGetEntity(nent, out var station))
+ break;
+ var largestGrid = _entityManager.EntitySysManager.GetEntitySystem().GetLargestGrid(station.Value);
+ if (largestGrid == null)
+ break;
+ _console.ExecuteCommand($"tpto {largestGrid.Value}");
+ break;
+ }
+ default:
+ throw new NotImplementedException();
+ }
}
private void Delete(NetEntity nent)
{
_console.ExecuteCommand($"delete {nent}");
+ RefreshObjectList();
}
public void RefreshObjectList()
@@ -79,25 +110,21 @@ public sealed partial class ObjectsTab : Control
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem().GetStationNames());
break;
case ObjectsTabSelection.Grids:
- {
- var query = _entityManager.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var metadata))
{
- entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- }
+ var query = _entityManager.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var metadata))
+ entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- break;
- }
+ break;
+ }
case ObjectsTabSelection.Maps:
- {
- var query = _entityManager.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out _, out var metadata))
{
- entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- }
+ var query = _entityManager.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out _, out var metadata))
+ entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
- break;
- }
+ break;
+ }
default:
throw new ArgumentOutOfRangeException(nameof(selection), selection, null);
}
diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
index c561125a30..7c9dde79b5 100644
--- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
+++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTabEntry.xaml
@@ -1,5 +1,6 @@
-
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 @@
+
+
+
+
+
+
+
+
+ Caution
+ OpenLeft
+
+
+
+
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 @@
+
+
diff --git a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
index a25d83d337..99eb2ec6a5 100644
--- a/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AdminOptionsTab.xaml.cs
@@ -51,6 +51,8 @@ public sealed partial class AdminOptionsTab : Control
playerTabSymbolSettings.Add(new OptionDropDownCVar.ValueOption(setting.ToString()!, Loc.GetString($"ui-options-admin-player-tab-symbol-setting-{setting.ToString()!.ToLower()}")));
}
+ Control.AddOptionColorSlider(CCVars.AdminLogsHighlightColor, ColorSliderLogsHighlight);
+
Control.AddOptionDropDown(CCVars.AdminPlayerTabSymbolSetting, DropDownPlayerTabSymbolSetting, playerTabSymbolSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabRoleSetting, DropDownPlayerTabRoleSetting, playerTabRoleSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabColorSetting, DropDownPlayerTabColorSetting, playerTabColorSettings);
diff --git a/Content.Client/Options/UI/Tabs/ExtraTab.xaml b/Content.Client/Options/UI/Tabs/ExtraTab.xaml
index 0113e528d6..b5e2807794 100644
--- a/Content.Client/Options/UI/Tabs/ExtraTab.xaml
+++ b/Content.Client/Options/UI/Tabs/ExtraTab.xaml
@@ -31,7 +31,6 @@
-
diff --git a/Content.Client/Options/UI/Tabs/ExtraTab.xaml.cs b/Content.Client/Options/UI/Tabs/ExtraTab.xaml.cs
index 40a3a29de9..3d65961233 100644
--- a/Content.Client/Options/UI/Tabs/ExtraTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/ExtraTab.xaml.cs
@@ -32,11 +32,6 @@ public sealed partial class ExtraTab : Control
SliderTtsRadio,
scale: ContentAudioSystem.TtsMultiplier);
- Control.AddOptionPercentSlider(
- SunriseCCVars.TTSAnnounceVolume,
- SliderTtsAnnounce,
- scale: ContentAudioSystem.TtsMultiplier);
-
Control.AddOptionCheckBox(SunriseCCVars.TTSClientEnabled, TtsClientCheckBox);
Control.AddOptionCheckBox(SunriseCCVars.TTSClientQueueEnabled, TtsClientCheckBoxQueue);
Control.AddOptionCheckBox(SunriseCCVars.TTSRadioGhostEnabled, TtsRadioGhostCheckBox);
diff --git a/Content.Client/Paper/UI/PaperWindow.xaml b/Content.Client/Paper/UI/PaperWindow.xaml
index 9699e939a6..35eabf10e5 100644
--- a/Content.Client/Paper/UI/PaperWindow.xaml
+++ b/Content.Client/Paper/UI/PaperWindow.xaml
@@ -13,8 +13,7 @@
-
+
diff --git a/Content.Client/Paper/UI/PaperWindow.xaml.cs b/Content.Client/Paper/UI/PaperWindow.xaml.cs
index 4f4cc01171..92e7dfb3f9 100644
--- a/Content.Client/Paper/UI/PaperWindow.xaml.cs
+++ b/Content.Client/Paper/UI/PaperWindow.xaml.cs
@@ -290,17 +290,17 @@ namespace Content.Client.Paper.UI
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
// Sunrise-Start
- var sprite = _entitySystemManager.GetEntitySystem();
- if (state.ImageContent != null)
- {
- ImageContent.Texture = sprite.Frame0(state.ImageContent);
- if (state.ImageScale != null)
- ImageContent.TextureScale = state.ImageScale.Value;
- ImageContent.Visible = true;
- BlankPaperIndicator.Visible = false;
- }
- else
- ImageContent.Visible = false;
+ var spriteSys = _entitySystemManager.GetEntitySystem();
+
+ Robust.Client.Graphics.Texture? tex = null;
+ if (state.ImageContent is {} spec)
+ tex = spriteSys.Frame0(spec);
+
+ ImageContent.Visible = tex != null;
+ ImageContent.Texture = tex;
+
+ ImageContent.HorizontalExpand = false;
+ ImageContent.VerticalExpand = false;
// Sunrise-End
StampDisplay.RemoveAllChildren();
diff --git a/Content.Client/Pinpointer/UI/NavMapControl.cs b/Content.Client/Pinpointer/UI/NavMapControl.cs
index b774b7d8b5..b033c493d9 100644
--- a/Content.Client/Pinpointer/UI/NavMapControl.cs
+++ b/Content.Client/Pinpointer/UI/NavMapControl.cs
@@ -135,7 +135,7 @@ public partial class NavMapControl : MapGridControl
},
VerticalExpand = false,
HorizontalExpand = true,
- SetWidth = 650f,
+ //SetWidth = 650f, Sunrise-Edit
Children =
{
new BoxContainer()
diff --git a/Content.Client/Pinpointer/UI/StationMapWindow.xaml b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
index c79fc8f9e7..659a6bec31 100644
--- a/Content.Client/Pinpointer/UI/StationMapWindow.xaml
+++ b/Content.Client/Pinpointer/UI/StationMapWindow.xaml
@@ -13,8 +13,8 @@
-
-
+
+
diff --git a/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs b/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
index 784c39a6ce..1f060e532d 100644
--- a/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
+++ b/Content.Client/Radiation/Overlays/RadiationDebugOverlay.cs
@@ -11,6 +11,8 @@ namespace Content.Client.Radiation.Overlays;
public sealed class RadiationDebugOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IResourceCache _cache = default!;
+
private readonly SharedMapSystem _mapSystem;
private readonly RadiationSystem _radiation;
@@ -24,8 +26,7 @@ public sealed class RadiationDebugOverlay : Overlay
_radiation = _entityManager.System();
_mapSystem = _entityManager.System();
- var cache = IoCManager.Resolve();
- _font = new VectorFont(cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
+ _font = new VectorFont(_cache.GetResource("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
index 56b54c176a..f6e6594af5 100644
--- a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
+++ b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
@@ -16,20 +16,20 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
get => _enableShuttlePosition;
set
{
- if (_enableShuttlePosition == value) return;
+ if (_enableShuttlePosition == value)
+ return;
_enableShuttlePosition = value;
- var overlayManager = IoCManager.Resolve();
if (_enableShuttlePosition)
{
_overlay = new EmergencyShuttleOverlay(EntityManager.TransformQuery, XformSystem);
- overlayManager.AddOverlay(_overlay);
+ _overlays.AddOverlay(_overlay);
RaiseNetworkEvent(new EmergencyShuttleRequestPositionMessage());
}
else
{
- overlayManager.RemoveOverlay(_overlay!);
+ _overlays.RemoveOverlay(_overlay!);
_overlay = null;
}
}
diff --git a/Content.Client/Storage/Components/EntityStorageComponent.cs b/Content.Client/Storage/Components/EntityStorageComponent.cs
deleted file mode 100644
index 514d5d6595..0000000000
--- a/Content.Client/Storage/Components/EntityStorageComponent.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Content.Shared.Storage.Components;
-using Robust.Shared.GameStates;
-
-namespace Content.Client.Storage.Components;
-
-[RegisterComponent]
-public sealed partial class EntityStorageComponent : SharedEntityStorageComponent
-{
-
-}
diff --git a/Content.Client/Storage/Systems/EntityStorageSystem.cs b/Content.Client/Storage/Systems/EntityStorageSystem.cs
index dd3f8d3860..ca2b986667 100644
--- a/Content.Client/Storage/Systems/EntityStorageSystem.cs
+++ b/Content.Client/Storage/Systems/EntityStorageSystem.cs
@@ -31,7 +31,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
SubscribeLocalEvent(OnHandleState);
}
- public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref SharedEntityStorageComponent? component)
+ public override bool ResolveStorage(EntityUid uid, [NotNullWhen(true)] ref EntityStorageComponent? component)
{
if (component != null)
return true;
diff --git a/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
index a10155f3e8..39d0f1ba47 100644
--- a/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
+++ b/Content.Client/UserInterface/Controls/MapGridControl.xaml.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using System.Text.RegularExpressions;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -85,7 +86,7 @@ public partial class MapGridControl : LayoutContainer
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
- SetSize = new Vector2(SizeFull, SizeFull);
+ MinSize = new Vector2(MathF.Round(SizeFull / 2f), MathF.Round(SizeFull / 2f)); // Sunrise-Edit
RectClipContent = true;
MouseFilter = MouseFilterMode.Stop;
ActualRadarRange = WorldRange;
diff --git a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
index ebc024bd2f..15af9dc981 100644
--- a/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
+++ b/Content.Client/Weapons/Ranged/Systems/GunSystem.cs
@@ -42,6 +42,7 @@ public sealed partial class GunSystem : SharedGunSystem
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IStateManager _state = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
@@ -75,11 +76,10 @@ public sealed partial class GunSystem : SharedGunSystem
return;
_spreadOverlay = value;
- var overlayManager = IoCManager.Resolve();
if (_spreadOverlay)
{
- overlayManager.AddOverlay(new GunSpreadOverlay(
+ _overlayManager.AddOverlay(new GunSpreadOverlay(
EntityManager,
_eyeManager,
Timing,
@@ -90,7 +90,7 @@ public sealed partial class GunSystem : SharedGunSystem
}
else
{
- overlayManager.RemoveOverlay();
+ _overlayManager.RemoveOverlay();
}
}
}
diff --git a/Content.Client/_Sunrise/MarkingEffectsClient/MarkingEffectShaders.cs b/Content.Client/_Sunrise/MarkingEffectsClient/MarkingEffectShaders.cs
index 1cf465c44d..6b2d3bf1da 100644
--- a/Content.Client/_Sunrise/MarkingEffectsClient/MarkingEffectShaders.cs
+++ b/Content.Client/_Sunrise/MarkingEffectsClient/MarkingEffectShaders.cs
@@ -23,8 +23,12 @@ public static class MarkingEffectShaders
if (color is not GradientMarkingEffect gradient)
return;
- instance.SetParameter("color1", ColorToVec(gradient.Colors["base"]));
- instance.SetParameter("color2", ColorToVec(gradient.Colors["gradient"]));
+ // Safely get colors with fallback for old imported characters
+ var baseColor = gradient.Colors.TryGetValue("base", out var bColor) ? bColor : Color.White;
+ var gradientColor = gradient.Colors.TryGetValue("gradient", out var gColor) ? gColor : baseColor;
+
+ instance.SetParameter("color1", ColorToVec(baseColor));
+ instance.SetParameter("color2", ColorToVec(gradientColor));
instance.SetParameter("texScale", texScale);
instance.SetParameter("offset", gradient.Offset);
instance.SetParameter("size", gradient.Size);
@@ -35,8 +39,13 @@ public static class MarkingEffectShaders
case MarkingEffectType.RoughGradient:
if (color is not RoughGradientMarkingEffect roughGradient)
return;
- instance.SetParameter("color1", ColorToVec(roughGradient.Colors["base"]));
- instance.SetParameter("color2", ColorToVec(roughGradient.Colors["gradient"]));
+
+ // Safely get colors with fallback for old imported characters
+ var baseColor2 = roughGradient.Colors.TryGetValue("base", out var bColor2) ? bColor2 : Color.White;
+ var gradientColor2 = roughGradient.Colors.TryGetValue("gradient", out var gColor2) ? gColor2 : baseColor2;
+
+ instance.SetParameter("color1", ColorToVec(baseColor2));
+ instance.SetParameter("color2", ColorToVec(gradientColor2));
instance.SetParameter("horizontal", roughGradient.Horizontal);
break;
}
diff --git a/Content.Client/_Sunrise/TTS/TTSSystem.cs b/Content.Client/_Sunrise/TTS/TTSSystem.cs
index 6772cf7a97..e311f4d56d 100644
--- a/Content.Client/_Sunrise/TTS/TTSSystem.cs
+++ b/Content.Client/_Sunrise/TTS/TTSSystem.cs
@@ -36,25 +36,22 @@ public sealed class TTSSystem : EntitySystem
private float _volume;
private float _radioVolume;
private int _fileIdx;
- private float _volumeAnnounce;
private bool _isQueueEnabled;
private bool _ghostRadioEnabled;
private readonly Queue _ttsQueue = new();
private (EntityUid Entity, AudioComponent Component)? _currentPlaying;
private static readonly AudioResource EmptyAudioResource = new();
- public sealed class QueuedTts(byte[] data, TtsType ttsType, ResolvedSoundSpecifier? announcementSound = null)
+ public sealed class QueuedTts(byte[] data, TtsType ttsType)
{
public byte[] Data = data;
- public ResolvedSoundSpecifier? AnnouncementSound = announcementSound;
public TtsType TtsType = ttsType;
}
public enum TtsType
{
Voice,
- Radio,
- Announce
+ Radio
}
public override void Initialize()
@@ -63,12 +60,10 @@ public sealed class TTSSystem : EntitySystem
_res.AddRoot(Prefix, ContentRoot);
_cfg.OnValueChanged(SunriseCCVars.TTSVolume, OnTtsVolumeChanged, true);
_cfg.OnValueChanged(SunriseCCVars.TTSRadioVolume, OnTtsRadioVolumeChanged, true);
- _cfg.OnValueChanged(SunriseCCVars.TTSAnnounceVolume, OnTtsAnnounceVolumeChanged, true);
_cfg.OnValueChanged(SunriseCCVars.TTSClientEnabled, OnTtsClientOptionChanged, true);
_cfg.OnValueChanged(SunriseCCVars.TTSClientQueueEnabled, OnTTSQueueOptionChanged, true);
_cfg.OnValueChanged(SunriseCCVars.TTSRadioGhostEnabled, OnTtsRadioGhostChanged, true);
SubscribeNetworkEvent(OnPlayTTS);
- SubscribeNetworkEvent(OnAnnounceTTSPlay);
}
public override void Shutdown()
@@ -76,7 +71,6 @@ public sealed class TTSSystem : EntitySystem
base.Shutdown();
_cfg.UnsubValueChanged(SunriseCCVars.TTSVolume, OnTtsVolumeChanged);
_cfg.UnsubValueChanged(SunriseCCVars.TTSRadioVolume, OnTtsRadioVolumeChanged);
- _cfg.UnsubValueChanged(SunriseCCVars.TTSAnnounceVolume, OnTtsAnnounceVolumeChanged);
_cfg.UnsubValueChanged(SunriseCCVars.TTSClientEnabled, OnTtsClientOptionChanged);
_cfg.UnsubValueChanged(SunriseCCVars.TTSClientQueueEnabled, OnTTSQueueOptionChanged);
_cfg.UnsubValueChanged(SunriseCCVars.TTSRadioGhostEnabled, OnTtsRadioGhostChanged);
@@ -105,10 +99,6 @@ public sealed class TTSSystem : EntitySystem
{
_isQueueEnabled = option;
}
- private void OnTtsAnnounceVolumeChanged(float volume)
- {
- _volumeAnnounce = volume;
- }
private void OnTtsClientOptionChanged(bool option)
{
@@ -120,15 +110,7 @@ public sealed class TTSSystem : EntitySystem
_ghostRadioEnabled = option;
}
- private void OnAnnounceTTSPlay(AnnounceTtsEvent ev)
- {
- if (_volumeAnnounce == 0)
- return;
- var entry = new QueuedTts(ev.Data, TtsType.Announce, ev.AnnouncementSound);
-
- _ttsQueue.Enqueue(entry);
- }
private void PlayNextInQueue()
{
@@ -145,9 +127,6 @@ public sealed class TTSSystem : EntitySystem
case TtsType.Radio:
volume = _radioVolume;
break;
- case TtsType.Announce:
- volume = _volumeAnnounce;
- break;
case TtsType.Voice:
volume = _volume;
break;
@@ -155,10 +134,6 @@ public sealed class TTSSystem : EntitySystem
var finalParams = AudioParams.Default.WithVolume(SharedAudioSystem.GainToVolume(volume));
- if (entry.AnnouncementSound != null)
- {
- _currentPlaying = _audio.PlayGlobal(entry.AnnouncementSound, new EntityUid(), finalParams.AddVolume(-5f));
- }
_currentPlaying = PlayTTSBytes(entry.Data, null, finalParams, true);
}
diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index 5830ea59da..aad87b711a 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -5,7 +5,6 @@ using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
-using Content.Shared.Body.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Mobs.Components;
using Content.Shared.Prototypes;
@@ -266,7 +265,6 @@ public sealed class CargoTest
{
foreach (var (proto, comp) in pair.GetPrototypesWithComponent())
{
- Assert.That(proto.TryGetComponent(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no BodyComponent!");
Assert.That(proto.TryGetComponent(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no MobStateComponent!");
}
});
diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
index e47c73611a..69fe66039b 100644
--- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
+++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
@@ -21,6 +21,7 @@ namespace Content.IntegrationTests.Tests.Doors
components:
- type: Physics
bodyType: Dynamic
+ - type: GravityAffected
- type: Fixtures
fixtures:
fix1:
diff --git a/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs b/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs
new file mode 100644
index 0000000000..a7203d9259
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Engineering/InflatablesDeflateTest.cs
@@ -0,0 +1,20 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Engineering.Systems;
+
+namespace Content.IntegrationTests.Tests.Engineering;
+
+[TestFixture]
+[TestOf(typeof(InflatableSafeDisassemblySystem))]
+public sealed class InflatablesDeflateTest : InteractionTest
+{
+ [Test]
+ public async Task Test()
+ {
+ await SpawnTarget(InflatableWall);
+
+ await InteractUsing(Needle);
+
+ AssertDeleted();
+ await AssertEntityLookup(new EntitySpecifier(InflatableWallStack.Id, 1));
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
index 6aa2763888..0951e7e260 100644
--- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
+++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
@@ -19,6 +19,7 @@ namespace Content.IntegrationTests.Tests.Gravity
- type: Alerts
- type: Physics
bodyType: Dynamic
+ - type: GravityAffected
- type: entity
name: WeightlessGravityGeneratorDummy
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index b32d6c2b8d..8257035de6 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -76,8 +76,8 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.True);
- Assert.That(!entityMan.GetComponent(grid1).EnabledVV);
- Assert.That(entityMan.GetComponent(grid2).EnabledVV);
+ Assert.That(!entityMan.GetComponent(grid1).Enabled);
+ Assert.That(entityMan.GetComponent(grid2).Enabled);
});
// Re-enable needs power so it turns off again.
@@ -94,7 +94,7 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.False);
- Assert.That(entityMan.GetComponent(grid2).EnabledVV, Is.False);
+ Assert.That(entityMan.GetComponent(grid2).Enabled, Is.False);
});
});
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
index 5db5d91d0d..8917ba7ead 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Constants.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Stacks;
+using Robust.Shared.Prototypes;
+
namespace Content.IntegrationTests.Tests.Interaction;
// This partial class contains various constant prototype IDs common to interaction tests.
@@ -32,4 +35,9 @@ public abstract partial class InteractionTest
protected const string Manipulator1 = "MicroManipulatorStockPart";
protected const string Battery1 = "PowerCellSmall";
protected const string Battery4 = "PowerCellHyper";
+
+ // Inflatables & Needle used to pop them
+ protected static readonly EntProtoId InflatableWall = "InflatableWall";
+ protected static readonly EntProtoId Needle = "WeaponMeleeNeedle";
+ protected static readonly ProtoId InflatableWallStack = "InflatableWall";
}
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 79756ea5b4..0ed42d3476 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -144,6 +144,7 @@ public abstract partial class InteractionTest
- type: Stripping
- type: Puller
- type: Physics
+ - type: GravityAffected
- type: Tag
tags:
- CanPilot
diff --git a/Content.IntegrationTests/Tests/_Sunrise/Chemistry/CryostasisBeakerTests.cs b/Content.IntegrationTests/Tests/_Sunrise/Chemistry/CryostasisBeakerTests.cs
new file mode 100644
index 0000000000..1cfe39864f
--- /dev/null
+++ b/Content.IntegrationTests/Tests/_Sunrise/Chemistry/CryostasisBeakerTests.cs
@@ -0,0 +1,84 @@
+using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
+using Content.Shared.FixedPoint;
+
+namespace Content.IntegrationTests.Tests._Sunrise.Chemistry;
+
+[TestFixture]
+[TestOf(typeof(CryostasisBeakerSystem))]
+public sealed class CryostasisBeakerTests
+{
+ [TestPrototypes]
+ private const string Prototypes = @"
+- type: entity
+ id: TestCryostasisBeaker
+ components:
+ - type: SolutionContainerManager
+ solutions:
+ beaker:
+ maxVol: 50
+ canReact: false
+ - type: CryostasisBeaker
+ maxTemperature: 293.15
+
+- type: reagent
+ id: TestReagent
+ name: reagent-name-nothing
+ desc: reagent-desc-nothing
+ physicalDesc: reagent-physical-desc-nothing
+";
+
+ [Test]
+ public async Task CryostasisBeakerPreventsHeating()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var testMap = await pair.CreateTestMap();
+
+ await server.WaitPost(() =>
+ {
+ var solutionSystem = server.System();
+
+ var beaker = server.EntMan.SpawnEntity("TestCryostasisBeaker", testMap.GridCoords);
+
+ Assert.That(solutionSystem.TryGetSolution(beaker, "beaker", out var solutionEntity, out var solution));
+
+ solutionSystem.TryAddReagent(solutionEntity.Value, "TestReagent", FixedPoint2.New(10));
+
+ solutionSystem.SetTemperature(solutionEntity.Value, 500.0f);
+
+ Assert.That(solution!.Temperature, Is.LessThanOrEqualTo(293.15f));
+
+ solutionSystem.AddThermalEnergy(solutionEntity.Value, 10000.0f);
+
+ Assert.That(solution.Temperature, Is.LessThanOrEqualTo(293.15f));
+ });
+ }
+
+ [Test]
+ public async Task NormalBeakerAllowsHeating()
+ {
+ await using var pair = await PoolManager.GetServerClient();
+ var server = pair.Server;
+
+ var testMap = await pair.CreateTestMap();
+
+ await server.WaitPost(() =>
+ {
+ var solutionSystem = server.System();
+
+ var beaker = server.EntMan.SpawnEntity("TestCryostasisBeaker", testMap.GridCoords);
+ if (server.EntMan.HasComponent(beaker))
+ server.EntMan.RemoveComponent(beaker);
+
+ Assert.That(solutionSystem.TryGetSolution(beaker, "beaker", out var solutionEntity, out var solution));
+
+ solutionSystem.TryAddReagent(solutionEntity.Value, "TestReagent", FixedPoint2.New(10));
+
+ solutionSystem.SetTemperature(solutionEntity.Value, 500.0f);
+
+ Assert.That(solution!.Temperature, Is.EqualTo(500.0f));
+ });
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs b/Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs
new file mode 100644
index 0000000000..ea06967c89
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs
@@ -0,0 +1,2125 @@
+//
+using System;
+using System.Collections.Generic;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresServerDbContext))]
+ [Migration("20250723055137_AdminLogsCurtime")]
+ partial class AdminLogsCurtime
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.1")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Deadminned")
+ .HasColumnType("boolean")
+ .HasColumnName("deadminned");
+
+ b.Property("Suspended")
+ .HasColumnType("boolean")
+ .HasColumnName("suspended");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("CurTime")
+ .HasColumnType("bigint")
+ .HasColumnName("cur_time");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Impact")
+ .HasColumnType("smallint")
+ .HasColumnName("impact");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("json");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("RoundId", "Id")
+ .HasName("PK_admin_log");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Message")
+ .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("IX_admin_log_type");
+
+ b.ToTable("admin_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.HasKey("RoundId", "LogId", "PlayerUserId")
+ .HasName("PK_admin_log_player");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+ b.ToTable("admin_log_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Dismissed")
+ .HasColumnType("boolean")
+ .HasColumnName("dismissed");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Seen")
+ .HasColumnType("boolean")
+ .HasColumnName("seen");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_messages");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_messages_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_messages_round_id");
+
+ b.ToTable("admin_messages", null, t =>
+ {
+ t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_notes_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Secret")
+ .HasColumnType("boolean")
+ .HasColumnName("secret");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_notes");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_notes_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_notes_round_id");
+
+ b.ToTable("admin_notes", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank");
+
+ b.ToTable("admin_rank", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank_flag");
+
+ b.HasIndex("AdminRankId");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_watchlists_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_watchlists");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_watchlists_round_id");
+
+ b.ToTable("admin_watchlists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("antag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("antag_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_antag");
+
+ b.HasIndex("ProfileId", "AntagName")
+ .IsUnique();
+
+ b.ToTable("antag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("assigned_user_id_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_assigned_user_id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.HasIndex("UserName")
+ .IsUnique();
+
+ b.ToTable("assigned_user_id", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_template_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("Length")
+ .HasColumnType("interval")
+ .HasColumnName("length");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_template");
+
+ b.ToTable("ban_template", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("UserId")
+ .HasName("PK_blacklist");
+
+ b.ToTable("blacklist", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Denied")
+ .HasColumnType("smallint")
+ .HasColumnName("denied");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("Trust")
+ .HasColumnType("real")
+ .HasColumnName("trust");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_connection_log");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_connection_log_server_id");
+
+ b.HasIndex("Time");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("connection_log", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ipintel_cache_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Score")
+ .HasColumnType("real")
+ .HasColumnName("score");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.HasKey("Id")
+ .HasName("PK_ipintel_cache");
+
+ b.ToTable("ipintel_cache", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("job_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_job");
+
+ b.HasIndex("ProfileId");
+
+ b.HasIndex("ProfileId", "JobName")
+ .IsUnique();
+
+ b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+ .IsUnique()
+ .HasFilter("priority = 3");
+
+ b.ToTable("job", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("play_time_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("PlayerId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_id");
+
+ b.Property("TimeSpent")
+ .HasColumnType("interval")
+ .HasColumnName("time_spent");
+
+ b.Property("Tracker")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("tracker");
+
+ b.HasKey("Id")
+ .HasName("PK_play_time");
+
+ b.HasIndex("PlayerId", "Tracker")
+ .IsUnique();
+
+ b.ToTable("play_time", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Player", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("player_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FirstSeenTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("first_seen_time");
+
+ b.Property("LastReadRules")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_read_rules");
+
+ b.Property("LastSeenAddress")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("last_seen_address");
+
+ b.Property("LastSeenTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_seen_time");
+
+ b.Property("LastSeenUserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("last_seen_user_name");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_player");
+
+ b.HasAlternateKey("UserId")
+ .HasName("ak_player_user_id");
+
+ b.HasIndex("LastSeenUserName");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("player", null, t =>
+ {
+ t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Preference", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminOOCColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("admin_ooc_color");
+
+ b.PrimitiveCollection>("ConstructionFavorites")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("construction_favorites");
+
+ b.Property("SelectedCharacterSlot")
+ .HasColumnType("integer")
+ .HasColumnName("selected_character_slot");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_preference");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("preference", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Age")
+ .HasColumnType("integer")
+ .HasColumnName("age");
+
+ b.Property("CharacterName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("char_name");
+
+ b.Property("EyeColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("eye_color");
+
+ b.Property("FacialHairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_color");
+
+ b.Property("FacialHairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_name");
+
+ b.Property("FlavorText")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flavor_text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("gender");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_color");
+
+ b.Property("HairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_name");
+
+ b.Property("Markings")
+ .HasColumnType("jsonb")
+ .HasColumnName("markings");
+
+ b.Property("PreferenceId")
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ b.Property("PreferenceUnavailable")
+ .HasColumnType("integer")
+ .HasColumnName("pref_unavailable");
+
+ b.Property("Sex")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("sex");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("skin_color");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasColumnName("slot");
+
+ b.Property("SpawnPriority")
+ .HasColumnType("integer")
+ .HasColumnName("spawn_priority");
+
+ b.Property("Species")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("species");
+
+ b.HasKey("Id")
+ .HasName("PK_profile");
+
+ b.HasIndex("PreferenceId")
+ .HasDatabaseName("IX_profile_preference_id");
+
+ b.HasIndex("Slot", "PreferenceId")
+ .IsUnique();
+
+ b.ToTable("profile", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("LoadoutName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("loadout_name");
+
+ b.Property("ProfileLoadoutGroupId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_loadout");
+
+ b.HasIndex("ProfileLoadoutGroupId");
+
+ b.ToTable("profile_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("GroupName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("group_name");
+
+ b.Property("ProfileRoleLoadoutId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_role_loadout_id");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_loadout_group");
+
+ b.HasIndex("ProfileRoleLoadoutId");
+
+ b.ToTable("profile_loadout_group", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_role_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("EntityName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("entity_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.Property("RoleName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_name");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_role_loadout");
+
+ b.HasIndex("ProfileId");
+
+ b.ToTable("profile_role_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+ {
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("RoleId")
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("PlayerUserId", "RoleId")
+ .HasName("PK_role_whitelists");
+
+ b.ToTable("role_whitelists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ServerId")
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_date");
+
+ b.HasKey("Id")
+ .HasName("PK_round");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_round_server_id");
+
+ b.HasIndex("StartDate");
+
+ b.ToTable("round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Server", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_server");
+
+ b.ToTable("server", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_server_ban");
+
+ b.HasIndex("Address");
+
+ b.HasIndex("BanningAdmin");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_server_ban_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_server_ban_round_id");
+
+ b.ToTable("server_ban", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+ t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("Flags")
+ .HasColumnType("integer")
+ .HasColumnName("flags");
+
+ b.HasKey("UserId")
+ .HasName("PK_server_ban_exemption");
+
+ b.ToTable("server_ban_exemption", null, t =>
+ {
+ t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_hit_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("ConnectionId")
+ .HasColumnType("integer")
+ .HasColumnName("connection_id");
+
+ b.HasKey("Id")
+ .HasName("PK_server_ban_hit");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+ b.HasIndex("ConnectionId")
+ .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+ b.ToTable("server_ban_hit", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_role_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_server_role_ban");
+
+ b.HasIndex("Address");
+
+ b.HasIndex("BanningAdmin");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_server_role_ban_round_id");
+
+ b.ToTable("server_role_ban", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+ t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("role_unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("UnbanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("unbanning_admin");
+
+ b.HasKey("Id")
+ .HasName("PK_server_role_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("server_role_unban", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("UnbanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("unbanning_admin");
+
+ b.HasKey("Id")
+ .HasName("PK_server_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("server_unban", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Trait", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("trait_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property