Merge remote-tracking branch 'sunrise'
This commit is contained in:
commit
c5fca9e384
715 changed files with 17289 additions and 5984 deletions
14
.github/copilot-instructions.md
vendored
14
.github/copilot-instructions.md
vendored
|
|
@ -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
|
||||
|
|
|
|||
84
.github/scripts/README.md
vendored
84
.github/scripts/README.md
vendored
|
|
@ -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.
|
||||
512
.github/scripts/xaml-preview.py
vendored
512
.github/scripts/xaml-preview.py
vendored
|
|
@ -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()
|
||||
279
.github/scripts/xaml_mockup_generator.py
vendored
279
.github/scripts/xaml_mockup_generator.py
vendored
|
|
@ -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()
|
||||
112
.github/workflows/xaml-preview.yml
vendored
112
.github/workflows/xaml-preview.yml
vendored
|
|
@ -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
3
.gitignore
vendored
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}"/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
32
Content.Client/CartridgeLoader/Cartridges/NavigatorUi.cs
Normal file
32
Content.Client/CartridgeLoader/Cartridges/NavigatorUi.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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);
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
5
Content.Client/Implants/SubdermalImplantSystem.cs
Normal file
5
Content.Client/Implants/SubdermalImplantSystem.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Implants;
|
||||
|
||||
namespace Content.Client.Implants;
|
||||
|
||||
public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem;
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
using Content.Shared.Kitchen;
|
||||
|
||||
namespace Content.Client.Kitchen;
|
||||
|
||||
public sealed class KitchenSpikeSystem : SharedKitchenSpikeSystem
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
35
Content.Client/Lathe/UI/QueuedRecipeControl.xaml
Normal file
35
Content.Client/Lathe/UI/QueuedRecipeControl.xaml
Normal 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>
|
||||
36
Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
Normal file
36
Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
5
Content.Client/Morgue/CrematoriumSystem.cs
Normal file
5
Content.Client/Morgue/CrematoriumSystem.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Morgue;
|
||||
|
||||
namespace Content.Client.Morgue;
|
||||
|
||||
public sealed class CrematoriumSystem : SharedCrematoriumSystem;
|
||||
5
Content.Client/Morgue/MorgueSystem.cs
Normal file
5
Content.Client/Morgue/MorgueSystem.cs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Morgue;
|
||||
|
||||
namespace Content.Client.Morgue;
|
||||
|
||||
public sealed class MorgueSystem : SharedMorgueSystem;
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'}" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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'}" />
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ public partial class NavMapControl : MapGridControl
|
|||
},
|
||||
VerticalExpand = false,
|
||||
HorizontalExpand = true,
|
||||
SetWidth = 650f,
|
||||
//SetWidth = 650f, Sunrise-Edit
|
||||
Children =
|
||||
{
|
||||
new BoxContainer()
|
||||
|
|
|
|||
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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!");
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ namespace Content.IntegrationTests.Tests.Doors
|
|||
components:
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
- type: GravityAffected
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
fix1:
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ namespace Content.IntegrationTests.Tests.Gravity
|
|||
- type: Alerts
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
- type: GravityAffected
|
||||
|
||||
- type: entity
|
||||
name: WeightlessGravityGeneratorDummy
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ public abstract partial class InteractionTest
|
|||
- type: Stripping
|
||||
- type: Puller
|
||||
- type: Physics
|
||||
- type: GravityAffected
|
||||
- type: Tag
|
||||
tags:
|
||||
- CanPilot
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
2125
Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs
generated
Normal file
2125
Content.Server.Database/Migrations/Postgres/20250723055137_AdminLogsCurtime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
2048
Content.Server.Database/Migrations/Sqlite/20250723055127_AdminLogsCurtime.Designer.cs
generated
Normal file
2048
Content.Server.Database/Migrations/Sqlite/20250723055127_AdminLogsCurtime.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
namespace Content.Server.CartridgeLoader.Cartridges;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class NavigatorCartridgeComponent : Component
|
||||
{
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
using Content.Shared.Changeling.Systems;
|
||||
|
||||
namespace Content.Server.Changeling.Systems;
|
||||
|
||||
public sealed class ChangelingIdentitySystem : SharedChangelingIdentitySystem;
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>))]
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue