Merge remote-tracking branch 'sunrise'

This commit is contained in:
ReWAFFlution 2025-08-25 05:43:34 +03:00
commit c5fca9e384
715 changed files with 17289 additions and 5984 deletions

View file

@ -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

View file

@ -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.

View file

@ -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}
<details><summary>📋 Click to view XAML source</summary>
```xml
{content_preview}
```
</details>
"""
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<<EOF")
print(preview_content)
print("EOF")
if __name__ == '__main__':
main()

View file

@ -1,279 +0,0 @@
#!/usr/bin/env python3
"""
Simple XAML visual mockup generator using Pillow.
Creates basic visual representations of XAML layouts.
"""
from PIL import Image, ImageDraw, ImageFont
from typing import Dict, List, Tuple, Optional
import os
import sys
# Add the script directory to path to import xaml-preview
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
try:
from xaml_preview import ControlInfo, parse_xaml_structure
except ImportError:
# If we can't import, we'll redefine the needed parts
import xml.etree.ElementTree as ET
from dataclasses import dataclass
@dataclass
class ControlInfo:
name: str
element_type: str
attributes: Dict[str, str]
children: List['ControlInfo']
text_content: Optional[str] = None
namespace: Optional[str] = None
def parse_xaml_structure(file_path: str) -> 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 <xaml-file> [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()

View file

@ -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 }}

3
.gitignore vendored
View file

@ -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.

View file

@ -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<IClientConsoleHost>();
// 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);
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;

View file

@ -1,5 +1,6 @@
<Control xmlns="https://spacestation14.io"
xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls">
xmlns:aui="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<PanelContainer StyleClasses="BackgroundDark">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical">
@ -52,6 +53,13 @@
<Button Name="ExportLogs" Access="Public" Text="{Loc admin-logs-export}"/>
<Button Name="PopOutButton" Access="Public" Text="{Loc admin-logs-pop-out}"/>
</BoxContainer>
<BoxContainer HorizontalExpand="True">
<Button Name="RenderRichTextButton" Access="Public" Text="{Loc admin-logs-render-rich-text}"
StyleClasses="OpenRight" ToggleMode="True"/>
<Button Name="RemoveMarkupButton" Access="Public" Text="{Loc admin-logs-remove-markup}"
StyleClasses="OpenLeft" ToggleMode="True"/>
<Control HorizontalExpand="True"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<LineEdit Name="LogSearch" Access="Public" StyleClasses="actionSearchBox"
HorizontalExpand="true" PlaceHolder="{Loc admin-logs-search-logs-placeholder}"/>

View file

@ -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<LogType> SelectedTypes { get; } = new();
public HashSet<Guid> 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<LogType> 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<SharedAdminLog> 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;

View file

@ -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();
}

View file

@ -0,0 +1,14 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Orientation="Vertical">
<BoxContainer Margin="2">
<Collapsible>
<CollapsibleHeading Name="DetailsHeading" Access="Public">
<RichTextLabel Margin="20 0 0 0" Name="Message" MinSize="50 10" VerticalExpand="True" Access="Public" />
</CollapsibleHeading>
<CollapsibleBody Name="DetailsBody" Access="Public" />
</Collapsible>
</BoxContainer>
<cc:HSeparator/>
</BoxContainer>

View file

@ -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<IConfigurationManager>();
RobustXamlLoader.Load(this);
Log = log;
_rawMessage = $"{log.Date:HH:mm:ss}: {log.Message}";
Message.SetMessage(_rawMessage);
DetailsHeading.OnToggled += DetailsToggled;
}
/// <summary>
/// Sets text to be highlighted from a search result, and renders rich text, or removes all rich text markup.
/// </summary>
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);
}
/// <summary>
/// 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.
/// </summary>
private void DetailsToggled(BaseButton.ButtonToggledEventArgs args)
{
if (!args.Pressed || DetailsBody.ChildCount > 0)
return;
DetailsBody.AddChild(new AdminLogEntryDetails(Log));
DetailsHeading.OnToggled -= DetailsToggled;
}
}

View file

@ -0,0 +1,44 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
StyleClasses="BackgroundDark">
<BoxContainer Orientation="Vertical" Margin="4">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc admin-logs-field-type}" Margin="0 0 4 0"/>
<Label Name="Type" Text="None" StyleClasses="LabelSecondaryColor" Access="Public"/>
</BoxContainer>
<cc:HSeparator/>
<BoxContainer Orientation="Vertical">
<Label Text="{Loc admin-logs-field-impact}" Margin="0 0 4 0"/>
<Label Name="Impact" Text="None" StyleClasses="LabelSecondaryColor" Access="Public"/>
</BoxContainer>
</BoxContainer>
<cc:VSeparator/>
<BoxContainer Orientation="Vertical" Margin="4">
<Label Text="{Loc admin-logs-field-time-header}"/>
<cc:HSeparator/>
<BoxContainer>
<Label Text="{Loc admin-logs-field-time-local}" Margin="0 0 4 0" HorizontalExpand="True"/>
<Label Name="LocalTime" Text="None" StyleClasses="LabelSecondaryColor" Access="Public" HorizontalExpand="True"/>
</BoxContainer>
<cc:HSeparator/>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc admin-logs-field-time-utc}" Margin="0 0 4 0" HorizontalExpand="True"/>
<Label Name="UTCTime" Text="None" StyleClasses="LabelSecondaryColor" Access="Public" HorizontalExpand="True"/>
</BoxContainer>
<cc:HSeparator/>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc admin-logs-field-time-round}" Margin="0 0 4 0" HorizontalExpand="True"/>
<Label Name="CurTime" Text="None" StyleClasses="LabelSecondaryColor" Access="Public" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
<cc:VSeparator/>
<BoxContainer Orientation="Vertical" Margin="4" HorizontalExpand="True">
<Label Text="{Loc admin-logs-field-players-header}"/>
<cc:HSeparator/>
<BoxContainer Orientation="Horizontal" Margin="4" VerticalExpand="True">
<controls:ListContainer Name="PlayerListContainer" Access="Public" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>

View file

@ -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<IEntityManager>();
_uiManager = IoCManager.Resolve<IUserInterfaceManager>();
_adminSystem = _entManager.System<AdminSystem>();
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<PlayerListData>();
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<VerbMenuUIController>().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);
}
}

View file

@ -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<MapComponent>(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<StationSystem>().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<StationSystem>().GetStationNames());
break;
case ObjectsTabSelection.Grids:
{
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
{
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
var query = _entityManager.AllEntityQueryEnumerator<MapGridComponent, MetaDataComponent>();
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<MapComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out _, out var metadata))
{
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
var query = _entityManager.AllEntityQueryEnumerator<MapComponent, MetaDataComponent>();
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);
}

View file

@ -1,5 +1,6 @@
<PanelContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Name="BackgroundColorPanel">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
@ -20,7 +21,7 @@
HorizontalExpand="True"
ClipText="True"/>
<customControls:VSeparator/>
<Button Name="DeleteButton"
<controls:ConfirmButton Name="DeleteButton"
Text="{Loc object-tab-entity-delete}"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"

View file

@ -59,7 +59,6 @@ public sealed partial class PlayerTab : Control
_config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;

View file

@ -3,17 +3,13 @@ using Content.Shared.CCVar;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Log;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Linq;
using System.Numerics;
using Robust.Client.GameObjects;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
@ -31,6 +27,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@ -65,18 +62,19 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
get => _overlayEnabled;
set
{
if (_overlayEnabled == value) return;
if (_overlayEnabled == value)
return;
_overlayEnabled = value;
var overlayManager = IoCManager.Resolve<IOverlayManager>();
if (_overlayEnabled)
{
_overlay = new AmbientSoundOverlay(EntityManager, this, EntityManager.System<EntityLookupSystem>());
overlayManager.AddOverlay(_overlay);
_overlayManager.AddOverlay(_overlay);
}
else
{
overlayManager.RemoveOverlay(_overlay!);
_overlayManager.RemoveOverlay(_overlay!);
_overlay = null;
}
}

View file

@ -3,11 +3,8 @@ using Content.Client.Gameplay;
using Content.Shared.Audio;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Random;
using Content.Shared.Random.Rules;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
@ -25,6 +22,7 @@ public sealed partial class ContentAudioSystem
{
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
@ -61,7 +59,7 @@ public sealed partial class ContentAudioSystem
private void InitializeAmbientMusic()
{
Subs.CVar(_configManager, CCVars.AmbientMusicVolume, AmbienceCVarChanged, true);
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("audio.ambience");
_sawmill = _logManager.GetSawmill("audio.ambience");
// Reset audio
_nextAudio = TimeSpan.MaxValue;

View file

@ -0,0 +1,32 @@
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader.Cartridges;
using Robust.Client.UserInterface;
namespace Content.Client.CartridgeLoader.Cartridges;
public sealed partial class NavigatorUi : UIFragment
{
private NavigatorUiFragment? _fragment;
private IEntityManager? _entManager;
public override Control GetUIFragmentRoot()
{
return _fragment!;
}
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_entManager = IoCManager.Resolve<IEntityManager>();
_fragment = new NavigatorUiFragment();
_fragment.Setup(fragmentOwner);
}
public override void UpdateState(BoundUserInterfaceState state)
{
if (state is not NavigatorUiState navigatorState || _entManager == null)
return;
var mapUid = _entManager.GetEntity(navigatorState.MapUid);
_fragment?.UpdateState(mapUid, navigatorState.StationName);
}
}

View file

@ -0,0 +1,15 @@
<cartridges:NavigatorUiFragment xmlns:cartridges="clr-namespace:Content.Client.CartridgeLoader.Cartridges"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns="https://spacestation14.io" Margin="1 0 2 0"
Orientation="Vertical">
<PanelContainer StyleClasses="BackgroundDark"></PanelContainer>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<controls:StripeBack Name="StationNameContainer">
<PanelContainer>
<Label Name="StationNameLabel" Align="Center" Text="{Loc 'navigator-cartridge-loading'}" StyleClasses="SubText"/>
</PanelContainer>
</controls:StripeBack>
<PanelContainer Name="NavMapContainer" HorizontalExpand="True" VerticalExpand="True" Margin="4">
</PanelContainer>
</BoxContainer>
</cartridges:NavigatorUiFragment>

View file

@ -0,0 +1,74 @@
using Content.Client.Pinpointer.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using System.Numerics;
namespace Content.Client.CartridgeLoader.Cartridges;
[GenerateTypedNameReferences]
public sealed partial class NavigatorUiFragment : BoxContainer
{
[Dependency] private readonly IEntityManager _entManager = default!;
public NavMapControl? NavMap;
private EntityUid? _owner;
public NavigatorUiFragment()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
}
public void Setup(EntityUid? owner)
{
_owner = owner;
NavMap = new NavMapControl();
NavMap.Owner = owner;
NavMap.HorizontalExpand = true;
NavMap.VerticalExpand = true;
NavMapContainer.AddChild(NavMap);
}
public void UpdateState(EntityUid? mapUid, string stationName)
{
StationNameLabel.Text = stationName;
if (NavMap != null && mapUid != null)
{
NavMap.MapUid = mapUid;
}
// Track owner position on the map
UpdateOwnerPosition();
}
private void UpdateOwnerPosition()
{
if (NavMap == null || _owner == null || !_entManager.EntityExists(_owner.Value))
return;
var transformSystem = _entManager.System<SharedTransformSystem>();
var ownerCoords = transformSystem.GetMapCoordinates(_owner.Value);
var ownerEntityCoords = new EntityCoordinates(_owner.Value, Vector2.Zero);
// Clear previous owner tracking
NavMap.TrackedCoordinates.Clear();
// Add owner position as a tracked coordinate
NavMap.TrackedCoordinates[ownerEntityCoords] = (true, Color.Red);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
// Periodically update owner position
UpdateOwnerPosition();
}
}

View file

@ -0,0 +1,30 @@
using Content.Shared.Changeling.Components;
using Content.Shared.Changeling.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Changeling.Systems;
public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingIdentityComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAfterAutoHandleState(Entity<ChangelingIdentityComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
public void UpdateUi(EntityUid uid)
{
if (_ui.TryGetOpenUi(uid, ChangelingTransformUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View file

@ -1,8 +1,8 @@
using Content.Shared.Changeling.Transform;
using Content.Shared.Changeling.Systems;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Changeling.Transform;
namespace Content.Client.Changeling.UI;
[UsedImplicitly]
public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
@ -16,16 +16,16 @@ public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owne
_window = this.CreateWindow<ChangelingTransformMenu>();
_window.OnIdentitySelect += SendIdentitySelect;
_window.Update(Owner);
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (state is not ChangelingTransformBoundUserInterfaceState current)
if (_window == null)
return;
_window?.UpdateState(current);
_window.Update(Owner);
}
public void SendIdentitySelect(NetEntity identityId)

View file

@ -1,11 +1,11 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Changeling.Transform;
using Content.Shared.Changeling.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Changeling.Transform;
namespace Content.Client.Changeling.UI;
[GenerateTypedNameReferences]
public sealed partial class ChangelingTransformMenu : RadialMenu
@ -19,13 +19,15 @@ public sealed partial class ChangelingTransformMenu : RadialMenu
IoCManager.InjectDependencies(this);
}
public void UpdateState(ChangelingTransformBoundUserInterfaceState state)
public void Update(EntityUid uid)
{
Main.DisposeAllChildren();
foreach (var identity in state.Identites)
{
var identityUid = _entity.GetEntity(identity);
if (!_entity.TryGetComponent<ChangelingIdentityComponent>(uid, out var identityComp))
return;
foreach (var identityUid in identityComp.ConsumedIdentities)
{
if (!_entity.TryGetComponent<MetaDataComponent>(identityUid, out var metadata))
continue;
@ -48,7 +50,7 @@ public sealed partial class ChangelingTransformMenu : RadialMenu
entView.SetEntity(identityUid);
button.OnButtonUp += _ =>
{
OnIdentitySelect?.Invoke(identity);
OnIdentitySelect?.Invoke(_entity.GetNetEntity(identityUid));
Close();
};
button.AddChild(entView);

View file

@ -23,6 +23,8 @@ public sealed class ImplanterSystem : SharedImplanterSystem
{
if (_uiSystem.TryGetOpenUi<DeimplantBoundUserInterface>(uid, DeimplantUiKey.Key, out var bui))
{
// TODO: Don't use protoId for deimplanting
// and especially not raw strings!
Dictionary<string, string> implants = new();
foreach (var implant in component.DeimplantWhitelist)
{

View file

@ -0,0 +1,5 @@
using Content.Shared.Implants;
namespace Content.Client.Implants;
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem;

View file

@ -1,8 +0,0 @@
using Content.Shared.Kitchen;
namespace Content.Client.Kitchen;
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
{
}

View file

@ -30,6 +30,10 @@ namespace Content.Client.Lathe.UI
{
SendMessage(new LatheQueueRecipeMessage(recipe, amount));
};
_menu.QueueDeleteAction += index => SendMessage(new LatheDeleteRequestMessage(index));
_menu.QueueMoveUpAction += index => SendMessage(new LatheMoveRequestMessage(index, -1));
_menu.QueueMoveDownAction += index => SendMessage(new LatheMoveRequestMessage(index, 1));
_menu.DeleteFabricatingAction += () => SendMessage(new LatheAbortFabricationMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)

View file

@ -1,6 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:ui="clr-namespace:Content.Client.Materials.UI"
Title="{Loc 'lathe-menu-title'}"
MinSize="550 450"
@ -110,6 +111,18 @@
HorizontalAlignment="Left"
Margin="130 0 0 0">
</Label>
<Button
Name="DeleteFabricating"
Margin="0"
Text="✖"
SetSize="38 32"
HorizontalAlignment="Right"
ToolTip="{Loc 'lathe-menu-delete-fabricating-tooltip'}">
<Button.StyleClasses>
<system:String>Caution</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
</PanelContainer>
</BoxContainer>
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">

View file

@ -26,6 +26,10 @@ public sealed partial class LatheMenu : DefaultWindow
public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed;
public event Action<string, int>? RecipeQueueAction;
public event Action<int>? QueueDeleteAction;
public event Action<int>? QueueMoveUpAction;
public event Action<int>? QueueMoveDownAction;
public event Action? DeleteFabricatingAction;
public List<ProtoId<LatheRecipePrototype>> 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
/// </summary>
/// <param name="queue"></param>
public void PopulateQueueList(IReadOnlyCollection<ProtoId<LatheRecipePrototype>> queue)
public void PopulateQueueList(IReadOnlyCollection<LatheRecipeBatch> 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++;
}

View file

@ -0,0 +1,35 @@
<Control xmlns="https://spacestation14.io"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<BoxContainer Orientation="Horizontal">
<BoxContainer
Name="RecipeDisplayContainer"
Margin="0 0 4 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MinSize="32 32"
/>
<Label Name="RecipeName" HorizontalExpand="True" />
<Button
Name="MoveUp"
Margin="0"
Text="⏶"
StyleClasses="OpenRight"
ToolTip="{Loc 'lathe-menu-move-up-tooltip'}"/>
<Button
Name="MoveDown"
Margin="0"
Text="⏷"
StyleClasses="OpenBoth"
ToolTip="{Loc 'lathe-menu-move-down-tooltip'}"/>
<Button
Name="Delete"
Margin="0"
Text="✖"
ToolTip="{Loc 'lathe-menu-delete-item-tooltip'}">
<Button.StyleClasses>
<system:String>Caution</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
</BoxContainer>
</Control>

View file

@ -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<int>? OnDeletePressed;
public Action<int>? OnMoveUpPressed;
public Action<int>? 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);
};
}
}

View file

@ -0,0 +1,5 @@
using Content.Shared.Morgue;
namespace Content.Client.Morgue;
public sealed class CrematoriumSystem : SharedCrematoriumSystem;

View file

@ -0,0 +1,5 @@
using Content.Shared.Morgue;
namespace Content.Client.Morgue;
public sealed class MorgueSystem : SharedMorgueSystem;

View file

@ -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<IOverlayManager>();
if (value == PathfindingDebugMode.None)
{
Breadcrumbs.Clear();
Polys.Clear();
overlayManager.RemoveOverlay<PathfindingOverlay>();
_overlayManager.RemoveOverlay<PathfindingOverlay>();
}
else if (!overlayManager.HasOverlay<PathfindingOverlay>())
else if (!_overlayManager.HasOverlay<PathfindingOverlay>())
{
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)

View file

@ -9,6 +9,9 @@
<ui:OptionDropDown Name="DropDownPlayerTabSymbolSetting" Title="{Loc 'ui-options-admin-player-tab-symbol-setting'}" />
<ui:OptionDropDown Name="DropDownPlayerTabRoleSetting" Title="{Loc 'ui-options-admin-player-tab-role-setting'}" />
<ui:OptionDropDown Name="DropDownPlayerTabColorSetting" Title="{Loc 'ui-options-admin-player-tab-color-setting'}" />
<Label Text="{Loc 'ui-options-admin-logs-title'}"
StyleClasses="LabelKeyText"/>
<ui:OptionColorSlider Name="ColorSliderLogsHighlight" Title="{Loc 'ui-options-admin-logs-highlight-color'}" />
<Label Text="{Loc 'ui-options-admin-overlay-title'}"
StyleClasses="LabelKeyText"/>
<ui:OptionDropDown Name="DropDownOverlayAntagFormat" Title="{Loc 'ui-options-admin-overlay-antag-format'}" />

View file

@ -51,6 +51,8 @@ public sealed partial class AdminOptionsTab : Control
playerTabSymbolSettings.Add(new OptionDropDownCVar<string>.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);

View file

@ -31,7 +31,6 @@
<CheckBox Name="TtsRadioGhostCheckBox" Text="{Loc 'ui-options-tts-radio-ghost-enabled'}" />
<ui:OptionSlider Name="SliderTts" Title="{Loc 'ui-options-tts-volume'}" />
<ui:OptionSlider Name="SliderTtsRadio" Title="{Loc 'ui-options-tts-radio-volume'}" />
<ui:OptionSlider Name="SliderTtsAnnounce" Title="{Loc 'ui-options-tts-announce-volume'}" />
<CheckBox Name="TapePlayerClientCheckBox" Text="{Loc 'ui-options-tape-player-enabled'}" />
<CheckBox Name="JumpSoundDisableCheckBox" Text="{Loc 'ui-options-jump-sound-disable'}" />
<CheckBox Name="VoteMusicDisableCheckBox" Text="{Loc 'ui-options-vote-music-disable'}" />

View file

@ -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);

View file

@ -13,8 +13,7 @@
<BoxContainer Orientation="Vertical" VerticalAlignment="Stretch">
<TextureButton Name="HeaderImage" HorizontalAlignment="Center" VerticalAlignment="Top" MouseFilter="Ignore"/>
<!-- Sunrise-start -->
<TextureRect Name="ImageContent" HorizontalAlignment="Stretch" VerticalAlignment="Top"
HorizontalExpand="True" VerticalExpand="True" Stretch="KeepAspectCentered" MouseFilter="Ignore"/>
<TextureRect Name="ImageContent" HorizontalAlignment="Stretch" VerticalAlignment="Top" Stretch="KeepAspectCentered" Visible="False"/>
<!-- Sunrise-end -->
<Control Name="TextAlignmentPadding" VerticalAlignment="Top"/>
<RichTextLabel Name="BlankPaperIndicator" StyleClasses="LabelSecondaryColor" VerticalAlignment="Top" HorizontalAlignment="Center"/>

View file

@ -290,17 +290,17 @@ namespace Content.Client.Paper.UI
BlankPaperIndicator.Visible = !isEditing && state.Text.Length == 0;
// Sunrise-Start
var sprite = _entitySystemManager.GetEntitySystem<SpriteSystem>();
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<SpriteSystem>();
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();

View file

@ -135,7 +135,7 @@ public partial class NavMapControl : MapGridControl
},
VerticalExpand = false,
HorizontalExpand = true,
SetWidth = 650f,
//SetWidth = 650f, Sunrise-Edit
Children =
{
new BoxContainer()

View file

@ -13,8 +13,8 @@
</PanelContainer>
</controls:StripeBack>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" VerticalAlignment="Top">
<ui:NavMapControl Name="NavMapScreen"/>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Stretch">
<ui:NavMapControl Name="NavMapScreen" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" HorizontalExpand="True" VerticalExpand="True"/>
<BoxContainer Orientation="Vertical" SetWidth="200">
<!-- Search bar -->

View file

@ -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<RadiationSystem>();
_mapSystem = _entityManager.System<SharedMapSystem>();
var cache = IoCManager.Resolve<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
_font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 8);
}
protected override void Draw(in OverlayDrawArgs args)

View file

@ -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<IOverlayManager>();
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;
}
}

View file

@ -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
{
}

View file

@ -31,7 +31,7 @@ public sealed class EntityStorageSystem : SharedEntityStorageSystem
SubscribeLocalEvent<EntityStorageComponent, ComponentHandleState>(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;

View file

@ -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;

View file

@ -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<IOverlayManager>();
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<GunSpreadOverlay>();
_overlayManager.RemoveOverlay<GunSpreadOverlay>();
}
}
}

View file

@ -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;
}

View file

@ -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<QueuedTts> _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<PlayTTSEvent>(OnPlayTTS);
SubscribeNetworkEvent<AnnounceTtsEvent>(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);
}

View file

@ -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<MobPriceComponent>())
{
Assert.That(proto.TryGetComponent<BodyComponent>(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no BodyComponent!");
Assert.That(proto.TryGetComponent<MobStateComponent>(out _, componentFactory), $"Found MobPriceComponent on {proto.ID}, but no MobStateComponent!");
}
});

View file

@ -21,6 +21,7 @@ namespace Content.IntegrationTests.Tests.Doors
components:
- type: Physics
bodyType: Dynamic
- type: GravityAffected
- type: Fixtures
fixtures:
fix1:

View file

@ -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));
}
}

View file

@ -19,6 +19,7 @@ namespace Content.IntegrationTests.Tests.Gravity
- type: Alerts
- type: Physics
bodyType: Dynamic
- type: GravityAffected
- type: entity
name: WeightlessGravityGeneratorDummy

View file

@ -76,8 +76,8 @@ namespace Content.IntegrationTests.Tests
Assert.Multiple(() =>
{
Assert.That(generatorComponent.GravityActive, Is.True);
Assert.That(!entityMan.GetComponent<GravityComponent>(grid1).EnabledVV);
Assert.That(entityMan.GetComponent<GravityComponent>(grid2).EnabledVV);
Assert.That(!entityMan.GetComponent<GravityComponent>(grid1).Enabled);
Assert.That(entityMan.GetComponent<GravityComponent>(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<GravityComponent>(grid2).EnabledVV, Is.False);
Assert.That(entityMan.GetComponent<GravityComponent>(grid2).Enabled, Is.False);
});
});

View file

@ -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<StackPrototype> InflatableWallStack = "InflatableWall";
}

View file

@ -144,6 +144,7 @@ public abstract partial class InteractionTest
- type: Stripping
- type: Puller
- type: Physics
- type: GravityAffected
- type: Tag
tags:
- CanPilot

View file

@ -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<SharedSolutionContainerSystem>();
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<SharedSolutionContainerSystem>();
var beaker = server.EntMan.SpawnEntity("TestCryostasisBeaker", testMap.GridCoords);
if (server.EntMan.HasComponent<CryostasisBeakerComponent>(beaker))
server.EntMan.RemoveComponent<CryostasisBeakerComponent>(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));
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class AdminLogsCurtime : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "cur_time",
table: "admin_log",
type: "bigint",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "cur_time",
table: "admin_log");
}
}
}

View file

@ -146,6 +146,10 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer")
.HasColumnName("admin_log_id");
b.Property<long>("CurTime")
.HasColumnType("bigint")
.HasColumnName("cur_time");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone")
.HasColumnName("date");

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class AdminLogsCurtime : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
name: "cur_time",
table: "admin_log",
type: "INTEGER",
nullable: false,
defaultValue: 0L);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "cur_time",
table: "admin_log");
}
}
}

View file

@ -133,6 +133,10 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("admin_log_id");
b.Property<long>("CurTime")
.HasColumnType("INTEGER")
.HasColumnName("cur_time");
b.Property<DateTime>("Date")
.HasColumnType("TEXT")
.HasColumnName("date");

View file

@ -720,6 +720,11 @@ namespace Content.Server.Database
[Required] public DateTime Date { get; set; }
/// <summary>
/// The current time in the round in ticks since the start of the round.
/// </summary>
public long CurTime { get; set; }
[Required] public string Message { get; set; } = default!;
[Required, Column(TypeName = "jsonb")] public JsonDocument Json { get; set; } = default!;

View file

@ -1,4 +1,4 @@
using Content.Server.Storage.Components;
using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;

View file

@ -1,5 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Administration;
using Content.Shared.Storage.Components;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;

View file

@ -1,4 +1,4 @@
using Content.Server.Storage.Components;
using Content.Shared.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Shared.Administration;
using Robust.Shared.Console;

View file

@ -51,7 +51,7 @@ public sealed partial class AdminLogManager
private void CacheLog(AdminLog log)
{
var players = log.Players.Select(player => player.PlayerUserId).ToArray();
var record = new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.Message, players);
var record = new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.CurTime, log.Message, players);
CacheLog(record);
}

View file

@ -87,6 +87,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
// Per round
private int _currentRoundId;
private int _currentLogId;
private TimeSpan _currentRoundStartTime;
private int NextLogId => Interlocked.Increment(ref _currentLogId);
private GameRunLevel _runLevel = GameRunLevel.PreRoundLobby;
@ -260,6 +261,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
public void RoundStarting(int id)
{
_currentRoundStartTime = _timing.CurTime;
_currentRoundId = id;
CacheNewRound();
}
@ -316,6 +318,7 @@ public sealed partial class AdminLogManager : SharedAdminLogManager, IAdminLogMa
Type = type,
Impact = impact,
Date = DateTime.UtcNow,
CurTime = (_timing.CurTime - _currentRoundStartTime).Ticks,
Message = message,
Json = json,
Players = new List<AdminLogPlayer>(players.Count)

View file

@ -6,7 +6,6 @@ using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Server.Clothing.Systems;
using Content.Server.Electrocution;
using Content.Server.Explosion.EntitySystems;
using Content.Server.GhostKick;
@ -16,13 +15,13 @@ using Content.Server.Pointing.Components;
using Content.Server.Polymorph.Systems;
using Content.Server.Popups;
using Content.Server.Speech.Components;
using Content.Server.Storage.Components;
using Content.Server.Storage.EntitySystems;
using Content.Server.Tabletop;
using Content.Server.Tabletop.Components;
using Content.Server.Terminator.Systems;
using Content.Shared.Administration;
using Content.Shared.Administration.Components;
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Clumsy;
@ -34,6 +33,7 @@ using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Electrocution;
using Content.Shared.Gravity;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
@ -46,7 +46,7 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Slippery;
using Content.Shared.Stunnable;
using Content.Shared.Storage.Components;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
@ -56,7 +56,6 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@ -743,6 +742,11 @@ public sealed partial class AdminVerbSystem
grav.Weightless = true;
Dirty(args.Target, grav);
EnsureComp<GravityAffectedComponent>(args.Target, out var weightless);
weightless.Weightless = true;
Dirty(args.Target, weightless);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))

View file

@ -1,7 +1,7 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Shared.Anomaly.Effects.Components;
using Content.Shared.Atmos.Components;
using Robust.Shared.Map;
namespace Content.Server.Anomaly.Effects;

View file

@ -1,5 +1,4 @@
using Content.Server.Botany.Components;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Botany;
@ -16,6 +15,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Kitchen.Components;
namespace Content.Server.Botany.Systems;

View file

@ -1,7 +1,7 @@
using Content.Server.Botany.Components;
using Content.Server.Kitchen.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Kitchen.Components;
using Content.Shared.Random;
using Robust.Shared.Containers;

View file

@ -1,7 +1,6 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Botany.Components;
using Content.Server.Hands.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Atmos;
@ -26,6 +25,7 @@ using Robust.Shared.Timing;
using Content.Shared.Administration.Logs;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Kitchen.Components;
using Content.Shared.Labels.Components;
namespace Content.Server.Botany.Systems;

View file

@ -0,0 +1,6 @@
namespace Content.Server.CartridgeLoader.Cartridges;
[RegisterComponent]
public sealed partial class NavigatorCartridgeComponent : Component
{
}

View file

@ -0,0 +1,66 @@
using Content.Server.Station.Systems;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared.Station.Components;
using Robust.Shared.Map;
using System.Linq;
namespace Content.Server.CartridgeLoader.Cartridges;
public sealed class NavigatorCartridgeSystem : EntitySystem
{
[Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NavigatorCartridgeComponent, CartridgeMessageEvent>(OnUiMessage);
SubscribeLocalEvent<NavigatorCartridgeComponent, CartridgeUiReadyEvent>(OnUiReady);
}
/// <summary>
/// The ui messages received here get wrapped by a CartridgeMessageEvent and are relayed from the <see cref="CartridgeLoaderSystem"/>
/// </summary>
/// <remarks>
/// The cartridge specific ui message event needs to inherit from the CartridgeMessageEvent
/// </remarks>
private void OnUiMessage(EntityUid uid, NavigatorCartridgeComponent component, CartridgeMessageEvent args)
{
UpdateUiState(uid, GetEntity(args.LoaderUid), component);
}
/// <summary>
/// This gets called when the ui fragment needs to be updated for the first time after activating
/// </summary>
private void OnUiReady(EntityUid uid, NavigatorCartridgeComponent component, CartridgeUiReadyEvent args)
{
UpdateUiState(uid, args.Loader, component);
}
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, NavigatorCartridgeComponent? component)
{
if (!Resolve(uid, ref component))
return;
var owningStation = _stationSystem.GetOwningStation(loaderUid);
var stationName = "Unknown Station";
NetEntity? mapUid = null;
if (owningStation != null && TryComp<MetaDataComponent>(owningStation.Value, out var metaData))
{
stationName = metaData.EntityName;
// Try to get the station's primary grid for the map
if (TryComp<StationDataComponent>(owningStation.Value, out var stationData) && stationData.Grids.Count > 0)
{
// Get the first grid as the map reference and convert to NetEntity
mapUid = GetNetEntity(stationData.Grids.First());
}
}
var state = new NavigatorUiState(mapUid, stationName);
_cartridgeLoader.UpdateCartridgeUiState(loaderUid, state);
}
}

View file

@ -0,0 +1,5 @@
using Content.Shared.Changeling.Systems;
namespace Content.Server.Changeling.Systems;
public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem;

View file

@ -3,6 +3,7 @@ using System.Linq;
using System.Text;
using Content.Server._Sunrise.Chat;
using Content.Server._Sunrise.Chat.Sanitization;
using Content.Server._Sunrise.AnnouncementSpeaker;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
@ -66,6 +67,7 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly AnnouncementSpeakerSystem _announcementSpeaker = default!;
public const string DefaultAnnouncementSound = "/Audio/Announcements/announce.ogg"; // Fish-edit
@ -350,7 +352,7 @@ public sealed partial class ChatSystem : SharedChatSystem
#region Announcements
/// <summary>
/// Dispatches an announcement to all.
/// Dispatches an announcement to all stations through their speaker networks.
/// </summary>
/// <param name="message">The contents of the message</param>
/// <param name="sender">The sender (Communications Console in Communications Console Announcement)</param>
@ -370,19 +372,25 @@ public sealed partial class ChatSystem : SharedChatSystem
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
// Sunrise-start
if (playDefault && announcementSound == null)
// Sunrise-start - Only show in chat for players with working speakers nearby
var filteredPlayers = GetPlayersWithWorkingSpeakers();
if (filteredPlayers.Recipients.Any())
{
announcementSound ??= new SoundPathSpecifier(DefaultAnnouncementSound);
_chatManager.ChatMessageToManyFiltered(filteredPlayers, ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
}
// Sunrise-end
if (playTts && announcementSound != null)
// Sunrise-start - Use speaker network instead of global broadcast
if (playTts && (playDefault || announcementSound != null))
{
//_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
var announcementEv = new AnnouncementSpokeEvent(Filter.Broadcast(), message, _audio.ResolveSound(announcementSound), announceVoice);
RaiseLocalEvent(announcementEv);
if (playDefault && announcementSound == null)
{
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
}
// Send announcement to all stations through their speaker networks
_announcementSpeaker.DispatchAnnouncementToAllStations(message, announcementSound, announceVoice);
}
// Sunrise-end
@ -413,17 +421,47 @@ public sealed partial class ChatSystem : SharedChatSystem
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
// Sunrise-start
if (playDefault && announcementSound == null)
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
if (playTts && announcementSound != null)
// Sunrise-start - Filter chat recipients by working speakers
var filteredChatPlayers = FilterPlayersByWorkingSpeakers(filter);
if (filteredChatPlayers.Recipients.Any())
{
//_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
RaiseLocalEvent(new AnnouncementSpokeEvent(filter, message, _audio.ResolveSound(announcementSound), announceVoice));
_chatManager.ChatMessageToManyFiltered(filteredChatPlayers, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
}
// Sunrise-edit
// Sunrise-end
// Sunrise-start - For filtered announcements, we may want to try speaker network if source is on a station
if (playTts && (playDefault || announcementSound != null))
{
if (playDefault && announcementSound == null)
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
var resolvedSound = announcementSound != null ? _audio.ResolveSound(announcementSound) : null;
// If we have a source, try to use the station's speaker network
if (source != null)
{
var station = _stationSystem.GetOwningStation(source.Value);
if (station != null)
{
_announcementSpeaker.DispatchAnnouncementToSpeakers(station.Value, message, announcementSound, announceVoice);
}
else
{
// Fallback to old broadcast system for non-station sources
var announcementEv = new AnnouncementSpokeEvent(filter, message, resolvedSound, announceVoice);
RaiseLocalEvent(announcementEv);
}
}
else
{
// No source, fallback to old broadcast system
var announcementEv = new AnnouncementSpokeEvent(filter, message, resolvedSound, announceVoice);
RaiseLocalEvent(announcementEv);
}
}
// Sunrise-end
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
}
@ -462,18 +500,24 @@ public sealed partial class ChatSystem : SharedChatSystem
var filter = _stationSystem.GetInStation(stationDataComp);
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
// Sunrise-start
if (playDefault && announcementSound == null)
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
if (playTts && announcementSound != null)
// Sunrise-start - Filter chat recipients by working speakers
var filteredChatPlayers = FilterPlayersByWorkingSpeakers(filter);
if (filteredChatPlayers.Recipients.Any())
{
//_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
RaiseLocalEvent(new AnnouncementSpokeEvent(filter, message, _audio.ResolveSound(announcementSound), announceVoice));
_chatManager.ChatMessageToManyFiltered(filteredChatPlayers, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
}
// Sunrise-edit
// Sunrise-end
// Sunrise-start - Use speaker network for station announcements
if (playTts && (playDefault || announcementSound != null))
{
if (playDefault && announcementSound == null)
announcementSound = new SoundPathSpecifier(DefaultAnnouncementSound);
// Send announcement to this specific station's speaker network
_announcementSpeaker.DispatchAnnouncementToSpeakers(station.Value, message, announcementSound, announceVoice);
}
// Sunrise-end
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
}
@ -823,6 +867,53 @@ public sealed partial class ChatSystem : SharedChatSystem
#region Utility
/// <summary>
/// Gets all players who have working announcement speakers nearby.
/// Used to filter chat recipients for announcements.
/// </summary>
private Filter GetPlayersWithWorkingSpeakers()
{
var filteredPlayers = Filter.Empty();
foreach (var player in _playerManager.Sessions)
{
if (player.AttachedEntity is not { Valid: true } playerEntity)
continue;
if (_announcementSpeaker.HasWorkingSpeakersNearby(playerEntity))
{
filteredPlayers = filteredPlayers.AddPlayer(player);
}
}
return filteredPlayers;
}
/// <summary>
/// Filters an existing filter to only include players with working speakers nearby.
/// </summary>
private Filter FilterPlayersByWorkingSpeakers(Filter originalFilter)
{
var filteredPlayers = Filter.Empty();
foreach (var player in originalFilter.Recipients)
{
if (player.AttachedEntity is not { Valid: true } playerEntity)
continue;
if (_announcementSpeaker.HasWorkingSpeakersNearby(playerEntity))
{
filteredPlayers = filteredPlayers.AddPlayer(player);
}
}
return filteredPlayers;
}
#endregion
#region Utility
private enum MessageRangeCheckResult
{
Disallowed,

View file

@ -75,7 +75,7 @@ namespace Content.Server.Communications
/// In practise this removes the "Sent by ScugMcWawa (Slugcat Captain)" at the bottom of the announcement.
/// </summary>
[DataField]
public bool AnnounceSentBy = true;
public bool AnnounceSentBy = false;
// Sunrise-Start
[DataField("announceVoice", customTypeSerializer:typeof(PrototypeIdSerializer<TTSVoicePrototype>))]

View file

@ -262,23 +262,23 @@ namespace Content.Server.Communications
if (comp.AnnounceSentBy)
msg += "\n" + Loc.GetString("comms-console-announcement-sent-by") + " " + author;
// Sunrise-start
var voice = comp.AnnounceVoice;
if (TryComp<TTSComponent>(message.Actor, out var ttsComponent))
{
voice = ttsComponent.VoicePrototypeId;
}
// Sunrise-end
if (comp.Global)
{
// Sunrise-start
var voice = comp.AnnounceVoice;
if (TryComp<TTSComponent>(message.Actor, out var ttsComponent))
{
voice = ttsComponent.VoicePrototypeId;
}
// Sunrise-end
_chatSystem.DispatchGlobalAnnouncement(msg, title, announcementSound: comp.Sound, colorOverride: comp.Color, announceVoice: voice); // Sunrise-edit
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following global announcement: {msg}");
return;
}
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color, announceVoice: comp.AnnounceVoice);
_chatSystem.DispatchStationAnnouncement(uid, msg, title, colorOverride: comp.Color, announceVoice: voice);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Actor):player} has sent the following station announcement: {msg}");

View file

@ -1,6 +1,6 @@
using Content.Server.Storage.Components;
using Content.Shared.Construction;
using Content.Shared.Examine;
using Content.Shared.Storage.Components;
using Content.Shared.Tools.Systems;
using JetBrains.Annotations;

View file

@ -1107,7 +1107,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
players[i] = log.Players[i].PlayerUserId;
}
yield return new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.Message, players);
yield return new SharedAdminLog(log.Id, log.Type, log.Impact, log.Date, log.CurTime, log.Message, players);
}
}

View file

@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
@ -22,6 +21,7 @@ using Content.Server.Temperature.Systems;
using Content.Server.Traits.Assorted;
using Content.Server.Zombies;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Body.Components;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.EntityEffects.EffectConditions;

View file

@ -5,6 +5,7 @@ using Content.Server.Atmos.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NPC.Pathfinding;
using Content.Shared._RMC14.Explosion;
using Content.Shared.Atmos.Components;
using Content.Shared.Camera;
using Content.Shared.CCVar;
using Content.Shared.Damage;

View file

@ -166,7 +166,7 @@ public sealed class SpraySystem : EntitySystem
if (TryComp<PhysicsComponent>(user, out var body))
{
if (_gravity.IsWeightless(user, body))
if (_gravity.IsWeightless(user))
{
// push back the player
_physics.ApplyLinearImpulse(user, -impulseDirection * entity.Comp.PushbackAmount, body: body);

View file

@ -18,7 +18,7 @@ namespace Content.Server.Gravity
/// </summary>
public void RefreshGravity(EntityUid uid, GravityComponent? gravity = null)
{
if (!Resolve(uid, ref gravity))
if (!GravityQuery.Resolve(uid, ref gravity))
return;
if (gravity.Inherent)
@ -61,7 +61,7 @@ namespace Content.Server.Gravity
/// </summary>
public void EnableGravity(EntityUid uid, GravityComponent? gravity = null)
{
if (!Resolve(uid, ref gravity))
if (!GravityQuery.Resolve(uid, ref gravity))
return;
if (gravity.Enabled || gravity.Inherent)

View file

@ -29,17 +29,17 @@ public sealed class ChameleonControllerSystem : SharedChameleonControllerSystem
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, ChameleonControllerSelectedOutfitMessage>(OnSelected);
SubscribeLocalEvent<ChameleonControllerImplantComponent, ChameleonControllerSelectedOutfitMessage>(OnSelected);
SubscribeLocalEvent<ChameleonClothingComponent, InventoryRelayedEvent<ChameleonControllerOutfitSelectedEvent>>(ChameleonControllerOutfitItemSelected);
}
private void OnSelected(Entity<SubdermalImplantComponent> ent, ref ChameleonControllerSelectedOutfitMessage args)
private void OnSelected(Entity<ChameleonControllerImplantComponent> ent, ref ChameleonControllerSelectedOutfitMessage args)
{
if (!_delay.TryResetDelay(ent.Owner, true) || ent.Comp.ImplantedEntity == null || !HasComp<ChameleonControllerImplantComponent>(ent))
if (!TryComp<SubdermalImplantComponent>(ent, out var implantComp) || implantComp.ImplantedEntity == null || !_delay.TryResetDelay(ent.Owner, true))
return;
ChangeChameleonClothingToOutfit(ent.Comp.ImplantedEntity.Value, args.SelectedChameleonOutfit);
ChangeChameleonClothingToOutfit(implantComp.ImplantedEntity.Value, args.SelectedChameleonOutfit);
}
/// <summary>

View file

@ -27,6 +27,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
SubscribeLocalEvent<ImplanterComponent, DrawEvent>(OnDraw);
}
// TODO: This all needs to be moved to shared and predicted.
private void OnImplanterAfterInteract(EntityUid uid, ImplanterComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || args.Handled)

View file

@ -1,7 +1,6 @@
using Content.Server.Radio.Components;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
@ -12,7 +11,7 @@ public sealed class RadioImplantSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<RadioImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
SubscribeLocalEvent<RadioImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
SubscribeLocalEvent<RadioImplantComponent, ImplantRemovedEvent>(OnImplantRemoved);
}
/// <summary>
@ -20,19 +19,16 @@ public sealed class RadioImplantSystem : EntitySystem
/// </summary>
private void OnImplantImplanted(Entity<RadioImplantComponent> ent, ref ImplantImplantedEvent args)
{
if (args.Implanted == null)
return;
var activeRadio = EnsureComp<ActiveRadioComponent>(args.Implanted.Value);
var activeRadio = EnsureComp<ActiveRadioComponent>(args.Implanted);
foreach (var channel in ent.Comp.RadioChannels)
{
if (activeRadio.Channels.Add(channel))
ent.Comp.ActiveAddedChannels.Add(channel);
}
EnsureComp<IntrinsicRadioReceiverComponent>(args.Implanted.Value);
EnsureComp<IntrinsicRadioReceiverComponent>(args.Implanted);
var intrinsicRadioTransmitter = EnsureComp<IntrinsicRadioTransmitterComponent>(args.Implanted.Value);
var intrinsicRadioTransmitter = EnsureComp<IntrinsicRadioTransmitterComponent>(args.Implanted);
foreach (var channel in ent.Comp.RadioChannels)
{
if (intrinsicRadioTransmitter.Channels.Add(channel))
@ -43,9 +39,9 @@ public sealed class RadioImplantSystem : EntitySystem
/// <summary>
/// Removes intrinsic radio components once the Radio Implant is removed
/// </summary>
private void OnRemove(Entity<RadioImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
private void OnImplantRemoved(Entity<RadioImplantComponent> ent, ref ImplantRemovedEvent args)
{
if (TryComp<ActiveRadioComponent>(args.Container.Owner, out var activeRadioComponent))
if (TryComp<ActiveRadioComponent>(args.Implanted, out var activeRadioComponent))
{
foreach (var channel in ent.Comp.ActiveAddedChannels)
{
@ -55,11 +51,11 @@ public sealed class RadioImplantSystem : EntitySystem
if (activeRadioComponent.Channels.Count == 0)
{
RemCompDeferred<ActiveRadioComponent>(args.Container.Owner);
RemCompDeferred<ActiveRadioComponent>(args.Implanted);
}
}
if (!TryComp<IntrinsicRadioTransmitterComponent>(args.Container.Owner, out var radioTransmitterComponent))
if (!TryComp<IntrinsicRadioTransmitterComponent>(args.Implanted, out var radioTransmitterComponent))
return;
foreach (var channel in ent.Comp.TransmitterAddedChannels)
@ -70,7 +66,7 @@ public sealed class RadioImplantSystem : EntitySystem
if (radioTransmitterComponent.Channels.Count == 0 || activeRadioComponent?.Channels.Count == 0)
{
RemCompDeferred<IntrinsicRadioTransmitterComponent>(args.Container.Owner);
RemCompDeferred<IntrinsicRadioTransmitterComponent>(args.Implanted);
}
}
}

View file

@ -18,6 +18,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
SubscribeLocalEvent<StoreComponent, ImplantRelayEvent<AfterInteractUsingEvent>>(OnStoreRelay);
}
// TODO: This shouldn't be in the SubdermalImplantSystem
private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent<AfterInteractUsingEvent> implantRelay)
{
var args = implantRelay.Event;

View file

@ -1,15 +0,0 @@
namespace Content.Server.Kitchen.Components;
/// <summary>
/// Applies to items that are capable of butchering entities, or
/// are otherwise sharp for some purpose.
/// </summary>
[RegisterComponent]
public sealed partial class SharpComponent : Component
{
// TODO just make this a tool type.
public HashSet<EntityUid> Butchering = new();
[DataField("butcherDelayModifier")]
public float ButcherDelayModifier = 1.0f;
}

View file

@ -1,292 +0,0 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components;
using Content.Server.Popups;
using Content.Shared.Chat;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using static Content.Shared.Kitchen.Components.KitchenSpikeComponent;
namespace Content.Server.Kitchen.EntitySystems
{
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly IAdminLogManager _logger = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly BodySystem _bodySystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KitchenSpikeComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<KitchenSpikeComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<KitchenSpikeComponent, DragDropTargetEvent>(OnDragDrop);
//DoAfter
SubscribeLocalEvent<KitchenSpikeComponent, SpikeDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ButcherableComponent, CanDropDraggedEvent>(OnButcherableCanDrop);
}
private void OnButcherableCanDrop(Entity<ButcherableComponent> entity, ref CanDropDraggedEvent args)
{
args.Handled = true;
args.CanDrop |= entity.Comp.Type != ButcheringType.Knife;
}
/// <summary>
/// TODO: Update this so it actually meatspikes the user instead of applying lethal damage to them.
/// </summary>
private void OnSuicideByEnvironment(Entity<KitchenSpikeComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)
return;
if (!TryComp<DamageableComponent>(args.Victim, out var damageableComponent))
return;
_suicide.ApplyLethalDamage((args.Victim, damageableComponent), "Piercing");
var othersMessage = Loc.GetString("comp-kitchen-spike-suicide-other",
("victim", Identity.Entity(args.Victim, EntityManager)),
("this", entity));
_popupSystem.PopupEntity(othersMessage, args.Victim, Filter.PvsExcept(args.Victim), true);
var selfMessage = Loc.GetString("comp-kitchen-spike-suicide-self",
("this", entity));
_popupSystem.PopupEntity(selfMessage, args.Victim, args.Victim);
args.Handled = true;
}
private void OnDoAfter(Entity<KitchenSpikeComponent> entity, ref SpikeDoAfterEvent args)
{
if (args.Args.Target == null)
return;
if (TryComp<ButcherableComponent>(args.Args.Target.Value, out var butcherable))
butcherable.BeingButchered = false;
if (args.Cancelled)
{
entity.Comp.InUse = false;
return;
}
if (args.Handled)
return;
if (Spikeable(entity, args.Args.User, args.Args.Target.Value, entity.Comp, butcherable))
Spike(entity, args.Args.User, args.Args.Target.Value, entity.Comp);
entity.Comp.InUse = false;
args.Handled = true;
}
private void OnDragDrop(Entity<KitchenSpikeComponent> entity, ref DragDropTargetEvent args)
{
if (args.Handled)
return;
args.Handled = true;
if (Spikeable(entity, args.User, args.Dragged, entity.Comp))
TrySpike(entity, args.User, args.Dragged, entity.Comp);
}
private void OnInteractHand(Entity<KitchenSpikeComponent> entity, ref InteractHandEvent args)
{
if (args.Handled)
return;
if (entity.Comp.PrototypesToSpawn?.Count > 0)
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-knife-needed"), entity, args.User);
args.Handled = true;
}
}
private void OnInteractUsing(Entity<KitchenSpikeComponent> entity, ref InteractUsingEvent args)
{
if (args.Handled)
return;
if (TryGetPiece(entity, args.User, args.Used))
args.Handled = true;
}
private void Spike(EntityUid uid, EntityUid userUid, EntityUid victimUid,
KitchenSpikeComponent? component = null, ButcherableComponent? butcherable = null)
{
if (!Resolve(uid, ref component) || !Resolve(victimUid, ref butcherable))
return;
var logImpact = LogImpact.Medium;
if (HasComp<HumanoidAppearanceComponent>(victimUid))
logImpact = LogImpact.Extreme;
_logger.Add(LogType.Gib, logImpact, $"{ToPrettyString(userUid):user} kitchen spiked {ToPrettyString(victimUid):target}");
// TODO VERY SUS
component.PrototypesToSpawn = EntitySpawnCollection.GetSpawns(butcherable.SpawnedEntities, _random);
// This feels not okay, but entity is getting deleted on "Spike", for now...
component.MeatSource1p = Loc.GetString("comp-kitchen-spike-remove-meat", ("victim", victimUid));
component.MeatSource0 = Loc.GetString("comp-kitchen-spike-remove-meat-last", ("victim", victimUid));
component.Victim = Name(victimUid);
UpdateAppearance(uid, null, component);
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-kill",
("user", Identity.Entity(userUid, EntityManager)),
("victim", Identity.Entity(victimUid, EntityManager)),
("this", uid)),
uid, PopupType.LargeCaution);
_transform.SetCoordinates(victimUid, Transform(uid).Coordinates);
// THE WHAT?
// TODO: Need to be able to leave them on the spike to do DoT, see ss13.
var gibs = _bodySystem.GibBody(victimUid);
foreach (var gib in gibs) {
QueueDel(gib);
}
_audio.PlayPvs(component.SpikeSound, uid);
}
private bool TryGetPiece(EntityUid uid, EntityUid user, EntityUid used,
KitchenSpikeComponent? component = null, SharpComponent? sharp = null)
{
if (!Resolve(uid, ref component) || component.PrototypesToSpawn == null || component.PrototypesToSpawn.Count == 0)
return false;
// Is using knife
if (!Resolve(used, ref sharp, false) )
{
return false;
}
var item = _random.PickAndTake(component.PrototypesToSpawn);
var ent = Spawn(item, Transform(uid).Coordinates);
_metaData.SetEntityName(ent,
Loc.GetString("comp-kitchen-spike-meat-name", ("name", Name(ent)), ("victim", component.Victim)));
if (component.PrototypesToSpawn.Count != 0)
_popupSystem.PopupEntity(component.MeatSource1p, uid, user, PopupType.MediumCaution);
else
{
UpdateAppearance(uid, null, component);
_popupSystem.PopupEntity(component.MeatSource0, uid, user, PopupType.MediumCaution);
}
return true;
}
private void UpdateAppearance(EntityUid uid, AppearanceComponent? appearance = null, KitchenSpikeComponent? component = null)
{
if (!Resolve(uid, ref component, ref appearance, false))
return;
_appearance.SetData(uid, KitchenSpikeVisuals.Status, component.PrototypesToSpawn?.Count > 0 ? KitchenSpikeStatus.Bloody : KitchenSpikeStatus.Empty, appearance);
}
private bool Spikeable(EntityUid uid, EntityUid userUid, EntityUid victimUid,
KitchenSpikeComponent? component = null, ButcherableComponent? butcherable = null)
{
if (!Resolve(uid, ref component))
return false;
if (component.PrototypesToSpawn?.Count > 0)
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-collect", ("this", uid)), uid, userUid);
return false;
}
if (!Resolve(victimUid, ref butcherable, false))
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-butcher", ("victim", Identity.Entity(victimUid, EntityManager)), ("this", uid)), victimUid, userUid);
return false;
}
switch (butcherable.Type)
{
case ButcheringType.Spike:
return true;
case ButcheringType.Knife:
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-butcher-knife", ("victim", Identity.Entity(victimUid, EntityManager)), ("this", uid)), victimUid, userUid);
return false;
default:
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-butcher", ("victim", Identity.Entity(victimUid, EntityManager)), ("this", uid)), victimUid, userUid);
return false;
}
}
public bool TrySpike(EntityUid uid, EntityUid userUid, EntityUid victimUid, KitchenSpikeComponent? component = null,
ButcherableComponent? butcherable = null, MobStateComponent? mobState = null)
{
if (!Resolve(uid, ref component) || component.InUse ||
!Resolve(victimUid, ref butcherable) || butcherable.BeingButchered)
return false;
// THE WHAT? (again)
// Prevent dead from being spiked TODO: Maybe remove when rounds can be played and DOT is implemented
if (Resolve(victimUid, ref mobState, false) &&
_mobStateSystem.IsAlive(victimUid, mobState))
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-deny-not-dead", ("victim", Identity.Entity(victimUid, EntityManager))),
victimUid, userUid);
return true;
}
if (userUid != victimUid)
{
_popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-begin-hook-victim", ("user", Identity.Entity(userUid, EntityManager)), ("this", uid)), victimUid, victimUid, PopupType.LargeCaution);
}
// TODO: make it work when SuicideEvent is implemented
// else
// _popupSystem.PopupEntity(Loc.GetString("comp-kitchen-spike-begin-hook-self", ("this", uid)), victimUid, Filter.Pvs(uid)); // This is actually unreachable and should be in SuicideEvent
butcherable.BeingButchered = true;
component.InUse = true;
var doAfterArgs = new DoAfterArgs(EntityManager, userUid, component.SpikeDelay + butcherable.ButcherDelay, new SpikeDoAfterEvent(), uid, target: victimUid, used: uid)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
BreakOnDropItem = false,
};
_doAfter.TryStartDoAfter(doAfterArgs);
return true;
}
}
}

View file

@ -1,5 +1,4 @@
using Content.Server.Body.Systems;
using Content.Server.Kitchen.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;
@ -8,6 +7,7 @@ using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;

Some files were not shown because too many files have changed in this diff Show more