Kaynağa Gözat

Squashed history before ccc8837b69

Create README.md

Import engine as submodule (#1)
Silver 9 yıl önce
işleme
c1bd8faf8e
100 değiştirilmiş dosya ile 6926 ekleme ve 0 silme
  1. 358 0
      .editorconfig
  2. 5 0
      .envrc
  3. 64 0
      .gitattributes
  4. 45 0
      .github/CODEOWNERS
  5. 7 0
      .github/ISSUE_TEMPLATE/config.yml
  6. 20 0
      .github/ISSUE_TEMPLATE/issue_report.md
  7. 18 0
      .github/ISSUE_TEMPLATE/toolshed-feature-request.md
  8. 36 0
      .github/PULL_REQUEST_TEMPLATE.md
  9. 9 0
      .github/config.yml
  10. 26 0
      .github/labeler.yml
  11. 185 0
      .github/rsi-schema.json
  12. 47 0
      .github/workflows/benchmarks.yml
  13. 41 0
      .github/workflows/build-docfx.yml
  14. 57 0
      .github/workflows/build-map-renderer.yml
  15. 62 0
      .github/workflows/build-test-debug.yml
  16. 15 0
      .github/workflows/check-crlf.yml
  17. 27 0
      .github/workflows/close-master-pr.yml
  18. 21 0
      .github/workflows/labeler-conflict.yml
  19. 16 0
      .github/workflows/labeler-needsreview.yml
  20. 14 0
      .github/workflows/labeler-pr.yml
  21. 23 0
      .github/workflows/labeler-review.yml
  22. 20 0
      .github/workflows/labeler-size.yml
  23. 16 0
      .github/workflows/labeler-stable.yml
  24. 16 0
      .github/workflows/labeler-staging.yml
  25. 16 0
      .github/workflows/labeler-untriaged.yml
  26. 14 0
      .github/workflows/no-submodule-update.yml
  27. 45 0
      .github/workflows/publish-testing.yml
  28. 59 0
      .github/workflows/publish.yml
  29. 69 0
      .github/workflows/rsi-diff.yml
  30. 66 0
      .github/workflows/test-packaging.yml
  31. 54 0
      .github/workflows/update-credits.yml
  32. 25 0
      .github/workflows/validate-rgas.yml
  33. 26 0
      .github/workflows/validate-rsis.yml
  34. 25 0
      .github/workflows/validate_mapfiles.yml
  35. 35 0
      .github/workflows/yaml-linter.yml
  36. 308 0
      .gitignore
  37. 4 0
      .gitmodules
  38. 7 0
      .run/Content Server+Client.run.xml
  39. 6 0
      .vscode/extensions.json
  40. 55 0
      .vscode/launch.json
  41. 37 0
      .vscode/tasks.json
  42. 5 0
      BuildChecker/.gitignore
  43. 52 0
      BuildChecker/BuildChecker.csproj
  44. 110 0
      BuildChecker/git_helper.py
  45. 13 0
      BuildChecker/hooks/post-checkout
  46. 5 0
      BuildChecker/hooks/post-merge
  47. 20 0
      BuildFiles/Mac/Space Station 14.app/Contents/Info.plist
  48. 8 0
      BuildFiles/Mac/Space Station 14.app/Contents/MacOS/SS14
  49. BIN
      BuildFiles/Mac/Space Station 14.app/Contents/Resources/ss14.icns
  50. 168 0
      Content.Benchmarks/ColorInterpolateBenchmark.cs
  51. 259 0
      Content.Benchmarks/ComponentFetchBenchmark.cs
  52. 273 0
      Content.Benchmarks/ComponentQueryBenchmark.cs
  53. 28 0
      Content.Benchmarks/Content.Benchmarks.csproj
  54. 70 0
      Content.Benchmarks/DependencyInjectBenchmark.cs
  55. 142 0
      Content.Benchmarks/DeviceNetworkingBenchmark.cs
  56. 68 0
      Content.Benchmarks/DynamicTreeBenchmark.cs
  57. 320 0
      Content.Benchmarks/EntityFetchBenchmark.cs
  58. 96 0
      Content.Benchmarks/EntityManagerGetAllComponents.cs
  59. 79 0
      Content.Benchmarks/MapLoadBenchmark.cs
  60. 265 0
      Content.Benchmarks/NetSerializerIntBenchmark.cs
  61. 431 0
      Content.Benchmarks/NetSerializerStringBenchmark.cs
  62. 32 0
      Content.Benchmarks/Program.cs
  63. 177 0
      Content.Benchmarks/PvsBenchmark.cs
  64. 66 0
      Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
  65. 72 0
      Content.Benchmarks/StereoToMonoBenchmark.cs
  66. 94 0
      Content.Client/Access/AccessOverlay.cs
  67. 11 0
      Content.Client/Access/AccessOverriderSystem.cs
  68. 7 0
      Content.Client/Access/AccessSystem.cs
  69. 42 0
      Content.Client/Access/Commands/ShowAccessReadersCommand.cs
  70. 13 0
      Content.Client/Access/IdCardConsoleSystem.cs
  71. 5 0
      Content.Client/Access/IdCardSystem.cs
  72. 4 0
      Content.Client/Access/UI/AccessLevelControl.xaml
  73. 59 0
      Content.Client/Access/UI/AccessLevelControl.xaml.cs
  74. 77 0
      Content.Client/Access/UI/AccessOverriderBoundUserInterface.cs
  75. 23 0
      Content.Client/Access/UI/AccessOverriderWindow.xaml
  76. 98 0
      Content.Client/Access/UI/AccessOverriderWindow.xaml.cs
  77. 61 0
      Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
  78. 14 0
      Content.Client/Access/UI/AgentIDCardWindow.xaml
  79. 96 0
      Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
  80. 82 0
      Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs
  81. 35 0
      Content.Client/Access/UI/IdCardConsoleWindow.xaml
  82. 205 0
      Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs
  83. 9 0
      Content.Client/Actions/ActionEvents.cs
  84. 356 0
      Content.Client/Actions/ActionsSystem.cs
  85. 116 0
      Content.Client/Actions/UI/ActionAlertTooltip.cs
  86. 121 0
      Content.Client/Administration/AdminNameOverlay.cs
  87. 10 0
      Content.Client/Administration/Components/HeadstandComponent.cs
  88. 7 0
      Content.Client/Administration/Components/KillSignComponent.cs
  89. 151 0
      Content.Client/Administration/Managers/ClientAdminManager.cs
  90. 76 0
      Content.Client/Administration/Managers/IClientAdminManager.cs
  91. 37 0
      Content.Client/Administration/QuickDialogSystem.cs
  92. 7 0
      Content.Client/Administration/Systems/AdminFrozenSystem.cs
  93. 52 0
      Content.Client/Administration/Systems/AdminSystem.Overlay.cs
  94. 55 0
      Content.Client/Administration/Systems/AdminSystem.cs
  95. 61 0
      Content.Client/Administration/Systems/AdminVerbSystem.cs
  96. 42 0
      Content.Client/Administration/Systems/BwoinkSystem.cs
  97. 35 0
      Content.Client/Administration/Systems/HeadstandSystem.cs
  98. 48 0
      Content.Client/Administration/Systems/KillSignSystem.cs
  99. 18 0
      Content.Client/Administration/UI/AdminAnnounceWindow.xaml
  100. 41 0
      Content.Client/Administration/UI/AdminAnnounceWindow.xaml.cs

+ 358 - 0
.editorconfig

@@ -0,0 +1,358 @@
+root = true
+
+[*]
+
+charset = utf-8
+
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
+
+# New line preferences
+#end_of_line = crlf
+insert_final_newline = true
+trim_trailing_whitespace = true
+max_line_length = 120
+
+#### .NET Coding Conventions ####
+
+# Organize usings
+#dotnet_separate_import_directive_groups = false
+#dotnet_sort_system_directives_first = true
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false:suggestion
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:suggestion
+dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:suggestion
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+
+# Expression-level preferences
+#dotnet_style_coalesce_expression = true:suggestion
+#dotnet_style_collection_initializer = true:suggestion
+#dotnet_style_explicit_tuple_names = true:suggestion
+#dotnet_style_null_propagation = true:suggestion
+#dotnet_style_object_initializer = true:suggestion
+#dotnet_style_prefer_auto_properties = true:silent
+#dotnet_style_prefer_compound_assignment = true:suggestion
+#dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+#dotnet_style_prefer_conditional_expression_over_return = true:silent
+#dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+#dotnet_style_prefer_inferred_tuple_names = true:suggestion
+#dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+#dotnet_style_prefer_simplified_interpolation = true:suggestion
+
+# Field preferences
+#dotnet_style_readonly_field = true:suggestion
+
+# Parameter preferences
+#dotnet_code_quality_unused_parameters = all:suggestion
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = true:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:suggestion
+csharp_style_expression_bodied_constructors = false:suggestion
+#csharp_style_expression_bodied_indexers = true:silent
+#csharp_style_expression_bodied_lambdas = true:silent
+#csharp_style_expression_bodied_local_functions = false:silent
+csharp_style_expression_bodied_methods = false:suggestion
+#csharp_style_expression_bodied_operators = false:silent
+csharp_style_expression_bodied_properties = true:suggestion
+
+# Pattern matching preferences
+#csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+#csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+#csharp_style_prefer_switch_expression = true:suggestion
+
+# Null-checking preferences
+#csharp_style_conditional_delegate_call = true:suggestion
+
+# Modifier preferences
+#csharp_prefer_static_local_function = true:suggestion
+csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
+
+# Code-block preferences
+#csharp_prefer_braces = true:silent
+#csharp_prefer_simple_using_statement = true:suggestion
+
+# Expression-level preferences
+#csharp_prefer_simple_default_expression = true:suggestion
+#csharp_style_deconstructed_variable_declaration = true:suggestion
+#csharp_style_inlined_variable_declaration = true:suggestion
+#csharp_style_pattern_local_over_anonymous_function = true:suggestion
+#csharp_style_prefer_index_operator = true:suggestion
+#csharp_style_prefer_range_operator = true:suggestion
+#csharp_style_throw_expression = true:suggestion
+#csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+#csharp_style_unused_value_expression_statement_preference = discard_variable:silent
+
+# 'using' directive preferences
+csharp_using_directive_placement = outside_namespace:silent
+
+#### C# Formatting Rules ####
+
+# New line preferences
+csharp_new_line_before_catch = true
+csharp_new_line_before_else = true
+csharp_new_line_before_finally = true
+#csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_before_members_in_object_initializers = false
+csharp_new_line_before_open_brace = all
+csharp_new_line_between_query_expression_clauses = true
+resharper_csharp_place_simple_embedded_statement_on_same_line = never
+resharper_csharp_keep_existing_embedded_arrangement = false
+
+# Indentation preferences
+#csharp_indent_block_contents = true
+csharp_indent_braces = false
+#csharp_indent_case_contents = true
+#csharp_indent_case_contents_when_block = true
+#csharp_indent_labels = one_less_than_current
+csharp_indent_switch_labels = true
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+#csharp_space_around_declaration_statements = false
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# Wrapping preferences
+csharp_preserve_single_line_blocks = true
+#csharp_preserve_single_line_statements = true
+
+#### Naming styles ####
+
+# Naming rules
+
+#dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+#dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+#dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
+
+#dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+#dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+#dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
+
+#dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+#dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+#dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
+
+# Symbol specifications
+
+#dotnet_naming_symbols.interface.applicable_kinds = interface
+#dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+#dotnet_naming_symbols.interface.required_modifiers =
+
+#dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+#dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+#dotnet_naming_symbols.types.required_modifiers =
+
+#dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+#dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+#dotnet_naming_symbols.non_field_members.required_modifiers =
+
+# Naming styles
+
+#dotnet_naming_style.pascal_case.required_prefix =
+#dotnet_naming_style.pascal_case.required_suffix =
+#dotnet_naming_style.pascal_case.word_separator =
+#dotnet_naming_style.pascal_case.capitalization = pascal_case
+
+#dotnet_naming_style.begins_with_i.required_prefix = I
+#dotnet_naming_style.begins_with_i.required_suffix =
+#dotnet_naming_style.begins_with_i.word_separator =
+#dotnet_naming_style.begins_with_i.capitalization = pascal_case
+
+dotnet_diagnostic.ide0055.severity = warning
+
+dotnet_naming_rule.constants_rule.severity = warning
+dotnet_naming_rule.constants_rule.style = upper_camel_case_style
+dotnet_naming_rule.constants_rule.symbols = constants_symbols
+
+dotnet_naming_rule.event_rule.severity = warning
+dotnet_naming_rule.event_rule.style = upper_camel_case_style
+dotnet_naming_rule.event_rule.symbols = event_symbols
+
+dotnet_naming_rule.interfaces_rule.severity = warning
+dotnet_naming_rule.interfaces_rule.style = i_upper_camel_case_style
+dotnet_naming_rule.interfaces_rule.symbols = interfaces_symbols
+
+dotnet_naming_rule.locals_rule.severity = warning
+dotnet_naming_rule.locals_rule.style = lower_camel_case_style_1
+dotnet_naming_rule.locals_rule.symbols = locals_symbols
+
+dotnet_naming_rule.local_constants_rule.severity = warning
+dotnet_naming_rule.local_constants_rule.style = lower_camel_case_style_1
+dotnet_naming_rule.local_constants_rule.symbols = local_constants_symbols
+
+dotnet_naming_rule.local_functions_rule.severity = warning
+dotnet_naming_rule.local_functions_rule.style = upper_camel_case_style
+dotnet_naming_rule.local_functions_rule.symbols = local_functions_symbols
+
+dotnet_naming_rule.method_rule.severity = warning
+dotnet_naming_rule.method_rule.style = upper_camel_case_style
+dotnet_naming_rule.method_rule.symbols = method_symbols
+
+dotnet_naming_rule.parameters_rule.severity = warning
+dotnet_naming_rule.parameters_rule.style = lower_camel_case_style_1
+dotnet_naming_rule.parameters_rule.symbols = parameters_symbols
+
+dotnet_naming_rule.private_constants_rule.severity = warning
+dotnet_naming_rule.private_constants_rule.style = upper_camel_case_style
+dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols
+
+dotnet_naming_rule.private_instance_fields_rule.severity = warning
+dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style
+dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols
+
+dotnet_naming_rule.private_static_fields_rule.severity = warning
+dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style
+dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols
+
+dotnet_naming_rule.private_static_readonly_rule.severity = warning
+dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style
+dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols
+
+dotnet_naming_rule.property_rule.severity = warning
+dotnet_naming_rule.property_rule.style = upper_camel_case_style
+dotnet_naming_rule.property_rule.symbols = property_symbols
+
+dotnet_naming_rule.public_fields_rule.severity = warning
+dotnet_naming_rule.public_fields_rule.style = upper_camel_case_style
+dotnet_naming_rule.public_fields_rule.symbols = public_fields_symbols
+
+dotnet_naming_rule.static_readonly_rule.severity = warning
+dotnet_naming_rule.static_readonly_rule.style = upper_camel_case_style
+dotnet_naming_rule.static_readonly_rule.symbols = static_readonly_symbols
+
+dotnet_naming_rule.types_and_namespaces_rule.severity = warning
+dotnet_naming_rule.types_and_namespaces_rule.style = upper_camel_case_style
+dotnet_naming_rule.types_and_namespaces_rule.symbols = types_and_namespaces_symbols
+
+dotnet_naming_rule.type_parameters_rule.severity = warning
+dotnet_naming_rule.type_parameters_rule.style = t_upper_camel_case_style
+dotnet_naming_rule.type_parameters_rule.symbols = type_parameters_symbols
+
+dotnet_naming_style.i_upper_camel_case_style.capitalization = pascal_case
+dotnet_naming_style.i_upper_camel_case_style.required_prefix = I
+
+dotnet_naming_style.lower_camel_case_style.capitalization = camel_case
+dotnet_naming_style.lower_camel_case_style.required_prefix = _
+dotnet_naming_style.lower_camel_case_style_1.capitalization = camel_case
+
+dotnet_naming_style.t_upper_camel_case_style.capitalization = pascal_case
+dotnet_naming_style.t_upper_camel_case_style.required_prefix = T
+dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case
+
+dotnet_naming_symbols.constants_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.constants_symbols.required_modifiers = const
+
+dotnet_naming_symbols.event_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.event_symbols.applicable_kinds = event
+
+dotnet_naming_symbols.interfaces_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.interfaces_symbols.applicable_kinds = interface
+
+dotnet_naming_symbols.locals_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.locals_symbols.applicable_kinds = local
+
+dotnet_naming_symbols.local_constants_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.local_constants_symbols.applicable_kinds = local
+dotnet_naming_symbols.local_constants_symbols.required_modifiers = const
+
+dotnet_naming_symbols.local_functions_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions_symbols.applicable_kinds = local_function
+
+dotnet_naming_symbols.method_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.method_symbols.applicable_kinds = method
+
+dotnet_naming_symbols.parameters_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.parameters_symbols.applicable_kinds = parameter
+
+dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_constants_symbols.required_modifiers = const
+
+dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field
+
+dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static
+
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly
+
+dotnet_naming_symbols.property_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.property_symbols.applicable_kinds = property
+
+dotnet_naming_symbols.public_fields_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.public_fields_symbols.applicable_kinds = field
+
+dotnet_naming_symbols.static_readonly_symbols.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.static_readonly_symbols.applicable_kinds = field
+dotnet_naming_symbols.static_readonly_symbols.required_modifiers = static, readonly
+
+dotnet_naming_symbols.types_and_namespaces_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.types_and_namespaces_symbols.applicable_kinds = namespace, class, struct, enum, delegate
+
+dotnet_naming_symbols.type_parameters_symbols.applicable_accessibilities = *
+dotnet_naming_symbols.type_parameters_symbols.applicable_kinds = type_parameter
+
+# ReSharper properties
+resharper_braces_for_ifelse = required_for_multiline
+resharper_csharp_wrap_arguments_style = chop_if_long
+resharper_csharp_wrap_parameters_style = chop_if_long
+resharper_keep_existing_attribute_arrangement = true
+resharper_wrap_chained_binary_patterns = chop_if_long
+resharper_wrap_chained_method_calls = chop_if_long
+resharper_csharp_trailing_comma_in_multiline_lists = true
+resharper_csharp_qualified_using_at_nested_scope = false
+resharper_csharp_prefer_qualified_reference = false
+resharper_csharp_allow_alias = false
+
+[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
+indent_size = 2
+
+[nuget.config]
+indent_size = 2
+
+[{*.yaml,*.yml}]
+ij_yaml_indent_sequence_value = false

+ 5 - 0
.envrc

@@ -0,0 +1,5 @@
+set -e
+if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
+    source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM="
+fi
+use flake

+ 64 - 0
.gitattributes

@@ -0,0 +1,64 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+*.cs     diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln       merge=binary
+#*.csproj    merge=binary
+#*.vbproj    merge=binary
+#*.vcxproj   merge=binary
+#*.vcproj    merge=binary
+#*.dbproj    merge=binary
+#*.fsproj    merge=binary
+#*.lsproj    merge=binary
+#*.wixproj   merge=binary
+#*.modelproj merge=binary
+#*.sqlproj   merge=binary
+#*.wwaproj   merge=binary
+Resources/Maps/**.yml merge=mapping-merge-driver
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg   binary
+#*.png   binary
+#*.gif   binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc   diff=astextplain
+#*.DOC   diff=astextplain
+#*.docx  diff=astextplain
+#*.DOCX  diff=astextplain
+#*.dot   diff=astextplain
+#*.DOT   diff=astextplain
+#*.pdf   diff=astextplain
+#*.PDF   diff=astextplain
+#*.rtf   diff=astextplain
+#*.RTF   diff=astextplain

+ 45 - 0
.github/CODEOWNERS

@@ -0,0 +1,45 @@
+# Last match in file takes precedence.
+
+# Sorting by path instead of by who added it one day :(
+# this isn't how codeowners rules work pls read the first comment instead of trying to force a sorting order
+
+/Resources/ConfigPresets/WizardsDen/  @nikthechampiongr @crazybrain23
+/Content.*/Administration/ @DrSmugleaf @nikthechampiongr @crazybrain23
+/Resources/ServerInfo/ @nikthechampiongr @crazybrain23
+/Resources/ServerInfo/Guidebook/ServerRules/ @nikthechampiongr @crazybrain23
+
+/Resources/Prototypes/Maps/** @Emisse
+
+/Resources/Prototypes/Body/ @DrSmugleaf # suffering
+/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
+/Resources/Prototypes/Entities/Mobs/Species/ @DrSmugleaf
+/Resources/Prototypes/Guidebook/rules.yml @nikthechampiongr @crazybrain23
+/Content.*/Body/ @DrSmugleaf
+/Content.YAMLLinter @DrSmugleaf
+/Content.Shared/Damage/ @DrSmugleaf
+
+/Content.*/Anomaly/ @TheShuEd
+/Resources/Prototypes/Entities/Structures/Specific/anomalies.yml @TheShuEd
+
+/Content.*/Forensics/ @ficcialfaint
+
+# SKREEEE
+/Content.*.Database/ @PJB3005 @DrSmugleaf
+/Content.Shared.Database/Log*.cs @PJB3005 @DrSmugleaf @nikthechampiongr @crazybrain23
+/Pow3r/ @PJB3005
+/Content.Server/Power/Pow3r/ @PJB3005
+
+# notafet
+/Content.*/Atmos/ @Partmedia
+/Content.*/Botany/ @Partmedia
+
+# Jezi
+/Content.*/Medical @Jezithyr
+/Content.*/Body @Jezithyr
+
+# Sloth
+/Content.*/Audio @metalgearsloth
+/Content.*/Movement @metalgearsloth
+/Content.*/NPC @metalgearsloth
+/Content.*/Shuttles @metalgearsloth
+/Content.*/Weapons @metalgearsloth

+ 7 - 0
.github/ISSUE_TEMPLATE/config.yml

@@ -0,0 +1,7 @@
+contact_links:
+  - name: Report a Security Vulnerability
+    url: https://github.com/space-wizards/space-station-14/blob/master/SECURITY.md
+    about: Please report security vulnerabilities privately so we can fix them before they are publicly disclosed.
+  - name: Request a Feature
+    url: https://discord.gg/rGvu9hKffJ
+    about: Submit feature requests on our Discord server (https://discord.gg/rGvu9hKffJ).

+ 20 - 0
.github/ISSUE_TEMPLATE/issue_report.md

@@ -0,0 +1,20 @@
+---
+name: Report an Issue
+about: "..."
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+## Description
+<!-- Explain your issue in detail. Issues without proper explanation are liable to be closed by maintainers. -->
+
+**Reproduction**
+<!-- Include the steps to reproduce if applicable. -->
+
+**Screenshots**
+<!-- If applicable, add screenshots to help explain your problem. -->
+
+**Additional context**
+<!-- Add any other context about the problem here. Anything you think is related to the issue. -->

+ 18 - 0
.github/ISSUE_TEMPLATE/toolshed-feature-request.md

@@ -0,0 +1,18 @@
+---
+name: Toolshed feature request
+about: Suggest a feature for Toolshed (for game admins/developers)
+title: "[TOOLSHED REQUEST]"
+labels: Toolshed
+assignees: moonheart08
+
+---
+
+**Is your feature request related to a problem/bug? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the command you'd like**
+A clear and concise description of what you want and what it should do.
+If you're a technical user (i.e. programmer) including type signatures is helpful.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.

+ 36 - 0
.github/PULL_REQUEST_TEMPLATE.md

@@ -0,0 +1,36 @@
+<!-- Guidelines: https://docs.spacestation14.io/en/getting-started/pr-guideline -->
+
+## About the PR
+<!-- What did you change? -->
+
+## Why / Balance
+<!-- Discuss how this would affect game balance or explain why it was changed. Link any relevant discussions or issues. -->
+
+## Technical details
+<!-- Summary of code changes for easier review. -->
+
+## Media
+<!-- Attach media if the PR makes ingame changes (clothing, items, features, etc). 
+Small fixes/refactors are exempt. Media may be used in SS14 progress reports with credit. -->
+
+## Requirements
+<!-- Confirm the following by placing an X in the brackets [X]: -->
+- [ ] I have read and am following the [Pull Request and Changelog Guidelines](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html).
+- [ ] I have added media to this PR or it does not require an ingame showcase.
+<!-- You should understand that not following the above may get your PR closed at maintainer’s discretion -->
+
+## Breaking changes
+<!-- List any breaking changes, including namespaces, public class/method/field changes, prototype renames; and provide instructions for fixing them.
+This will be posted in #codebase-changes. -->
+
+**Changelog**
+<!-- Add a Changelog entry to make players aware of new features or changes that could affect gameplay.
+Make sure to read the guidelines and take this Changelog template out of the comment block in order for it to show up.
+Changelog must have a :cl: symbol, so the bot recognizes the changes and adds them to the game's changelog. -->
+<!--
+:cl:
+- add: Added fun!
+- remove: Removed fun!
+- tweak: Changed fun!
+- fix: Fixed fun!
+-->

+ 9 - 0
.github/config.yml

@@ -0,0 +1,9 @@
+Project-Condor:
+  org: space-wizards
+  project: 2
+  inbox: Inbox
+  labels:
+    "W: In Progress": "In Progress"
+    "W: Discussion": "Design and Discussion"
+    "W: Backlog": "Backlog"
+    "W: Next": "Next"

+ 26 - 0
.github/labeler.yml

@@ -0,0 +1,26 @@
+"Changes: Sprites":
+- changed-files:
+  - any-glob-to-any-file: '**/*.rsi/*.png'
+
+"Changes: Map":
+- changed-files:
+  - any-glob-to-any-file:
+    - 'Resources/Maps/**/*.yml'
+    - 'Resources/Prototypes/Maps/**/*.yml'
+
+"Changes: UI":
+- changed-files:
+  - any-glob-to-any-file: '**/*.xaml*'
+
+"Changes: Shaders":
+- changed-files:
+  - any-glob-to-any-file: '**/*.swsl'
+
+"Changes: Audio":
+- changed-files:
+  - any-glob-to-any-file: '**/*.ogg'
+
+"Changes: No C#":
+- changed-files:
+  # Equiv to any-glob-to-all as long as this has one matcher. If ALL changed files are not C# files, then apply label.
+  - all-globs-to-all-files: "!**/*.cs"

+ 185 - 0
.github/rsi-schema.json

@@ -0,0 +1,185 @@
+{
+   "$schema":"http://json-schema.org/draft-07/schema",
+   "default":{
+      
+   },
+   "description":"JSON Schema for SS14 RSI validation.",
+   "examples":[
+      {
+         "version":1,
+         "license":"CC-BY-SA-3.0",
+         "copyright":"Taken from CODEBASE at COMMIT LINK",
+         "size":{
+            "x":32,
+            "y":32
+         },
+         "states":[
+            {
+               "name":"basic"
+            },
+            {
+               "name":"basic-directions",
+               "directions":4
+            },
+            {
+               "name":"basic-delays",
+               "delays":[
+                  [
+                     0.1,
+                     0.1
+                  ]
+               ]
+            },
+            {
+               "name":"basic-delays-directions",
+               "directions":4,
+               "delays":[
+                  [
+                     0.1,
+                     0.1
+                  ],
+                  [
+                     0.1,
+                     0.1
+                  ],
+                  [
+                     0.1,
+                     0.1
+                  ],
+                  [
+                     0.1,
+                     0.1
+                  ]
+               ]
+            }
+         ]
+      }
+   ],
+   "required":[
+      "version",
+      "license",
+      "copyright",
+      "size",
+      "states"
+   ],
+   "title":"RSI Schema",
+   "type":"object",
+   "properties":{
+      "version":{
+         "$id":"#/properties/version",
+         "default":"",
+         "description":"RSI version integer.",
+         "title":"The version schema",
+         "type":"integer"
+      },
+      "license":{
+         "$id":"#/properties/license",
+         "default":"",
+         "description":"The license for the associated icon states. Restricted to SS14-compatible asset licenses.",
+         "enum":[
+            "CC-BY-SA-3.0",
+            "CC-BY-SA-4.0",
+            "CC-BY-NC-3.0",
+            "CC-BY-NC-4.0",
+            "CC-BY-NC-SA-3.0",
+            "CC-BY-NC-SA-4.0",
+            "CC0-1.0"
+         ],
+         "examples":[
+            "CC-BY-SA-3.0"
+         ],
+         "title":"License",
+         "type":"string"
+      },
+      "copyright":{
+         "$id":"#/properties/copyright",
+         "type":"string",
+         "title":"Copyright Info",
+         "description":"The copyright holder. This is typically a link to the commit of the codebase that the icon is pulled from.",
+         "default":"",
+         "examples":[
+            "Taken from CODEBASE at COMMIT LINK"
+         ]
+      },
+      "size":{
+         "$id":"#/properties/size",
+         "default":{
+            
+         },
+         "description":"The dimensions of the sprites inside the RSI.  This is not the size of the PNG files that store the sprite sheet.",
+         "examples":[
+            {
+               "x":32,
+               "y":32
+            }
+         ],
+         "title":"Sprite Dimensions",
+         "required":[
+            "x",
+            "y"
+         ],
+         "type":"object",
+         "properties":{
+            "x":{
+               "$id":"#/properties/size/properties/x",
+               "type":"integer",
+               "default":32,
+               "examples":[
+                  32
+               ]
+            },
+            "y":{
+               "$id":"#/properties/size/properties/y",
+               "type":"integer",
+               "default":32,
+               "examples":[
+                  32
+               ]
+            }
+         },
+         "additionalProperties":true
+      },
+      "states":{
+         "$id":"#/properties/states",
+         "type":"array",
+         "title":"Icon States",
+         "description":"Metadata for icon states. Includes name, directions, delays, etc.",
+         "default":[
+            
+         ],
+         "examples":[
+            [
+               {
+                  "name":"basic"
+               },
+               {
+                  "name":"basic-directions",
+                  "directions":4
+               }
+            ]
+         ],
+         "additionalItems":true,
+         "items":{
+            "$id":"#/properties/states/items",
+            "type":"object",
+            "required":[
+               "name"
+            ],
+            "properties":{
+               "name":{
+                  "type":"string"
+               },
+               "directions":{
+                  "type":"integer",
+                  "enum":[
+                     1,
+                     4,
+                     8
+                  ]
+               }
+            }
+         }
+      }
+   },
+   "additionalProperties":true
+}

+ 47 - 0
.github/workflows/benchmarks.yml

@@ -0,0 +1,47 @@
+name: Benchmarks
+on:
+  workflow_dispatch:
+  schedule:
+    - cron: '0 8 * * *'
+
+concurrency: benchmarks
+
+jobs:
+  benchmark:
+    name: Run Benchmarks
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3.6.0
+      with:
+        submodules: 'recursive'
+    - name: Get Engine version
+      run: |
+        cd RobustToolbox
+        git fetch --depth=1
+        echo "::set-output name=out::$(git rev-parse HEAD)"
+      id: engine_version
+    - name: Run script on centcomm
+      uses: appleboy/ssh-action@master
+      with:
+        host: centcomm.spacestation14.io
+        username: robust-benchmark-runner
+        key: ${{ secrets.CENTCOMM_ROBUST_BENCHMARK_RUNNER_KEY }}
+        command_timeout: 100000m
+        script: |
+          mkdir benchmark_run_content_${{ github.sha }}
+          cd benchmark_run_content_${{ github.sha }}
+          git clone https://github.com/space-wizards/space-station-14.git repo_dir --recursive
+          cd repo_dir
+          git checkout ${{ github.sha }}
+          cd Content.Benchmarks
+          dotnet restore
+          export ROBUST_BENCHMARKS_ENABLE_SQL=1
+          export ROBUST_BENCHMARKS_SQL_ADDRESS="${{ secrets.BENCHMARKS_WRITE_ADDRESS }}"
+          export ROBUST_BENCHMARKS_SQL_PORT="${{ secrets.BENCHMARKS_WRITE_PORT }}"
+          export ROBUST_BENCHMARKS_SQL_USER="${{ secrets.BENCHMARKS_WRITE_USER }}"
+          export ROBUST_BENCHMARKS_SQL_PASSWORD="${{ secrets.BENCHMARKS_WRITE_PASSWORD  }}"
+          export ROBUST_BENCHMARKS_SQL_DATABASE="content_benchmarks"
+          export GITHUB_SHA="${{ github.sha }}"
+          dotnet run --filter '*' --configuration Release
+          cd ../../..
+          rm -rf benchmark_run_content_${{ github.sha }}

+ 41 - 0
.github/workflows/build-docfx.yml

@@ -0,0 +1,41 @@
+name: Build & Publish Docfx
+
+on:
+  schedule:
+    - cron: "0 0 * * 0"
+
+jobs:
+  docfx:
+   runs-on: ubuntu-latest
+   steps:
+    - uses: actions/checkout@v3.6.0
+    - name: Setup submodule
+      run: |
+        git submodule update --init --recursive
+    - name: Pull engine updates
+      uses: space-wizards/submodule-dependency@v0.1.5
+    - name: Update Engine Submodules
+      run: |
+        cd RobustToolbox/
+        git submodule update --init --recursive
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v3.2.0
+      with:
+        dotnet-version: 9.0.x
+
+    - name: Install dependencies
+      run: dotnet restore
+
+    - name: Build Project
+      run: dotnet build --no-restore /p:WarningsAsErrors=nullable
+
+    - name: Build DocFX
+      uses: nikeee/docfx-action@v1.0.0
+      with:
+        args: Content.Docfx/docfx.json
+
+    - name: Publish Docfx Documentation on GitHub Pages
+      uses: maxheld83/ghpages@master
+      env:
+        BUILD_DIR: Content.Docfx/_content-site
+        GH_PAT: ${{ secrets.GH_PAT }}

+ 57 - 0
.github/workflows/build-map-renderer.yml

@@ -0,0 +1,57 @@
+name: Build & Test Map Renderer
+
+on:
+  push:
+    branches: [ master, staging, stable ]
+  merge_group:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+    branches: [ master, staging, stable ]
+
+jobs:
+  build:
+    if: github.actor != 'PJBot' && github.event.pull_request.draft == false
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - name: Checkout Master
+        uses: actions/checkout@v3.6.0
+
+      - name: Setup Submodule
+        run: |
+          git submodule update --init --recursive
+
+      - name: Pull engine updates
+        uses: space-wizards/submodule-dependency@v0.1.5
+
+      - name: Update Engine Submodules
+        run: |
+          cd RobustToolbox/
+          git submodule update --init --recursive
+
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v3.2.0
+        with:
+          dotnet-version: 9.0.x
+
+      - name: Install dependencies
+        run: dotnet restore
+
+      - name: Build Project
+        run: dotnet build Content.MapRenderer --configuration Release --no-restore /p:WarningsAsErrors=nullable /m
+
+      - name: Run Map Renderer
+        run: dotnet run --project Content.MapRenderer Dev
+
+  ci-success:
+    name: Build & Test Debug
+    needs:
+      - build
+    runs-on: ubuntu-latest
+    steps:
+      - name: CI succeeded
+        run: exit 0

+ 62 - 0
.github/workflows/build-test-debug.yml

@@ -0,0 +1,62 @@
+name: Build & Test Debug
+
+on:
+  push:
+    branches: [ master, staging, stable ]
+  merge_group:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+    branches: [ master, staging, stable ]
+
+jobs:
+  build:
+    if: github.actor != 'PJBot' && github.event.pull_request.draft == false
+    strategy:
+      matrix:
+        os: [ubuntu-latest]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+    - name: Checkout Master
+      uses: actions/checkout@v3.6.0
+
+    - name: Setup Submodule
+      run: |
+        git submodule update --init --recursive
+
+    - name: Pull engine updates
+      uses: space-wizards/submodule-dependency@v0.1.5
+
+    - name: Update Engine Submodules
+      run: |
+        cd RobustToolbox/
+        git submodule update --init --recursive
+
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v3.2.0
+      with:
+        dotnet-version: 9.0.x
+
+    - name: Install dependencies
+      run: dotnet restore
+
+    - name: Build Project
+      run: dotnet build --configuration DebugOpt --no-restore /p:WarningsAsErrors=nullable /m
+
+    - name: Run Content.Tests
+      run: dotnet test --no-build --configuration DebugOpt Content.Tests/Content.Tests.csproj -- NUnit.ConsoleOut=0
+
+    - name: Run Content.IntegrationTests
+      shell: pwsh
+      run: |
+        $env:DOTNET_gcServer=1
+        dotnet test --no-build --configuration DebugOpt Content.IntegrationTests/Content.IntegrationTests.csproj -- NUnit.ConsoleOut=0 NUnit.MapWarningTo=Failed
+  ci-success:
+    name: Build & Test Debug
+    needs:
+      - build
+    runs-on: ubuntu-latest
+    steps:
+      - name: CI succeeded
+        run: exit 0

+ 15 - 0
.github/workflows/check-crlf.yml

@@ -0,0 +1,15 @@
+name: CRLF Check
+
+on:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+
+jobs:
+  build:
+    name: CRLF Check
+    if: github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3.6.0
+    - name: Check for CRLF
+      run: Tools/check_crlf.py

+ 27 - 0
.github/workflows/close-master-pr.yml

@@ -0,0 +1,27 @@
+name: Close PRs on master
+
+on:
+  pull_request_target:
+    types: [ opened, ready_for_review ]
+    
+jobs:
+  run:
+    runs-on: ubuntu-latest
+    if: ${{github.head_ref == 'master' || github.head_ref == 'main' || github.head_ref == 'develop'}}
+    
+    steps:    
+    - uses: superbrothers/close-pull-request@v3
+      with:
+        comment: "Thank you for contributing to the Space Station 14 repository. Unfortunately, it looks like you submitted your pull request from the master branch. We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n You can move your current work from the master branch to another branch by doing `git branch <branch_name` and resetting the master branch."
+
+    # If you prefer to just comment on the pr and not close it, uncomment the bellow and comment the above
+      
+    # - uses: actions/github-script@v7
+    #   with:
+    #     script: |
+    #       github.rest.issues.createComment({
+    #         issue_number: ${{ github.event.number }},
+    #         owner: context.repo.owner,
+    #         repo: context.repo.repo,
+    #         body: "Thank you for contributing to the Space Station 14 repository. Unfortunately, it looks like you submitted your pull request from the master branch. We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n You can move your current work from the master branch to another branch by doing `git branch <branch_name` and resetting the master branch. \n\n This pr won't be automatically closed. However, a maintainer may close it for this reason."
+    #       })

+ 21 - 0
.github/workflows/labeler-conflict.yml

@@ -0,0 +1,21 @@
+name: Check Merge Conflicts
+
+on:
+  pull_request_target:
+    types:
+      - opened
+      - synchronize
+      - reopened
+      - ready_for_review
+
+jobs:
+  Label:
+    if: ( github.event.pull_request.draft == false ) && ( github.actor != 'PJBot' )
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check for Merge Conflicts
+        uses: eps1lon/actions-label-merge-conflict@v3.0.0
+        with:
+          dirtyLabel: "S: Merge Conflict"
+          repoToken: "${{ secrets.GITHUB_TOKEN }}"
+          commentOnDirty: "This pull request has conflicts, please resolve those before we can evaluate the pull request."

+ 16 - 0
.github/workflows/labeler-needsreview.yml

@@ -0,0 +1,16 @@
+name: "Labels: Review"
+
+on:
+  pull_request_target:
+    types: [review_requested]
+
+jobs:
+  add_label:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions-ecosystem/action-add-labels@v1
+      with:
+        labels: "S: Needs Review"
+    - uses: actions-ecosystem/action-remove-labels@v1
+      with:
+        labels: "S: Awaiting Changes"

+ 14 - 0
.github/workflows/labeler-pr.yml

@@ -0,0 +1,14 @@
+name: "Labels: PR"
+
+on:
+- pull_request_target
+
+jobs:
+  labeler:
+    if: github.actor != 'PJBot'
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/labeler@v5

+ 23 - 0
.github/workflows/labeler-review.yml

@@ -0,0 +1,23 @@
+name: "Labels: Approved"
+on:
+  pull_request_review:
+    types: [submitted]
+jobs:
+  add_label:
+    # Change the repository name after you've made sure the team name is correct for your fork!
+    if: ${{ (github.repository == 'space-wizards/space-station-14') && (github.event.review.state == 'APPROVED') }}
+    permissions:
+      contents: read
+      pull-requests: write
+    runs-on: ubuntu-latest
+    steps:
+    - uses: tspascoal/get-user-teams-membership@v3
+      id: checkUserMember
+      with:
+        username: ${{ github.actor }}
+        team: "content-maintainers,junior-maintainers"
+        GITHUB_TOKEN: ${{ secrets.LABELER_PAT }}
+    - if: ${{ steps.checkUserMember.outputs.isTeamMember == 'true' }}
+      uses: actions-ecosystem/action-add-labels@v1
+      with:
+        labels: "S: Approved"

+ 20 - 0
.github/workflows/labeler-size.yml

@@ -0,0 +1,20 @@
+name: "Labels: Size"
+on: pull_request_target
+jobs:
+  size-label:
+    runs-on: ubuntu-latest
+    steps:
+      - name: size-label
+        uses: "pascalgn/size-label-action@v0.5.5"
+        env:
+          GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+        with:
+          # Custom size configuration
+          sizes: >
+            {
+              "0": "XS",
+              "10": "S",
+              "100": "M",
+              "1000": "L",
+              "5000": "XL"
+            }

+ 16 - 0
.github/workflows/labeler-stable.yml

@@ -0,0 +1,16 @@
+name: "Labels: Branch stable"
+
+on:
+  pull_request_target:
+    types:
+      - opened
+    branches:
+      - 'stable'
+
+jobs:
+  add_label:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions-ecosystem/action-add-labels@v1
+      with:
+        labels: "Branch: Stable"

+ 16 - 0
.github/workflows/labeler-staging.yml

@@ -0,0 +1,16 @@
+name: "Labels: Branch staging"
+
+on:
+  pull_request_target:
+    types:
+      - opened
+    branches:
+      - 'staging'
+
+jobs:
+  add_label:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions-ecosystem/action-add-labels@v1
+      with:
+        labels: "Branch: Staging"

+ 16 - 0
.github/workflows/labeler-untriaged.yml

@@ -0,0 +1,16 @@
+name: "Labels: Untriaged"
+
+on:
+  issues:
+    types: [opened]
+  pull_request_target:
+    types: [opened]
+
+jobs:
+  add_label:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions-ecosystem/action-add-labels@v1
+      if: join(github.event.issue.labels) == ''
+      with:
+        labels: "S: Untriaged"

+ 14 - 0
.github/workflows/no-submodule-update.yml

@@ -0,0 +1,14 @@
+name: No submodule update checker
+
+on:
+  pull_request:
+    paths:
+      - 'RobustToolbox'
+
+jobs:
+  this_aint_right:
+    name: Submodule update in pr found
+    runs-on: ubuntu-latest
+    steps:
+      - name: Fail
+        run: exit 1

+ 45 - 0
.github/workflows/publish-testing.yml

@@ -0,0 +1,45 @@
+name: Publish Testing
+
+concurrency:
+  group: publish-testing
+
+on:
+  workflow_dispatch:
+  schedule:
+  - cron: '0 10 * * *'
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v3.6.0
+      with:
+        submodules: 'recursive'
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v3.2.0
+      with:
+        dotnet-version: 9.0.x
+
+    - name: Get Engine Tag
+      run: |
+        cd RobustToolbox
+        git fetch --depth=1
+
+    - name: Install dependencies
+      run: dotnet restore
+
+    - name: Build Packaging
+      run: dotnet build Content.Packaging --configuration Release --no-restore /m
+
+    - name: Package server
+      run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+
+    - name: Package client
+      run: dotnet run --project Content.Packaging client --no-wipe-release
+
+    - name: Publish version
+      run: Tools/publish_multi_request.py --fork-id wizards-testing
+      env:
+        PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+        GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}

+ 59 - 0
.github/workflows/publish.yml

@@ -0,0 +1,59 @@
+name: Publish
+
+concurrency:
+  group: publish
+
+on:
+  workflow_dispatch:
+  # schedule:
+   # - cron: '0 10 * * *'
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+
+    steps:
+    - name: Install dependencies
+      run: sudo apt-get install -y python3-paramiko python3-lxml
+
+    - uses: actions/checkout@v3.6.0
+      with:
+        submodules: 'recursive'
+    - name: Setup .NET Core
+      uses: actions/setup-dotnet@v3.2.0
+      with:
+        dotnet-version: 9.0.x
+
+    - name: Get Engine Tag
+      run: |
+        cd RobustToolbox
+        git fetch --depth=1
+
+    - name: Install dependencies
+      run: dotnet restore
+
+    - name: Build Packaging
+      run: dotnet build Content.Packaging --configuration Release --no-restore /m
+
+    - name: Package server
+      run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+
+    - name: Package client
+      run: dotnet run --project Content.Packaging client --no-wipe-release
+
+    - name: Publish version
+      run: Tools/publish_multi_request.py
+      env:
+        PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+        GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
+
+    - name: Publish changelog (Discord)
+      run: Tools/actions_changelogs_since_last_run.py
+      env:
+        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
+
+    - name: Publish changelog (RSS)
+      run: Tools/actions_changelog_rss.py
+      env:
+        CHANGELOG_RSS_KEY: ${{ secrets.CHANGELOG_RSS_KEY }}

+ 69 - 0
.github/workflows/rsi-diff.yml

@@ -0,0 +1,69 @@
+name: Diff RSIs
+
+on:
+  pull_request_target:
+    paths:
+      - '**.rsi/**.png'
+
+jobs:
+  diff:
+    name: Diff
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v3.6.0
+
+      - name: Get changed files
+        id: files
+        uses: Ana06/get-changed-files@v2.3.0
+        with:
+          format: 'space-delimited'
+          filter: | 
+            **.rsi
+            **.png
+
+      - name: Diff changed RSIs
+        id: diff
+        uses: space-wizards/RSIDiffBot@v1.1
+        with:
+          modified: ${{ steps.files.outputs.modified }}
+          removed: ${{ steps.files.outputs.removed }}
+          added: ${{ steps.files.outputs.added }}
+          basename: ${{ github.event.pull_request.base.repo.full_name }}
+          basesha: ${{ github.event.pull_request.base.sha }}
+          headname: ${{ github.event.pull_request.head.repo.full_name }}
+          headsha: ${{ github.event.pull_request.head.sha }}
+
+      - name: Potentially find comment
+        uses: peter-evans/find-comment@v1
+        id: fc
+        with:
+          issue-number: ${{ github.event.number }}
+          comment-author: 'github-actions[bot]'
+          body-includes: RSI Diff Bot
+
+      - name: Create comment if it doesn't exist
+        if: steps.fc.outputs.comment-id == ''
+        uses: peter-evans/create-or-update-comment@v1
+        with:
+          issue-number: ${{ github.event.number }}
+          body: |
+            ${{ steps.diff.outputs.summary-details }}
+
+      - name: Update comment if it exists
+        if: steps.fc.outputs.comment-id != ''
+        uses: peter-evans/create-or-update-comment@v1
+        with:
+          comment-id: ${{ steps.fc.outputs.comment-id }}
+          edit-mode: replace
+          body: |
+            ${{ steps.diff.outputs.summary-details }}
+
+      - name: Update comment to read that it has been edited
+        if: steps.fc.outputs.comment-id != ''
+        uses: peter-evans/create-or-update-comment@v1
+        with:
+          comment-id: ${{ steps.fc.outputs.comment-id }}
+          edit-mode: append
+          body: |
+            Edit: diff updated after ${{ github.event.pull_request.head.sha }}

+ 66 - 0
.github/workflows/test-packaging.yml

@@ -0,0 +1,66 @@
+name: Test Packaging
+
+on:
+  push:
+    branches: [ master, staging, stable ]
+    paths:
+      - '**.cs'
+      - '**.csproj'
+      - '**.sln'
+      - '**.git**'
+      - '**.yml'
+      # no docs on which one of these is supposed to work, so
+      # why not just do both
+      - 'RobustToolbox'
+      - 'RobustToolbox/**'
+  merge_group:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+    branches: [ master, staging, stable ]
+    paths:
+      - '**.cs'
+      - '**.csproj'
+      - '**.sln'
+      - '**.git**'
+      - '**.yml'
+      - 'RobustToolbox'
+      - 'RobustToolbox/**'
+
+jobs:
+  build:
+    name: Test Packaging
+    if: github.actor != 'PJBot' && github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout Master
+        uses: actions/checkout@v3.6.0
+
+      - name: Setup Submodule
+        run: |
+          git submodule update --init --recursive
+
+      - name: Pull engine updates
+        uses: space-wizards/submodule-dependency@v0.1.5
+
+      - name: Update Engine Submodules
+        run: |
+          cd RobustToolbox/
+          git submodule update --init --recursive
+
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v3.2.0
+        with:
+          dotnet-version: 9.0.x
+
+      - name: Install dependencies
+        run: dotnet restore
+
+      - name: Build Packaging
+        run: dotnet build Content.Packaging --configuration Release --no-restore /m
+
+      - name: Package server
+        run: dotnet run --project Content.Packaging server --platform win-x64 --platform linux-x64 --platform osx-x64 --platform linux-arm64
+
+      - name: Package client
+        run: dotnet run --project Content.Packaging client --no-wipe-release

+ 54 - 0
.github/workflows/update-credits.yml

@@ -0,0 +1,54 @@
+name: Update Contrib and Patreons in credits
+
+on:
+  workflow_dispatch:
+  schedule:
+    - cron: 0 0 * * 0
+    
+jobs:
+  get_credits:
+    runs-on: ubuntu-latest
+    # Hey there fork dev! If you like to include your own contributors in this then you can probably just change this to your own repo
+    # Do this in dump_github_contributors.ps1 too into your own repo
+    if: github.repository == 'space-wizards/space-station-14'
+    
+    steps:
+      - uses: actions/checkout@v3.6.0
+        with:
+          ref: master
+        
+      - name: Get this week's Contributors
+        shell: pwsh
+        env:
+          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+        run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt
+
+      # TODO
+      #- name: Get this week's Patreons
+      #  run: Tools/script2dumppatreons > Resources/Credits/Patrons.yml        
+      
+      # MAKE SURE YOU ENABLED "Allow GitHub Actions to create and approve pull requests" IN YOUR ACTIONS, OTHERWISE IT WILL MOST LIKELY FAIL
+
+
+      # For this you can use a pat token of an account with direct push access to the repo if you have protected branches. 
+      # Uncomment this and comment the other line if you do this.
+      # https://github.com/stefanzweifel/git-auto-commit-action#push-to-protected-branches
+      
+      #- name: Commit new credit files
+      #  uses: stefanzweifel/git-auto-commit-action@v4
+      #  with:
+      #    commit_message: Update Credits
+      #    commit_author: PJBot <pieterjan.briers+bot@gmail.com>
+      
+      # This will make a PR
+      - name: Set current date as env variable
+        run: echo "NOW=$(date +'%Y-%m-%dT%H-%M-%S')" >> $GITHUB_ENV
+        
+      - name: Create Pull Request
+        uses: peter-evans/create-pull-request@v5
+        with:
+          commit-message: Update Credits
+          title: Update Credits
+          body: This is an automated Pull Request. This PR updates the github contributors in the credits section.
+          author: PJBot <pieterjan.briers+bot@gmail.com>
+          branch: automated/credits-${{env.NOW}}

+ 25 - 0
.github/workflows/validate-rgas.yml

@@ -0,0 +1,25 @@
+name: RGA schema validator
+on:
+  push:
+    branches: [ master, staging, stable ]
+  merge_group:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+
+jobs:
+  yaml-schema-validation:
+    name: YAML RGA schema validator
+    if: github.actor != 'PJBot' && github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3.6.0
+    - name: Setup Submodule
+      run: git submodule update --init
+    - name: Pull engine updates
+      uses: space-wizards/submodule-dependency@v0.1.5
+    - uses: PaulRitter/yaml-schema-validator@v1
+      with:
+        schema: RobustToolbox/Schemas/rga.yml
+        path_pattern: .*attributions.ya?ml$
+        validators_path: RobustToolbox/Schemas/rga_validators.py
+        validators_requirements: RobustToolbox/Schemas/rga_requirements.txt

+ 26 - 0
.github/workflows/validate-rsis.yml

@@ -0,0 +1,26 @@
+name: RSI Validator
+
+on:
+  push:
+    branches: [ master, staging, stable ]
+  merge_group:
+  pull_request:
+    paths:
+      - '**.rsi/**'
+
+jobs:
+  validate_rsis:
+    name: Validate RSIs
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3.6.0
+      - name: Setup Submodule
+        run: git submodule update --init
+      - name: Pull engine updates
+        uses: space-wizards/submodule-dependency@v0.1.5
+      - name: Install Python dependencies
+        run: |
+          pip3 install --ignore-installed --user pillow jsonschema
+      - name: Validate RSIs
+        run: |
+          python3 RobustToolbox/Schemas/validate_rsis.py Resources/

+ 25 - 0
.github/workflows/validate_mapfiles.yml

@@ -0,0 +1,25 @@
+name: Map file schema validator
+on:
+  push:
+    branches: [ master, staging, stable ]
+  merge_group:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+
+jobs:
+  yaml-schema-validation:
+    name: YAML map schema validator
+    if: github.actor != 'PJBot' && github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3.6.0
+    - name: Setup Submodule
+      run: git submodule update --init
+    - name: Pull engine updates
+      uses: space-wizards/submodule-dependency@v0.1.5
+    - uses: PaulRitter/yaml-schema-validator@v1
+      with:
+        schema: RobustToolbox/Schemas/mapfile.yml
+        path_pattern: .*Resources/Maps/.*
+        validators_path: RobustToolbox/Schemas/mapfile_validators.py
+        validators_requirements: RobustToolbox/Schemas/mapfile_requirements.txt

+ 35 - 0
.github/workflows/yaml-linter.yml

@@ -0,0 +1,35 @@
+name: YAML Linter
+
+on:
+  push:
+    branches: [ master, staging, stable ]
+  merge_group:
+  pull_request:
+    types: [ opened, reopened, synchronize, ready_for_review ]
+
+jobs:
+  build:
+    name: YAML Linter
+    if: github.actor != 'PJBot' && github.event.pull_request.draft == false
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3.6.0
+      - name: Setup submodule
+        run: |
+          git submodule update --init --recursive
+      - name: Pull engine updates
+        uses: space-wizards/submodule-dependency@v0.1.5
+      - name: Update Engine Submodules
+        run: |
+          cd RobustToolbox/
+          git submodule update --init --recursive
+      - name: Setup .NET Core
+        uses: actions/setup-dotnet@v3.2.0
+        with:
+          dotnet-version: 9.0.x
+      - name: Install dependencies
+        run: dotnet restore
+      - name: Build
+        run: dotnet build --configuration Release --no-restore /p:WarningsAsErrors= /m
+      - name: Run Linter
+        run: dotnet run --project Content.YAMLLinter/Content.YAMLLinter.csproj --no-build

+ 308 - 0
.gitignore

@@ -0,0 +1,308 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+.nuget/
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+.editorconfig
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Visual Studio Code workspace settings.
+.vscode/*
+!.vscode/extensions.json
+!.vscode/launch.json
+!.vscode/tasks.json
+
+# Release package files go here:
+release/
+
+# Apple please go.
+.DS_Store
+# KDE, come in.
+.directory
+
+BuildFiles/Mac/Space Station 14.app/Contents/MacOS/Godot
+BuildFiles/Mac/Space Station 14.app/Contents/MacOS/GodotSharpTools.dll
+BuildFiles/Mac/Space Station 14.app/Contents/MacOS/mscorlib.dll
+BuildFiles/Mac/Space Station 14.app/Contents/MacOS/libmonosgen-2.0.dylib
+BuildFiles/Windows/Godot/*
+
+# Working on the tools scripts is getting annoying okay?
+.mypy_cache/
+
+# Windows image file caches
+Thumbs.db
+ehthumbs.db
+
+# Merge driver stuff
+Content.Tools/test/out.yml
+
+# Windows
+desktop.ini
+
+# Images generated using the MapRenderer
+Resources/MapImages
+
+## Docfx stuff
+/Content.Docfx/api/
+/Content.Docfx/*site
+
+*.bak
+
+# Direnv stuff
+.direnv/

+ 4 - 0
.gitmodules

@@ -0,0 +1,4 @@
+[submodule "RobustToolbox"]
+	path = RobustToolbox
+	url = https://github.com/space-wizards/RobustToolbox.git
+	branch = master

+ 7 - 0
.run/Content Server+Client.run.xml

@@ -0,0 +1,7 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="Content Server+Client" type="CompoundRunConfigurationType">
+    <toRun name="Content.Client" type="DotNetProject" />
+    <toRun name="Content.Server" type="DotNetProject" />
+    <method v="2" />
+  </configuration>
+</component>

+ 6 - 0
.vscode/extensions.json

@@ -0,0 +1,6 @@
+{
+    "recommendations": [
+        "ms-dotnettools.csharp",
+        "editorconfig.editorconfig"
+    ]
+}

+ 55 - 0
.vscode/launch.json

@@ -0,0 +1,55 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "name": "Client",
+            "type": "coreclr",
+            "request": "launch",
+            "program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
+            "args": [],
+            "console": "internalConsole",
+            "stopAtEntry": false
+        },
+        {
+            "name": "Client (Compatibility renderer)",
+            "type": "coreclr",
+            "request": "launch",
+            "program": "${workspaceFolder}/bin/Content.Client/Content.Client.dll",
+            "args": "--cvar display.compat=true",
+            "console": "internalConsole",
+            "stopAtEntry": false
+        },
+        {
+            "name": "Server",
+            "type": "coreclr",
+            "request": "launch",
+            "program": "${workspaceFolder}/bin/Content.Server/Content.Server.dll",
+            "args": [],
+            "console": "integratedTerminal",
+            "stopAtEntry": false
+        },
+        {
+            "name": "YAML Linter",
+            "type": "coreclr",
+            "request": "launch",
+            "preLaunchTask": "build-yaml-linter",
+            "program": "${workspaceFolder}/bin/Content.YAMLLinter/Content.YAMLLinter.dll",
+            "cwd": "${workspaceFolder}/Content.YAMLLinter",
+            "console": "internalConsole",
+            "stopAtEntry": false
+        }
+    ],
+    "compounds": [
+        {
+            "name": "Server/Client",
+            "configurations": [
+                "Server",
+                "Client"
+            ],
+            "preLaunchTask": "build"
+        }
+    ]
+}

+ 37 - 0
.vscode/tasks.json

@@ -0,0 +1,37 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "label": "build",
+            "command": "dotnet",
+            "type": "shell",
+            "args": [
+                "build",
+                "/property:GenerateFullPaths=true", // Ask dotnet build to generate full paths for file names.
+                "/consoleloggerparameters:'ForceNoAlign;NoSummary'" // Do not generate summary otherwise it leads to duplicate errors in Problems panel
+            ],
+            "group": {
+                "kind": "build",
+                "isDefault": true
+            },
+            "presentation": {
+                "reveal": "silent"
+            },
+            "problemMatcher": "$msCompile"
+        },
+        {
+            "label": "build-yaml-linter",
+            "command": "dotnet",
+            "type": "process",
+            "args": [
+                "build",
+                "${workspaceFolder}/Content.YAMLLinter/Content.YAMLLinter.csproj",
+                "/property:GenerateFullPaths=true",
+                "/consoleloggerparameters:'ForceNoAlign;NoSummary'"
+            ],
+            "problemMatcher": "$msCompile"
+        }
+    ]
+}

+ 5 - 0
BuildChecker/.gitignore

@@ -0,0 +1,5 @@
+INSTALLED_HOOKS_VERSION
+DISABLE_SUBMODULE_AUTOUPDATE
+*.nuget*
+project.assets.json
+project.packagespec.json

+ 52 - 0
BuildChecker/BuildChecker.csproj

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+This is a dummy .csproj file to check things like submodules.
+Better this than other errors.
+
+If you want to create this kind of file yourself, you have to create an empty .NET application,
+Then strip it of everything until you have the <Project> tags.
+VS refuses to load the project if you make a bare project file and use Add -> Existing Project... for some reason.
+
+You want to handle the Build, Clean and Rebuild tasks to prevent missing task errors on build.
+
+If you want to learn more about these kinds of things, check out Microsoft's official documentation about MSBuild:
+https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild
+-->
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <Python>python3</Python>
+    <Python Condition="'$(OS)'=='Windows_NT' Or '$(OS)'=='Windows'">py -3</Python>
+    <ProjectGuid>{C899FCA4-7037-4E49-ABC2-44DE72487110}</ProjectGuid>
+    <TargetFramework>net4.7.2</TargetFramework>
+    <RestorePackages>false</RestorePackages>
+  </PropertyGroup>
+  <PropertyGroup>
+    <OutputType>Library</OutputType>
+  </PropertyGroup>
+  <PropertyGroup>
+    <StartupObject />
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
+    <OutputPath>bin\Debug\</OutputPath>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
+    <OutputPath>bin\Release\</OutputPath>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Tools|AnyCPU' ">
+    <OutputPath>bin\Tools\</OutputPath>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'DebugOpt|AnyCPU' ">
+    <OutputPath>bin\DebugOpt\</OutputPath>
+  </PropertyGroup>
+  <Target Name="Build">
+    <Exec Command="$(Python) git_helper.py" CustomErrorRegularExpression="^Error" />
+  </Target>
+  <Target Name="Rebuild" DependsOnTargets="Build" />
+  <Target Name="Clean">
+    <Message Importance="low" Text="Ignoring 'Clean' target." />
+  </Target>
+  <Target Name="Compile">
+  </Target>
+  <Target Name="CoreCompile">
+  </Target>
+</Project>

+ 110 - 0
BuildChecker/git_helper.py

@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+# Installs git hooks, updates them, updates submodules, that kind of thing.
+
+import subprocess
+import sys
+import os
+import shutil
+from pathlib import Path
+from typing import List
+
+SOLUTION_PATH = Path("..") / "SpaceStation14.sln"
+# If this doesn't match the saved version we overwrite them all.
+CURRENT_HOOKS_VERSION = "2"
+QUIET = len(sys.argv) == 2 and sys.argv[1] == "--quiet"
+
+
+def run_command(command: List[str], capture: bool = False) -> subprocess.CompletedProcess:
+    """
+    Runs a command with pretty output.
+    """
+    text = ' '.join(command)
+    if not QUIET:
+        print("$ {}".format(text))
+
+    sys.stdout.flush()
+
+    completed = None
+
+    if capture:
+        completed = subprocess.run(command, cwd="..", stdout=subprocess.PIPE)
+    else:
+        completed = subprocess.run(command, cwd="..")
+
+    if completed.returncode != 0:
+        print("Error: command exited with code {}!".format(completed.returncode))
+
+    return completed
+
+
+def update_submodules():
+    """
+    Updates all submodules.
+    """
+
+    if ('GITHUB_ACTIONS' in os.environ):
+        return
+
+    if os.path.isfile("DISABLE_SUBMODULE_AUTOUPDATE"):
+        return
+
+    if shutil.which("git") is None:
+        raise FileNotFoundError("git not found in PATH")
+
+    # If the status doesn't match, force VS to reload the solution.
+    # status = run_command(["git", "submodule", "status"], capture=True)
+    run_command(["git", "submodule", "update", "--init", "--recursive"])
+    # status2 = run_command(["git", "submodule", "status"], capture=True)
+
+    # Something changed.
+    # if status.stdout != status2.stdout:
+    #     print("Git submodules changed. Reloading solution.")
+    #     reset_solution()
+
+
+def install_hooks():
+    """
+    Installs the necessary git hooks into .git/hooks.
+    """
+
+    # Read version file.
+    if os.path.isfile("INSTALLED_HOOKS_VERSION"):
+        with open("INSTALLED_HOOKS_VERSION", "r") as f:
+            if f.read() == CURRENT_HOOKS_VERSION:
+                if not QUIET:
+                    print("No hooks change detected.")
+                return
+
+    with open("INSTALLED_HOOKS_VERSION", "w") as f:
+        f.write(CURRENT_HOOKS_VERSION)
+
+    print("Hooks need updating.")
+
+    hooks_target_dir = Path("..")/".git"/"hooks"
+    hooks_source_dir = Path("hooks")
+
+    # Clear entire tree since we need to kill deleted files too.
+    for filename in os.listdir(str(hooks_target_dir)):
+        os.remove(str(hooks_target_dir/filename))
+
+    for filename in os.listdir(str(hooks_source_dir)):
+        print("Copying hook {}".format(filename))
+        shutil.copy2(str(hooks_source_dir/filename),
+                        str(hooks_target_dir/filename))
+
+
+def reset_solution():
+    """
+    Force VS to think the solution has been changed to prompt the user to reload it, thus fixing any load errors.
+    """
+
+    with SOLUTION_PATH.open("r") as f:
+        content = f.read()
+
+    with SOLUTION_PATH.open("w") as f:
+        f.write(content)
+
+
+if __name__ == '__main__':
+    install_hooks()
+    update_submodules()

+ 13 - 0
BuildChecker/hooks/post-checkout

@@ -0,0 +1,13 @@
+#!/bin/bash
+
+gitroot=`git rev-parse --show-toplevel`
+
+cd "$gitroot/BuildChecker"
+
+if [[ `uname` == MINGW* || `uname` == CYGWIN* ]]; then
+    # Windows
+    py -3 git_helper.py --quiet
+else
+    # Not Windows, so probably some other Unix thing.
+    python3 git_helper.py --quiet
+fi

+ 5 - 0
BuildChecker/hooks/post-merge

@@ -0,0 +1,5 @@
+#!/bin/bash
+
+# Just call post-checkout since it does the same thing.
+gitroot=`git rev-parse --show-toplevel`
+bash "$gitroot/.git/hooks/post-checkout"

+ 20 - 0
BuildFiles/Mac/Space Station 14.app/Contents/Info.plist

@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>CFBundleName</key>
+    <string>SS14</string>
+    <key>CFBundleDisplayName</key>
+    <string>Space Station 14</string>
+    <key>CFBundleExecutable</key>
+    <string>SS14</string>
+    <!--
+    Just a note about this icon.
+    MacOS seems REALLY iffy about this and even when the file is correct,
+    it can take forever before it decides to actually update it and display it.
+    TL;DR Apple is stupid.
+    -->
+    <key>CFBundleIconFile</key>
+    <string>ss14</string>
+</dict>
+</plist>

+ 8 - 0
BuildFiles/Mac/Space Station 14.app/Contents/MacOS/SS14

@@ -0,0 +1,8 @@
+#!/bin/sh
+
+# cd to file containing script or something?
+BASEDIR=$(dirname "$0")
+echo "$BASEDIR"
+cd "$BASEDIR"
+
+exec ../Resources/Robust.Client "$@"

BIN
BuildFiles/Mac/Space Station 14.app/Contents/Resources/ss14.icns


+ 168 - 0
Content.Benchmarks/ColorInterpolateBenchmark.cs

@@ -0,0 +1,168 @@
+#if NETCOREAPP
+using System.Runtime.Intrinsics;
+using System.Runtime.Intrinsics.X86;
+#endif
+using System;
+using System.Runtime.CompilerServices;
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Maths;
+using Robust.Shared.Random;
+using SysVector4 = System.Numerics.Vector4;
+
+namespace Content.Benchmarks
+{
+    [DisassemblyDiagnoser]
+    [Virtual]
+    public class ColorInterpolateBenchmark
+    {
+#if NETCOREAPP
+        private const MethodImplOptions AggressiveOpt = MethodImplOptions.AggressiveOptimization;
+#else
+        private const MethodImplOptions AggressiveOpt = default;
+#endif
+
+        private (Color, Color)[] _colors;
+        private Color[] _output;
+
+        [Params(100)] public int N { get; set; }
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            var random = new Random(3005);
+
+            _colors = new (Color, Color)[N];
+            _output = new Color[N];
+
+            for (var i = 0; i < N; i++)
+            {
+                var r1 = random.NextFloat();
+                var g1 = random.NextFloat();
+                var b1 = random.NextFloat();
+                var a1 = random.NextFloat();
+
+                var r2 = random.NextFloat();
+                var g2 = random.NextFloat();
+                var b2 = random.NextFloat();
+                var a2 = random.NextFloat();
+
+                _colors[i] = (new Color(r1, g1, b1, a1), new Color(r2, g2, b2, a2));
+            }
+        }
+
+        [Benchmark]
+        public void BenchSimple()
+        {
+            for (var i = 0; i < N; i++)
+            {
+                ref var tuple = ref _colors[i];
+                _output[i] = InterpolateSimple(tuple.Item1, tuple.Item2, 0.5f);
+            }
+        }
+
+
+        [Benchmark]
+        public void BenchSysVector4In()
+        {
+            for (var i = 0; i < N; i++)
+            {
+                ref var tuple = ref _colors[i];
+                _output[i] = InterpolateSysVector4In(tuple.Item1, tuple.Item2, 0.5f);
+            }
+        }
+
+        [Benchmark]
+        public void BenchSysVector4()
+        {
+            for (var i = 0; i < N; i++)
+            {
+                ref var tuple = ref _colors[i];
+                _output[i] = InterpolateSysVector4(tuple.Item1, tuple.Item2, 0.5f);
+            }
+        }
+
+#if NETCOREAPP
+        [Benchmark]
+        public void BenchSimd()
+        {
+            for (var i = 0; i < N; i++)
+            {
+                ref var tuple = ref _colors[i];
+                _output[i] = InterpolateSimd(tuple.Item1, tuple.Item2, 0.5f);
+            }
+        }
+
+        [Benchmark]
+        public void BenchSimdIn()
+        {
+            for (var i = 0; i < N; i++)
+            {
+                ref var tuple = ref _colors[i];
+                _output[i] = InterpolateSimdIn(tuple.Item1, tuple.Item2, 0.5f);
+            }
+        }
+#endif
+
+        [MethodImpl(AggressiveOpt)]
+        public static Color InterpolateSimple(Color a, Color b, float lambda)
+        {
+            return new(
+                a.R + (b.R - a.R) * lambda,
+                a.G + (b.G - a.G) * lambda,
+                a.B + (b.G - a.B) * lambda,
+                a.A + (b.A - a.A) * lambda
+            );
+        }
+
+        [MethodImpl(AggressiveOpt)]
+        public static Color InterpolateSysVector4(Color a, Color b,
+            float lambda)
+        {
+            ref var sva = ref Unsafe.As<Color, SysVector4>(ref a);
+            ref var svb = ref Unsafe.As<Color, SysVector4>(ref b);
+
+            var res = SysVector4.Lerp(sva, svb, lambda);
+
+            return Unsafe.As<SysVector4, Color>(ref res);
+        }
+
+        [MethodImpl(AggressiveOpt)]
+        public static Color InterpolateSysVector4In(in Color endPoint1, in Color endPoint2,
+            float lambda)
+        {
+            ref var sva = ref Unsafe.As<Color, SysVector4>(ref Unsafe.AsRef(in endPoint1));
+            ref var svb = ref Unsafe.As<Color, SysVector4>(ref Unsafe.AsRef(in endPoint2));
+
+            var res = SysVector4.Lerp(svb, sva, lambda);
+
+            return Unsafe.As<SysVector4, Color>(ref res);
+        }
+
+#if NETCOREAPP
+        [MethodImpl(AggressiveOpt)]
+        public static Color InterpolateSimd(Color a, Color b,
+            float lambda)
+        {
+            var vecA = Unsafe.As<Color, Vector128<float>>(ref a);
+            var vecB = Unsafe.As<Color, Vector128<float>>(ref b);
+
+            vecB = Fma.MultiplyAdd(Sse.Subtract(vecB, vecA), Vector128.Create(lambda), vecA);
+
+            return Unsafe.As<Vector128<float>, Color>(ref vecB);
+        }
+
+        [MethodImpl(AggressiveOpt)]
+        public static Color InterpolateSimdIn(in Color a, in Color b,
+            float lambda)
+        {
+            var vecA = Unsafe.As<Color, Vector128<float>>(ref Unsafe.AsRef(in a));
+            var vecB = Unsafe.As<Color, Vector128<float>>(ref Unsafe.AsRef(in b));
+
+            vecB = Fma.MultiplyAdd(Sse.Subtract(vecB, vecA), Vector128.Create(lambda), vecA);
+
+            return Unsafe.As<Vector128<float>, Color>(ref vecB);
+        }
+#endif
+    }
+}

+ 259 - 0
Content.Benchmarks/ComponentFetchBenchmark.cs

@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Utility;
+
+namespace Content.Benchmarks
+{
+    [SimpleJob]
+    [Virtual]
+    public class ComponentFetchBenchmark
+    {
+        [Params(5000)] public int NEnt { get; set; }
+
+        private readonly Dictionary<(EntityUid, Type), BComponent>
+            _componentsFlat = new();
+
+        private readonly Dictionary<Type, Dictionary<EntityUid, BComponent>> _componentsPart =
+            new();
+
+        private UniqueIndex<Type, BComponent> _allComponents = new();
+
+        private readonly List<EntityUid> _lookupEntities = new();
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            var random = new Random();
+
+            _componentsPart[typeof(BComponent1)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent2)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent3)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent4)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponentLookup)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent6)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent7)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent8)] = new Dictionary<EntityUid, BComponent>();
+            _componentsPart[typeof(BComponent9)] = new Dictionary<EntityUid, BComponent>();
+
+            for (var i = 0u; i < NEnt; i++)
+            {
+                var eId = new EntityUid(i);
+
+                if (random.Next(1) == 0)
+                {
+                    _lookupEntities.Add(eId);
+                }
+
+                var comps = new List<BComponent>
+                {
+                    new BComponent1(),
+                    new BComponent2(),
+                    new BComponent3(),
+                    new BComponent4(),
+                    new BComponent6(),
+                    new BComponent7(),
+                    new BComponent8(),
+                    new BComponent9(),
+                };
+
+                if (random.Next(1000) == 0)
+                {
+                    comps.Add(new BComponentLookup());
+                }
+
+                foreach (var comp in comps)
+                {
+                    comp.Uid = eId;
+                    var type = comp.GetType();
+                    _componentsPart[type][eId] = comp;
+                    _componentsFlat[(eId, type)] = comp;
+                    _allComponents.Add(type, comp);
+                }
+            }
+        }
+
+        // These two benchmarks are find "needles in haystack" components.
+        // We try to look up a component that 0.1% of entities have on 1% of entities.
+        // Examples of this in the engine are VisibilityComponent lookups during PVS.
+        [Benchmark]
+        public void FindPart()
+        {
+            foreach (var entityUid in _lookupEntities)
+            {
+                var d = _componentsPart[typeof(BComponentLookup)];
+                d.TryGetValue(entityUid, out _);
+            }
+        }
+
+        [Benchmark]
+        public void FindFlat()
+        {
+            foreach (var entityUid in _lookupEntities)
+            {
+                _componentsFlat.TryGetValue((entityUid, typeof(BComponentLookup)), out _);
+            }
+        }
+
+        // Iteration benchmarks:
+        // We try to iterate every instance of a single component (BComponent1) and see which is faster.
+        [Benchmark]
+        public void IterPart()
+        {
+            var list = _componentsPart[typeof(BComponent1)];
+            var arr = new BComponent[list.Count];
+            var i = 0;
+            foreach (var c in list.Values)
+            {
+                arr[i++] = c;
+            }
+        }
+
+        [Benchmark]
+        public void IterFlat()
+        {
+            var list = _allComponents[typeof(BComponent1)];
+            var arr = new BComponent[list.Count];
+            var i = 0;
+            foreach (var c in list)
+            {
+                arr[i++] = c;
+            }
+        }
+
+        // We do the same as the iteration benchmarks but re-fetch the component every iteration.
+        // This is what entity systems mostly do via entity queries because crappy code.
+        [Benchmark]
+        public void IterFetchPart()
+        {
+            var list = _componentsPart[typeof(BComponent1)];
+            var arr = new BComponent[list.Count];
+            var i = 0;
+            foreach (var c in list.Values)
+            {
+                var eId = c.Uid;
+                var d = _componentsPart[typeof(BComponent1)];
+                arr[i++] = d[eId];
+            }
+        }
+
+        [Benchmark]
+        public void IterFetchFlat()
+        {
+            var list = _allComponents[typeof(BComponent1)];
+            var arr = new BComponent[list.Count];
+            var i = 0;
+            foreach (var c in list)
+            {
+                var eId = c.Uid;
+                arr[i++] = _componentsFlat[(eId, typeof(BComponent1))];
+            }
+        }
+
+        // Same as the previous benchmarks but with BComponentLookup instead.
+        // Which is only on 1% of entities.
+        [Benchmark]
+        public void IterFetchPartRare()
+        {
+            var list = _componentsPart[typeof(BComponentLookup)];
+            var arr = new BComponent[list.Count];
+            var i = 0;
+            foreach (var c in list.Values)
+            {
+                var eId = c.Uid;
+                var d = _componentsPart[typeof(BComponentLookup)];
+                arr[i++] = d[eId];
+            }
+        }
+
+        [Benchmark]
+        public void IterFetchFlatRare()
+        {
+            var list = _allComponents[typeof(BComponentLookup)];
+            var arr = new BComponent[list.Count];
+            var i = 0;
+            foreach (var c in list)
+            {
+                var eId = c.Uid;
+                arr[i++] = _componentsFlat[(eId, typeof(BComponentLookup))];
+            }
+        }
+
+        private readonly struct EntityUid : IEquatable<EntityUid>
+        {
+            public readonly uint Value;
+
+            public EntityUid(uint value)
+            {
+                Value = value;
+            }
+
+            public bool Equals(EntityUid other)
+            {
+                return Value == other.Value;
+            }
+
+            public override bool Equals(object obj)
+            {
+                return obj is EntityUid other && Equals(other);
+            }
+
+            public override int GetHashCode()
+            {
+                return (int) Value;
+            }
+
+            public static bool operator ==(EntityUid left, EntityUid right)
+            {
+                return left.Equals(right);
+            }
+
+            public static bool operator !=(EntityUid left, EntityUid right)
+            {
+                return !left.Equals(right);
+            }
+        }
+
+        private abstract class BComponent
+        {
+            public EntityUid Uid;
+        }
+
+        private sealed class BComponent1 : BComponent
+        {
+        }
+
+        private sealed class BComponent2 : BComponent
+        {
+        }
+
+        private sealed class BComponent3 : BComponent
+        {
+        }
+
+        private sealed class BComponent4 : BComponent
+        {
+        }
+
+        private sealed class BComponentLookup : BComponent
+        {
+        }
+
+        private sealed class BComponent6 : BComponent
+        {
+        }
+
+        private sealed class BComponent7 : BComponent
+        {
+        }
+
+        private sealed class BComponent8 : BComponent
+        {
+        }
+
+        private sealed class BComponent9 : BComponent
+        {
+        }
+    }
+}

+ 273 - 0
Content.Benchmarks/ComponentQueryBenchmark.cs

@@ -0,0 +1,273 @@
+#nullable enable
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Configs;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Shared.Clothing.Components;
+using Content.Shared.Doors.Components;
+using Content.Shared.Item;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.EntitySerialization;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Benchmarks;
+
+/// <summary>
+/// Benchmarks for comparing the speed of various component fetching/lookup related methods, including directed event
+/// subscriptions
+/// </summary>
+[Virtual]
+[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
+[CategoriesColumn]
+public class ComponentQueryBenchmark
+{
+    public const string Map = "Maps/atlas.yml";
+
+    private TestPair _pair = default!;
+    private IEntityManager _entMan = default!;
+    private EntityQuery<ItemComponent> _itemQuery;
+    private EntityQuery<ClothingComponent> _clothingQuery;
+    private EntityQuery<MapComponent> _mapQuery;
+    private EntityUid[] _items = default!;
+
+    [GlobalSetup]
+    public void Setup()
+    {
+        ProgramShared.PathOffset = "../../../../";
+        PoolManager.Startup(typeof(QueryBenchSystem).Assembly);
+
+        _pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
+        _entMan = _pair.Server.ResolveDependency<IEntityManager>();
+
+        _itemQuery = _entMan.GetEntityQuery<ItemComponent>();
+        _clothingQuery = _entMan.GetEntityQuery<ClothingComponent>();
+        _mapQuery = _entMan.GetEntityQuery<MapComponent>();
+
+        _pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
+        _pair.Server.WaitPost(() =>
+        {
+            var map = new ResPath(Map);
+            var opts = DeserializationOptions.Default with {InitializeMaps = true};
+            if (!_entMan.System<MapLoaderSystem>().TryLoadMap(map, out _, out _, opts))
+                throw new Exception("Map load failed");
+        }).GetAwaiter().GetResult();
+
+        _items = new EntityUid[_entMan.Count<ItemComponent>()];
+        var i = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent>();
+        while (enumerator.MoveNext(out var uid, out _))
+        {
+            _items[i++] = uid;
+        }
+    }
+
+    [GlobalCleanup]
+    public async Task Cleanup()
+    {
+        await _pair.DisposeAsync();
+        PoolManager.Shutdown();
+    }
+
+    #region TryComp
+
+    /// <summary>
+    /// Baseline TryComp benchmark. When the benchmark was created, around 40% of the items were clothing.
+    /// </summary>
+    [Benchmark(Baseline = true)]
+    [BenchmarkCategory("TryComp")]
+    public int TryComp()
+    {
+        var hashCode = 0;
+        foreach (var uid in _items)
+        {
+            if (_clothingQuery.TryGetComponent(uid, out var clothing))
+                hashCode = HashCode.Combine(hashCode, clothing.GetHashCode());
+        }
+        return hashCode;
+    }
+
+    /// <summary>
+    /// Variant of <see cref="TryComp"/> that is meant to always fail to get a component.
+    /// </summary>
+    [Benchmark]
+    [BenchmarkCategory("TryComp")]
+    public int TryCompFail()
+    {
+        var hashCode = 0;
+        foreach (var uid in _items)
+        {
+            if (_mapQuery.TryGetComponent(uid, out var map))
+                hashCode = HashCode.Combine(hashCode, map.GetHashCode());
+        }
+        return hashCode;
+    }
+
+    /// <summary>
+    /// Variant of <see cref="TryComp"/> that is meant to always succeed getting a component.
+    /// </summary>
+    [Benchmark]
+    [BenchmarkCategory("TryComp")]
+    public int TryCompSucceed()
+    {
+        var hashCode = 0;
+        foreach (var uid in _items)
+        {
+            if (_itemQuery.TryGetComponent(uid, out var item))
+                hashCode = HashCode.Combine(hashCode, item.GetHashCode());
+        }
+        return hashCode;
+    }
+
+    /// <summary>
+    /// Variant of <see cref="TryComp"/> that uses `Resolve()` to try get the component.
+    /// </summary>
+    [Benchmark]
+    [BenchmarkCategory("TryComp")]
+    public int Resolve()
+    {
+        var hashCode = 0;
+        foreach (var uid in _items)
+        {
+            DoResolve(uid, ref hashCode);
+        }
+        return hashCode;
+    }
+
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void DoResolve(EntityUid uid, ref int hash, ClothingComponent? clothing = null)
+    {
+        if (_clothingQuery.Resolve(uid, ref clothing, false))
+            hash = HashCode.Combine(hash, clothing.GetHashCode());
+    }
+
+    #endregion
+
+    #region Enumeration
+
+    [Benchmark]
+    [BenchmarkCategory("Item Enumerator")]
+    public int SingleItemEnumerator()
+    {
+        var hashCode = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<ItemComponent>();
+        while (enumerator.MoveNext(out var item))
+        {
+            hashCode = HashCode.Combine(hashCode, item.GetHashCode());
+        }
+
+        return hashCode;
+    }
+
+    [Benchmark]
+    [BenchmarkCategory("Item Enumerator")]
+    public int DoubleItemEnumerator()
+    {
+        var hashCode = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<ClothingComponent, ItemComponent>();
+        while (enumerator.MoveNext(out _, out var item))
+        {
+            hashCode = HashCode.Combine(hashCode, item.GetHashCode());
+        }
+
+        return hashCode;
+    }
+
+    [Benchmark]
+    [BenchmarkCategory("Item Enumerator")]
+    public int TripleItemEnumerator()
+    {
+        var hashCode = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<ClothingComponent, ItemComponent, TransformComponent>();
+        while (enumerator.MoveNext(out _, out _, out var xform))
+        {
+            hashCode = HashCode.Combine(hashCode, xform.GetHashCode());
+        }
+
+        return hashCode;
+    }
+
+    [Benchmark]
+    [BenchmarkCategory("Airlock Enumerator")]
+    public int SingleAirlockEnumerator()
+    {
+        var hashCode = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<AirlockComponent>();
+        while (enumerator.MoveNext(out var airlock))
+        {
+            hashCode = HashCode.Combine(hashCode, airlock.GetHashCode());
+        }
+
+        return hashCode;
+    }
+
+    [Benchmark]
+    [BenchmarkCategory("Airlock Enumerator")]
+    public int DoubleAirlockEnumerator()
+    {
+        var hashCode = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<AirlockComponent, DoorComponent>();
+        while (enumerator.MoveNext(out _, out var door))
+        {
+            hashCode = HashCode.Combine(hashCode, door.GetHashCode());
+        }
+
+        return hashCode;
+    }
+
+    [Benchmark]
+    [BenchmarkCategory("Airlock Enumerator")]
+    public int TripleAirlockEnumerator()
+    {
+        var hashCode = 0;
+        var enumerator = _entMan.AllEntityQueryEnumerator<AirlockComponent, DoorComponent, TransformComponent>();
+        while (enumerator.MoveNext(out _, out _, out var xform))
+        {
+            hashCode = HashCode.Combine(hashCode, xform.GetHashCode());
+        }
+
+        return hashCode;
+    }
+
+    #endregion
+
+    [Benchmark(Baseline = true)]
+    [BenchmarkCategory("Events")]
+    public int StructEvents()
+    {
+        var ev = new QueryBenchEvent();
+        foreach (var uid in _items)
+        {
+            _entMan.EventBus.RaiseLocalEvent(uid, ref ev);
+        }
+
+        return ev.HashCode;
+    }
+}
+
+[ByRefEvent]
+public struct QueryBenchEvent
+{
+    public int HashCode;
+}
+
+public sealed class QueryBenchSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        base.Initialize();
+        SubscribeLocalEvent<ClothingComponent, QueryBenchEvent>(OnEvent);
+    }
+
+    private void OnEvent(EntityUid uid, ClothingComponent component, ref QueryBenchEvent args)
+    {
+        args.HashCode = HashCode.Combine(args.HashCode, component.GetHashCode());
+    }
+}

+ 28 - 0
Content.Benchmarks/Content.Benchmarks.csproj

@@ -0,0 +1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
+  <PropertyGroup>
+    <!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
+    <TargetFramework>$(TargetFramework)</TargetFramework>
+    <OutputPath>..\bin\Content.Benchmarks\</OutputPath>
+    <IsPackable>false</IsPackable>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <OutputType>Exe</OutputType>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <LangVersion>12</LangVersion>
+  </PropertyGroup>
+  <ItemGroup>
+    <PackageReference Include="BenchmarkDotNet" />
+  </ItemGroup>
+  <ItemGroup>
+    <ProjectReference Include="..\Content.Client\Content.Client.csproj" />
+    <ProjectReference Include="..\Content.Server\Content.Server.csproj" />
+    <ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
+    <ProjectReference Include="..\Content.Tests\Content.Tests.csproj" />
+    <ProjectReference Include="..\Content.IntegrationTests\Content.IntegrationTests.csproj" />
+    <ProjectReference Include="..\RobustToolbox\Robust.Benchmarks\Robust.Benchmarks.csproj" />
+    <ProjectReference Include="..\RobustToolbox\Robust.Client\Robust.Client.csproj" />
+    <ProjectReference Include="..\RobustToolbox\Robust.Server\Robust.Server.csproj" />
+    <ProjectReference Include="..\RobustToolbox\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
+    <ProjectReference Include="..\RobustToolbox\Robust.Shared\Robust.Shared.csproj" />
+  </ItemGroup>
+</Project>

+ 70 - 0
Content.Benchmarks/DependencyInjectBenchmark.cs

@@ -0,0 +1,70 @@
+/*
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.IoC;
+
+namespace Content.Benchmarks
+{
+    // To actually run this benchmark you'll have to make DependencyCollection public so it's accessible.
+
+    [Virtual]
+    public class DependencyInjectBenchmark
+    {
+        [Params(InjectMode.Reflection, InjectMode.DynamicMethod)]
+        public InjectMode Mode { get; set; }
+
+        private DependencyCollection _dependencyCollection;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _dependencyCollection = new DependencyCollection();
+            _dependencyCollection.Register<X1, X1>();
+            _dependencyCollection.Register<X2, X2>();
+            _dependencyCollection.Register<X3, X3>();
+            _dependencyCollection.Register<X4, X4>();
+            _dependencyCollection.Register<X5, X5>();
+
+            _dependencyCollection.BuildGraph();
+
+            switch (Mode)
+            {
+                case InjectMode.Reflection:
+                    break;
+                case InjectMode.DynamicMethod:
+                    // Running this without oneOff will cause DependencyCollection to cache the DynamicMethod injector.
+                    // So future injections (even with oneOff) will keep using the DynamicMethod.
+                    // AKA, be fast.
+                    _dependencyCollection.InjectDependencies(new TestDummy());
+                    break;
+            }
+        }
+
+        [Benchmark]
+        public void Inject()
+        {
+            _dependencyCollection.InjectDependencies(new TestDummy(), true);
+        }
+
+        public enum InjectMode
+        {
+            Reflection,
+            DynamicMethod
+        }
+
+        private sealed class X1 { }
+        private sealed class X2 { }
+        private sealed class X3 { }
+        private sealed class X4 { }
+        private sealed class X5 { }
+
+        private sealed class TestDummy
+        {
+            [Dependency] private readonly X1 _x1;
+            [Dependency] private readonly X2 _x2;
+            [Dependency] private readonly X3 _x3;
+            [Dependency] private readonly X4 _x4;
+            [Dependency] private readonly X5 _x5;
+        }
+    }
+}
+*/

+ 142 - 0
Content.Benchmarks/DeviceNetworkingBenchmark.cs

@@ -0,0 +1,142 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.IntegrationTests.Tests.DeviceNetwork;
+using Content.Server.DeviceNetwork.Systems;
+using Content.Shared.DeviceNetwork;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.Benchmarks;
+
+[Virtual]
+[MemoryDiagnoser]
+public class DeviceNetworkingBenchmark
+{
+    private TestPair _pair = default!;
+    private DeviceNetworkTestSystem _deviceNetTestSystem = default!;
+    private DeviceNetworkSystem _deviceNetworkSystem = default!;
+    private EntityUid _sourceEntity;
+    private EntityUid _sourceWirelessEntity;
+    private readonly List<EntityUid> _targetEntities = new();
+    private readonly List<EntityUid> _targetWirelessEntities = new();
+
+
+    private NetworkPayload _payload = default!;
+
+    [TestPrototypes]
+    private const string Prototypes = @"
+- type: entity
+  name: DummyNetworkDevicePrivate
+  id: DummyNetworkDevicePrivate
+  components:
+    - type: DeviceNetwork
+      transmitFrequency: 100
+      receiveFrequency: 100
+      deviceNetId: Private
+- type: entity
+  name: DummyWirelessNetworkDevice
+  id: DummyWirelessNetworkDevice
+  components:
+    - type: DeviceNetwork
+      transmitFrequency: 100
+      receiveFrequency: 100
+      deviceNetId: Wireless
+    - type: WirelessNetworkConnection
+      range: 100
+        ";
+
+    //public static IEnumerable<int> EntityCountSource { get; set; }
+
+    //[ParamsSource(nameof(EntityCountSource))]
+    public int EntityCount = 500;
+
+    [GlobalSetup]
+    public async Task SetupAsync()
+    {
+        ProgramShared.PathOffset = "../../../../";
+        PoolManager.Startup(typeof(DeviceNetworkingBenchmark).Assembly);
+        _pair = await PoolManager.GetServerClient();
+        var server = _pair.Server;
+
+        await server.WaitPost(() =>
+        {
+            var entityManager = server.InstanceDependencyCollection.Resolve<IEntityManager>();
+            _deviceNetworkSystem = entityManager.EntitySysManager.GetEntitySystem<DeviceNetworkSystem>();
+            _deviceNetTestSystem = entityManager.EntitySysManager.GetEntitySystem<DeviceNetworkTestSystem>();
+
+            var testValue = "test";
+            _payload = new NetworkPayload
+            {
+                ["Test"] = testValue,
+                ["testnumber"] = 1,
+                ["testbool"] = true
+            };
+
+            _sourceEntity = entityManager.SpawnEntity("DummyNetworkDevicePrivate", MapCoordinates.Nullspace);
+            _sourceWirelessEntity = entityManager.SpawnEntity("DummyWirelessNetworkDevice", MapCoordinates.Nullspace);
+
+            for (var i = 0; i < EntityCount; i++)
+            {
+                _targetEntities.Add(entityManager.SpawnEntity("DummyNetworkDevicePrivate", MapCoordinates.Nullspace));
+                _targetWirelessEntities.Add(entityManager.SpawnEntity("DummyWirelessNetworkDevice", MapCoordinates.Nullspace));
+            }
+        });
+    }
+
+    [GlobalCleanup]
+    public async Task Cleanup()
+    {
+        await _pair.DisposeAsync();
+        PoolManager.Shutdown();
+    }
+
+    [Benchmark(Baseline = true, Description = "Entity Events")]
+    public async Task EventSentBaseline()
+    {
+        var server = _pair.Server;
+
+        _pair.Server.Post(() =>
+        {
+            foreach (var entity in _targetEntities)
+            {
+                _deviceNetTestSystem.SendBaselineTestEvent(entity);
+            }
+        });
+
+        await server.WaitRunTicks(1);
+        await server.WaitIdleAsync();
+    }
+
+    [Benchmark(Description = "Device Net Broadcast No Connection Checks")]
+    public async Task DeviceNetworkBroadcastNoConnectionChecks()
+    {
+        var server = _pair.Server;
+
+        _pair.Server.Post(() =>
+        {
+            _deviceNetworkSystem.QueuePacket(_sourceEntity, null, _payload, 100);
+        });
+
+        await server.WaitRunTicks(1);
+        await server.WaitIdleAsync();
+    }
+
+    [Benchmark(Description = "Device Net Broadcast Wireless Connection Checks")]
+    public async Task DeviceNetworkBroadcastWirelessConnectionChecks()
+    {
+        var server = _pair.Server;
+
+        _pair.Server.Post(() =>
+        {
+            _deviceNetworkSystem.QueuePacket(_sourceWirelessEntity, null, _payload, 100);
+        });
+
+        await server.WaitRunTicks(1);
+        await server.WaitIdleAsync();
+    }
+}

+ 68 - 0
Content.Benchmarks/DynamicTreeBenchmark.cs

@@ -0,0 +1,68 @@
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Maths;
+using Robust.Shared.Physics;
+
+namespace Content.Benchmarks
+{
+    [SimpleJob, MemoryDiagnoser]
+    [Virtual]
+    public class DynamicTreeBenchmark
+    {
+        private static readonly Box2[] Aabbs1 =
+        {
+            ((Box2) default).Enlarged(1), //2x2 square
+            ((Box2) default).Enlarged(2), //4x4 square
+            new(-3, 3, -3, 3), // point off to the bottom left
+            new(-3, -3, -3, -3), // point off to the top left
+            new(3, 3, 3, 3), // point off to the bottom right
+            new(3, -3, 3, -3), // point off to the top right
+            ((Box2) default).Enlarged(1), //2x2 square
+            ((Box2) default).Enlarged(2), //4x4 square
+            ((Box2) default).Enlarged(1), //2x2 square
+            ((Box2) default).Enlarged(2), //4x4 square
+            ((Box2) default).Enlarged(1), //2x2 square
+            ((Box2) default).Enlarged(2), //4x4 square
+            ((Box2) default).Enlarged(1), //2x2 square
+            ((Box2) default).Enlarged(2), //4x4 square
+            ((Box2) default).Enlarged(3), //6x6 square
+            new(-3, 3, -3, 3), // point off to the bottom left
+            new(-3, -3, -3, -3), // point off to the top left
+            new(3, 3, 3, 3), // point off to the bottom right
+            new(3, -3, 3, -3), // point off to the top right
+        };
+
+        private B2DynamicTree<int> _b2Tree;
+        private DynamicTree<int> _tree;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _b2Tree = new B2DynamicTree<int>();
+            _tree = new DynamicTree<int>((in int value) => Aabbs1[value], capacity: 16);
+
+            for (var i = 0; i < Aabbs1.Length; i++)
+            {
+                var aabb = Aabbs1[i];
+                _b2Tree.CreateProxy(aabb, uint.MaxValue, i);
+                _tree.Add(i);
+            }
+        }
+
+        [Benchmark]
+        public void BenchB2()
+        {
+            object state = null;
+            _b2Tree.Query(ref state, (ref object _, DynamicTree.Proxy __) => true, new Box2(-1, -1, 1, 1));
+        }
+
+        [Benchmark]
+        public void BenchQ()
+        {
+            foreach (var _ in _tree.QueryAabb(new Box2(-1, -1, 1, 1), true))
+            {
+
+            }
+        }
+    }
+}

+ 320 - 0
Content.Benchmarks/EntityFetchBenchmark.cs

@@ -0,0 +1,320 @@
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Utility;
+
+namespace Content.Benchmarks
+{
+    [SimpleJob]
+    [Virtual]
+    public class EntityFetchBenchmark
+    {
+        [Params(1000)] public int N { get; set; }
+
+        public int M { get; set; } = 10;
+
+        private readonly DictEntityStorage _dictStorage = new();
+        private readonly GenEntityStorage _genStorage = new();
+
+        private IEntityStorage<DictEntity, DictEntityUid> _dictStorageInterface;
+        private IEntityStorage<GenEntity, GenEntityUid> _genStorageInterface;
+
+        private DictEntityUid[] _toReadDict;
+        private DictEntity[] _toWriteDict;
+
+        private GenEntityUid[] _toReadGen;
+        private GenEntity[] _toWriteGen;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _dictStorageInterface = _dictStorage;
+            _genStorageInterface = _genStorage;
+
+            var r = new Random();
+
+            var allocatedGen = new List<GenEntity>();
+            var allocatedDict = new List<DictEntity>();
+
+            for (var i = 0; i < N; i++)
+            {
+                allocatedGen.Add(_genStorage.NewEntity());
+                allocatedDict.Add(_dictStorage.NewEntity());
+            }
+
+            var delTo = N / 2;
+            for (var i = 0; i < delTo; i++)
+            {
+                var index = r.Next(allocatedDict.Count);
+
+                var gEnt = allocatedGen[index];
+                var dEnt = allocatedDict[index];
+
+                _genStorage.DeleteEntity(gEnt);
+                _dictStorage.DeleteEntity(dEnt);
+
+                allocatedGen.RemoveSwap(i);
+                allocatedDict.RemoveSwap(i);
+            }
+
+            for (var i = 0; i < N; i++)
+            {
+                allocatedGen.Add(_genStorage.NewEntity());
+                allocatedDict.Add(_dictStorage.NewEntity());
+            }
+
+            for (var i = 0; i < delTo; i++)
+            {
+                var index = r.Next(allocatedDict.Count);
+
+                var gEnt = allocatedGen[index];
+                var dEnt = allocatedDict[index];
+
+                _genStorage.DeleteEntity(gEnt);
+                _dictStorage.DeleteEntity(dEnt);
+
+                allocatedGen.RemoveSwap(i);
+                allocatedDict.RemoveSwap(i);
+            }
+
+            _toReadDict = new DictEntityUid[M];
+            _toWriteDict = new DictEntity[M];
+            _toReadGen = new GenEntityUid[M];
+            _toWriteGen = new GenEntity[M];
+
+            for (var i = 0; i < M; i++)
+            {
+                var index = r.Next(allocatedDict.Count);
+
+                _toReadDict[i] = allocatedDict[index].Uid;
+                _toReadGen[i] = allocatedGen[index].Uid;
+            }
+        }
+
+        [Benchmark]
+        public void BenchGenId()
+        {
+            for (var i = 0; i < M; i++)
+            {
+                var uid = _toReadGen[i];
+                if (_genStorage.TryGetEntity(uid, out var entity))
+                {
+                    _toWriteGen[i] = entity;
+                }
+            }
+        }
+
+        [Benchmark]
+        public void BenchDict()
+        {
+            for (var i = 0; i < M; i++)
+            {
+                var uid = _toReadDict[i];
+                if (_dictStorage.TryGetEntity(uid, out var entity))
+                {
+                    _toWriteDict[i] = entity;
+                }
+            }
+        }
+
+        [Benchmark]
+        public void BenchGenIdInterface()
+        {
+            for (var i = 0; i < M; i++)
+            {
+                var uid = _toReadGen[i];
+                if (_genStorageInterface.TryGetEntity(uid, out var entity))
+                {
+                    _toWriteGen[i] = entity;
+                }
+            }
+        }
+
+        [Benchmark]
+        public void BenchDictInterface()
+        {
+            for (var i = 0; i < M; i++)
+            {
+                var uid = _toReadDict[i];
+                if (_dictStorageInterface.TryGetEntity(uid, out var entity))
+                {
+                    _toWriteDict[i] = entity;
+                }
+            }
+        }
+
+        private sealed class DictEntityStorage : EntityStorage<DictEntity, DictEntityUid>
+        {
+            private int _nextValue;
+
+            private readonly Dictionary<DictEntityUid, DictEntity> _dict = new();
+
+            public override bool TryGetEntity(DictEntityUid entityUid, out DictEntity entity)
+            {
+                if (!_dict.TryGetValue(entityUid, out entity))
+                {
+                    return false;
+                }
+
+                return !entity.Deleted;
+            }
+
+            public DictEntity NewEntity()
+            {
+                var e = new DictEntity(new DictEntityUid(_nextValue++));
+                _dict.Add(e.Uid, e);
+                return e;
+            }
+
+            public void DeleteEntity(DictEntity e)
+            {
+                DebugTools.Assert(!e.Deleted);
+
+                e.Deleted = true;
+
+                _dict.Remove(e.Uid);
+            }
+        }
+
+        private interface IEntityStorage<TEntity, TEntityUid>
+        {
+            public bool TryGetEntity(TEntityUid entityUid, out TEntity entity);
+        }
+
+        private abstract class EntityStorage<TEntity, TEntityUid> : IEntityStorage<TEntity, TEntityUid>
+        {
+            public abstract bool TryGetEntity(TEntityUid entityUid, out TEntity entity);
+
+            public TEntity GetEntity(TEntityUid entityUid)
+            {
+                if (!TryGetEntity(entityUid, out var entity))
+                    throw new ArgumentException($"Failed to get entity {entityUid} from storage.");
+
+                return entity;
+            }
+        }
+
+        private sealed class GenEntityStorage : EntityStorage<GenEntity, GenEntityUid>
+        {
+            private (int generation, GenEntity entity)[] _entities = new (int, GenEntity)[1];
+            private readonly List<int> _availableSlots = new() { 0 };
+
+            public override bool TryGetEntity(GenEntityUid entityUid, out GenEntity entity)
+            {
+                var (generation, genEntity) = _entities[entityUid.Index];
+                entity = genEntity;
+
+                return generation == entityUid.Generation;
+            }
+
+            public GenEntity NewEntity()
+            {
+                if (_availableSlots.Count == 0)
+                {
+                    // Reallocate
+                    var oldEntities = _entities;
+                    _entities = new (int, GenEntity)[_entities.Length * 2];
+                    oldEntities.CopyTo(_entities, 0);
+
+                    for (var i = oldEntities.Length; i < _entities.Length; i++)
+                    {
+                        _availableSlots.Add(i);
+                    }
+                }
+
+                var index = _availableSlots.Pop();
+                ref var slot = ref _entities[index];
+                var slotEntity = new GenEntity(new GenEntityUid(slot.generation, index));
+                slot.entity = slotEntity;
+
+                return slotEntity;
+            }
+
+            public void DeleteEntity(GenEntity e)
+            {
+                DebugTools.Assert(!e.Deleted);
+
+                e.Deleted = true;
+
+                ref var slot = ref _entities[e.Uid.Index];
+                slot.entity = null;
+                slot.generation += 1;
+
+                _availableSlots.Add(e.Uid.Index);
+            }
+        }
+
+
+        private readonly struct DictEntityUid : IEquatable<DictEntityUid>
+        {
+            public readonly int Value;
+
+            public DictEntityUid(int value)
+            {
+                Value = value;
+            }
+
+            public bool Equals(DictEntityUid other)
+            {
+                return Value == other.Value;
+            }
+
+            public override bool Equals(object obj)
+            {
+                return obj is DictEntityUid other && Equals(other);
+            }
+
+            public override int GetHashCode()
+            {
+                return Value;
+            }
+
+            public static bool operator ==(DictEntityUid left, DictEntityUid right)
+            {
+                return left.Equals(right);
+            }
+
+            public static bool operator !=(DictEntityUid left, DictEntityUid right)
+            {
+                return !left.Equals(right);
+            }
+        }
+
+        private readonly struct GenEntityUid
+        {
+            public readonly int Generation;
+            public readonly int Index;
+
+            public GenEntityUid(int generation, int index)
+            {
+                Generation = generation;
+                Index = index;
+            }
+        }
+
+        private sealed class DictEntity
+        {
+            public DictEntity(DictEntityUid uid)
+            {
+                Uid = uid;
+            }
+
+            public DictEntityUid Uid { get; }
+
+            public bool Deleted { get; set; }
+        }
+
+        private sealed class GenEntity
+        {
+            public GenEntityUid Uid { get; }
+
+            public bool Deleted { get; set; }
+
+            public GenEntity(GenEntityUid uid)
+            {
+                Uid = uid;
+            }
+        }
+    }
+}

+ 96 - 0
Content.Benchmarks/EntityManagerGetAllComponents.cs

@@ -0,0 +1,96 @@
+using BenchmarkDotNet.Attributes;
+using Moq;
+using Robust.Shared.Analyzers;
+using Robust.Shared.Exceptions;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Map;
+using Robust.Shared.Reflection;
+
+namespace Content.Benchmarks
+{
+    [Virtual]
+    public partial class EntityManagerGetAllComponents
+    {
+        private IEntityManager _entityManager;
+
+        [Params(5000)] public int N { get; set; }
+
+        public static void TestRun()
+        {
+            var x = new EntityManagerGetAllComponents
+            {
+                N = 500
+            };
+            x.Setup();
+            x.Run();
+        }
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            // Initialize component manager.
+            IoCManager.InitThread();
+
+            IoCManager.Register<IEntityManager, EntityManager>();
+            IoCManager.Register<IRuntimeLog, RuntimeLog>();
+            IoCManager.Register<ILogManager, LogManager>();
+            IoCManager.Register<IDynamicTypeFactory, DynamicTypeFactory>();
+            IoCManager.Register<IEntitySystemManager, EntitySystemManager>();
+            IoCManager.RegisterInstance<IReflectionManager>(new Mock<IReflectionManager>().Object);
+
+            var dummyReg = new ComponentRegistration(
+                "Dummy",
+                typeof(DummyComponent),
+                CompIdx.Index<DummyComponent>());
+
+            var componentFactory = new Mock<IComponentFactory>();
+            componentFactory.Setup(p => p.GetComponent<DummyComponent>()).Returns(new DummyComponent());
+            componentFactory.Setup(m => m.GetIndex(typeof(DummyComponent))).Returns(CompIdx.Index<DummyComponent>());
+            componentFactory.Setup(p => p.GetRegistration(It.IsAny<DummyComponent>())).Returns(dummyReg);
+            componentFactory.Setup(p => p.GetAllRegistrations()).Returns(new[] { dummyReg });
+            componentFactory.Setup(p => p.GetAllRefTypes()).Returns(new[] { CompIdx.Index<DummyComponent>() });
+
+            IoCManager.RegisterInstance<IComponentFactory>(componentFactory.Object);
+
+            IoCManager.BuildGraph();
+            _entityManager = IoCManager.Resolve<IEntityManager>();
+            _entityManager.Initialize();
+
+            // Initialize N entities with one component.
+            for (var i = 0; i < N; i++)
+            {
+                var entity = _entityManager.SpawnEntity(null, EntityCoordinates.Invalid);
+                _entityManager.AddComponent<DummyComponent>(entity);
+            }
+        }
+
+        [Benchmark]
+        public int Run()
+        {
+            var count = 0;
+
+            foreach (var _ in _entityManager.EntityQuery<DummyComponent>(true))
+            {
+                count += 1;
+            }
+
+            return count;
+        }
+
+        [Benchmark]
+        public int Noop()
+        {
+            var count = 0;
+
+            _entityManager.TryGetComponent(default, out DummyComponent _);
+
+            return count;
+        }
+
+        private sealed partial class DummyComponent : Component
+        {
+        }
+    }
+}

+ 79 - 0
Content.Benchmarks/MapLoadBenchmark.cs

@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Maps;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Benchmarks;
+
+[Virtual]
+public class MapLoadBenchmark
+{
+    private TestPair _pair = default!;
+    private MapLoaderSystem _mapLoader = default!;
+    private SharedMapSystem _mapSys = default!;
+
+    [GlobalSetup]
+    public void Setup()
+    {
+        ProgramShared.PathOffset = "../../../../";
+        PoolManager.Startup();
+
+        _pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
+        var server = _pair.Server;
+
+        Paths = server.ResolveDependency<IPrototypeManager>()
+            .EnumeratePrototypes<GameMapPrototype>()
+            .ToDictionary(x => x.ID, x => x.MapPath.ToString());
+
+        _mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
+        _mapSys = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<SharedMapSystem>();
+    }
+
+    [GlobalCleanup]
+    public async Task Cleanup()
+    {
+        await _pair.DisposeAsync();
+        PoolManager.Shutdown();
+    }
+
+    public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Convex"};
+
+    [ParamsSource(nameof(MapsSource))]
+    public string Map;
+
+    public Dictionary<string, string> Paths;
+    private MapId _mapId;
+
+    [Benchmark]
+    public async Task LoadMap()
+    {
+        var mapPath = new ResPath(Paths[Map]);
+        var server = _pair.Server;
+        await server.WaitPost(() =>
+        {
+            var success = _mapLoader.TryLoadMap(mapPath, out var map, out _);
+            if (!success)
+                throw new Exception("Map load failed");
+            _mapId = map.Value.Comp.MapId;
+        });
+    }
+
+    [IterationCleanup]
+    public void IterationCleanup()
+    {
+        var server = _pair.Server;
+        server.WaitPost(() => _mapSys.DeleteMap(_mapId))
+            .Wait();
+    }
+}

+ 265 - 0
Content.Benchmarks/NetSerializerIntBenchmark.cs

@@ -0,0 +1,265 @@
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+
+namespace Content.Benchmarks
+{
+    [SimpleJob]
+    [Virtual]
+    public class NetSerializerIntBenchmark
+    {
+        private MemoryStream _writeStream;
+        private MemoryStream _readStream;
+        private readonly ushort _x16 = 5;
+        private readonly uint _x32 = 5;
+        private readonly ulong _x64 = 5;
+        private ushort _read16;
+        private uint _read32;
+        private ulong _read64;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _writeStream = new MemoryStream(64);
+            _readStream = new MemoryStream();
+            _readStream.Write(new byte[] { 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8 });
+        }
+
+        [Benchmark]
+        public void BenchWrite16Span()
+        {
+            _writeStream.Position = 0;
+            WriteUInt16Span(_writeStream, _x16);
+        }
+
+        [Benchmark]
+        public void BenchWrite32Span()
+        {
+            _writeStream.Position = 0;
+            WriteUInt32Span(_writeStream, _x32);
+        }
+
+        [Benchmark]
+        public void BenchWrite64Span()
+        {
+            _writeStream.Position = 0;
+            WriteUInt64Span(_writeStream, _x64);
+        }
+
+        [Benchmark]
+        public void BenchRead16Span()
+        {
+            _readStream.Position = 0;
+            _read16 = ReadUInt16Span(_readStream);
+        }
+
+        [Benchmark]
+        public void BenchRead32Span()
+        {
+            _readStream.Position = 0;
+            _read32 = ReadUInt32Span(_readStream);
+        }
+
+        [Benchmark]
+        public void BenchRead64Span()
+        {
+            _readStream.Position = 0;
+            _read64 = ReadUInt64Span(_readStream);
+        }
+
+        [Benchmark]
+        public void BenchWrite16Byte()
+        {
+            _writeStream.Position = 0;
+            WriteUInt16Byte(_writeStream, _x16);
+        }
+
+        [Benchmark]
+        public void BenchWrite32Byte()
+        {
+            _writeStream.Position = 0;
+            WriteUInt32Byte(_writeStream, _x32);
+        }
+
+        [Benchmark]
+        public void BenchWrite64Byte()
+        {
+            _writeStream.Position = 0;
+            WriteUInt64Byte(_writeStream, _x64);
+        }
+
+        [Benchmark]
+        public void BenchRead16Byte()
+        {
+            _readStream.Position = 0;
+            _read16 = ReadUInt16Byte(_readStream);
+        }
+        [Benchmark]
+        public void BenchRead32Byte()
+        {
+            _readStream.Position = 0;
+            _read32 = ReadUInt32Byte(_readStream);
+        }
+
+        [Benchmark]
+        public void BenchRead64Byte()
+        {
+            _readStream.Position = 0;
+            _read64 = ReadUInt64Byte(_readStream);
+        }
+
+        private static void WriteUInt16Byte(Stream stream, ushort value)
+        {
+            stream.WriteByte((byte) value);
+            stream.WriteByte((byte) (value >> 8));
+        }
+
+        private static void WriteUInt32Byte(Stream stream, uint value)
+        {
+            stream.WriteByte((byte) value);
+            stream.WriteByte((byte) (value >> 8));
+            stream.WriteByte((byte) (value >> 16));
+            stream.WriteByte((byte) (value >> 24));
+        }
+
+        private static void WriteUInt64Byte(Stream stream, ulong value)
+        {
+            stream.WriteByte((byte) value);
+            stream.WriteByte((byte) (value >> 8));
+            stream.WriteByte((byte) (value >> 16));
+            stream.WriteByte((byte) (value >> 24));
+            stream.WriteByte((byte) (value >> 32));
+            stream.WriteByte((byte) (value >> 40));
+            stream.WriteByte((byte) (value >> 48));
+            stream.WriteByte((byte) (value >> 56));
+        }
+
+        private static ushort ReadUInt16Byte(Stream stream)
+        {
+            ushort a = 0;
+
+            for (var i = 0; i < 16; i += 8)
+            {
+                var val = stream.ReadByte();
+                if (val == -1)
+                    throw new EndOfStreamException();
+
+                a |= (ushort) (val << i);
+            }
+
+            return a;
+        }
+
+        private static uint ReadUInt32Byte(Stream stream)
+        {
+            uint a = 0;
+
+            for (var i = 0; i < 32; i += 8)
+            {
+                var val = stream.ReadByte();
+                if (val == -1)
+                    throw new EndOfStreamException();
+
+                a |= (uint) val << i;
+            }
+
+            return a;
+        }
+
+        private static ulong ReadUInt64Byte(Stream stream)
+        {
+            ulong a = 0;
+
+            for (var i = 0; i < 64; i += 8)
+            {
+                var val = stream.ReadByte();
+                if (val == -1)
+                    throw new EndOfStreamException();
+
+                a |= (ulong) val << i;
+            }
+
+            return a;
+        }
+
+        private static void WriteUInt16Span(Stream stream, ushort value)
+        {
+            Span<byte> buf = stackalloc byte[2];
+            BinaryPrimitives.WriteUInt16LittleEndian(buf, value);
+
+            stream.Write(buf);
+        }
+
+        private static void WriteUInt32Span(Stream stream, uint value)
+        {
+            Span<byte> buf = stackalloc byte[4];
+            BinaryPrimitives.WriteUInt32LittleEndian(buf, value);
+
+            stream.Write(buf);
+        }
+
+        private static void WriteUInt64Span(Stream stream, ulong value)
+        {
+            Span<byte> buf = stackalloc byte[8];
+            BinaryPrimitives.WriteUInt64LittleEndian(buf, value);
+
+            stream.Write(buf);
+        }
+
+        private static ushort ReadUInt16Span(Stream stream)
+        {
+            Span<byte> buf = stackalloc byte[2];
+            var wSpan = buf;
+
+            while (true)
+            {
+                var read = stream.Read(wSpan);
+                if (read == 0)
+                    throw new EndOfStreamException();
+                if (read == wSpan.Length)
+                    break;
+                wSpan = wSpan[read..];
+            }
+
+            return BinaryPrimitives.ReadUInt16LittleEndian(buf);
+        }
+
+        private static uint ReadUInt32Span(Stream stream)
+        {
+            Span<byte> buf = stackalloc byte[4];
+            var wSpan = buf;
+
+            while (true)
+            {
+                var read = stream.Read(wSpan);
+                if (read == 0)
+                    throw new EndOfStreamException();
+                if (read == wSpan.Length)
+                    break;
+                wSpan = wSpan[read..];
+            }
+
+            return BinaryPrimitives.ReadUInt32LittleEndian(buf);
+        }
+
+        private static ulong ReadUInt64Span(Stream stream)
+        {
+            Span<byte> buf = stackalloc byte[8];
+            var wSpan = buf;
+
+            while (true)
+            {
+                var read = stream.Read(wSpan);
+                if (read == 0)
+                    throw new EndOfStreamException();
+                if (read == wSpan.Length)
+                    break;
+                wSpan = wSpan[read..];
+            }
+
+            return BinaryPrimitives.ReadUInt64LittleEndian(buf);
+        }
+    }
+}

+ 431 - 0
Content.Benchmarks/NetSerializerStringBenchmark.cs

@@ -0,0 +1,431 @@
+using System;
+using System.Buffers;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+using System.Text.Unicode;
+using BenchmarkDotNet.Attributes;
+using Lidgren.Network;
+using NetSerializer;
+using Robust.Shared.Analyzers;
+
+namespace Content.Benchmarks
+{
+    // Code for the *Slow and *Unsafe implementations taken from NetSerializer, licensed under the MIT license.
+
+    [MemoryDiagnoser]
+    [Virtual]
+    public class NetSerializerStringBenchmark
+    {
+        private const int StringByteBufferLength = 256;
+        private const int StringCharBufferLength = 128;
+
+        private string _toSerialize;
+
+        [Params(8, 64, 256, 1024)]
+        public int StringLength { get; set; }
+
+        private readonly MemoryStream _outputStream = new(2048);
+        private readonly MemoryStream _inputStream = new(2048);
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            Span<byte> buf = stackalloc byte[StringLength / 2];
+            new Random().NextBytes(buf);
+            _toSerialize = NetUtility.ToHexString(buf);
+            Primitives.WritePrimitive(_inputStream, _toSerialize);
+        }
+
+        [Benchmark]
+        public void BenchWriteCore()
+        {
+            _outputStream.Position = 0;
+            WritePrimitiveCore(_outputStream, _toSerialize);
+        }
+
+        [Benchmark]
+        public void BenchReadCore()
+        {
+            _inputStream.Position = 0;
+            ReadPrimitiveCore(_inputStream, out _);
+        }
+
+        [Benchmark]
+        public void BenchWriteUnsafe()
+        {
+            _outputStream.Position = 0;
+            WritePrimitiveUnsafe(_outputStream, _toSerialize);
+        }
+
+        [Benchmark]
+        public void BenchReadUnsafe()
+        {
+            _inputStream.Position = 0;
+            ReadPrimitiveUnsafe(_inputStream, out _);
+        }
+
+        [Benchmark]
+        public void BenchWriteSlow()
+        {
+            _outputStream.Position = 0;
+            WritePrimitiveSlow(_outputStream, _toSerialize);
+        }
+
+        [Benchmark]
+        public void BenchReadSlow()
+        {
+            _inputStream.Position = 0;
+            ReadPrimitiveSlow(_inputStream, out _);
+        }
+
+        public static void WritePrimitiveCore(Stream stream, string value)
+        {
+            if (value == null)
+            {
+                Primitives.WritePrimitive(stream, (uint) 0);
+                return;
+            }
+
+            if (value.Length == 0)
+            {
+                Primitives.WritePrimitive(stream, (uint) 1);
+                return;
+            }
+
+            Span<byte> buf = stackalloc byte[StringByteBufferLength];
+
+            var totalChars = value.Length;
+            var totalBytes = Encoding.UTF8.GetByteCount(value);
+
+            Primitives.WritePrimitive(stream, (uint) totalBytes + 1);
+            Primitives.WritePrimitive(stream, (uint) totalChars);
+
+            var totalRead = 0;
+            ReadOnlySpan<char> span = value;
+            while (true)
+            {
+                var finalChunk = totalRead + totalChars >= totalChars;
+                Utf8.FromUtf16(span, buf, out var read, out var wrote, isFinalBlock: finalChunk);
+                stream.Write(buf[0..wrote]);
+                totalRead += read;
+                if (read >= totalChars)
+                {
+                    break;
+                }
+
+                span = span[read..];
+                totalChars -= read;
+            }
+        }
+
+        public static void ReadPrimitiveCore(Stream stream, out string value)
+        {
+            Primitives.ReadPrimitive(stream, out uint totalBytes);
+
+            if (totalBytes == 0)
+            {
+                value = null;
+                return;
+            }
+
+            if (totalBytes == 1)
+            {
+                value = string.Empty;
+                return;
+            }
+
+            totalBytes -= 1;
+
+            Primitives.ReadPrimitive(stream, out uint totalChars);
+
+            value = string.Create((int) totalChars, ((int) totalBytes, stream), StringSpanRead);
+        }
+
+        private static void StringSpanRead(Span<char> span, (int totalBytes, Stream stream) tuple)
+        {
+            Span<byte> buf = stackalloc byte[StringByteBufferLength];
+
+            // ReSharper disable VariableHidesOuterVariable
+            var (totalBytes, stream) = tuple;
+            // ReSharper restore VariableHidesOuterVariable
+
+            var totalBytesRead = 0;
+            var totalCharsRead = 0;
+            var writeBufStart = 0;
+
+            while (totalBytesRead < totalBytes)
+            {
+                var bytesLeft = totalBytes - totalBytesRead;
+                var bytesReadLeft = Math.Min(buf.Length, bytesLeft);
+                var writeSlice = buf[writeBufStart..(bytesReadLeft - writeBufStart)];
+                var bytesInBuffer = stream.Read(writeSlice);
+                if (bytesInBuffer == 0) throw new EndOfStreamException();
+
+                var readFromStream = bytesInBuffer + writeBufStart;
+                var final = readFromStream == bytesLeft;
+                var status = Utf8.ToUtf16(buf[..readFromStream], span[totalCharsRead..], out var bytesRead, out var charsRead, isFinalBlock: final);
+
+                totalBytesRead += bytesRead;
+                totalCharsRead += charsRead;
+                writeBufStart = 0;
+
+                if (status == OperationStatus.DestinationTooSmall)
+                {
+                    // Malformed data?
+                    throw new InvalidDataException();
+                }
+
+                if (status == OperationStatus.NeedMoreData)
+                {
+                    // We got cut short in the middle of a multi-byte UTF-8 sequence.
+                    // So we need to move it to the bottom of the span, then read the next bit *past* that.
+                    // This copy should be fine because we're only ever gonna be copying up to 4 bytes
+                    // from the end of the buffer to the start.
+                    // So no chance of overlap.
+                    buf[bytesRead..].CopyTo(buf);
+                    writeBufStart = bytesReadLeft - bytesRead;
+                    continue;
+                }
+
+                Debug.Assert(status == OperationStatus.Done);
+            }
+        }
+
+        public static void WritePrimitiveSlow(Stream stream, string value)
+        {
+            if (value == null)
+            {
+                Primitives.WritePrimitive(stream, (uint) 0);
+                return;
+            }
+            else if (value.Length == 0)
+            {
+                Primitives.WritePrimitive(stream, (uint) 1);
+                return;
+            }
+
+            var encoding = new UTF8Encoding(false, true);
+
+            var len = encoding.GetByteCount(value);
+
+            Primitives.WritePrimitive(stream, (uint) len + 1);
+            Primitives.WritePrimitive(stream, (uint) value.Length);
+
+            var buf = new byte[len];
+
+            encoding.GetBytes(value, 0, value.Length, buf, 0);
+
+            stream.Write(buf, 0, len);
+        }
+
+        public static void ReadPrimitiveSlow(Stream stream, out string value)
+        {
+            Primitives.ReadPrimitive(stream, out uint len);
+
+            if (len == 0)
+            {
+                value = null;
+                return;
+            }
+            else if (len == 1)
+            {
+                value = string.Empty;
+                return;
+            }
+
+            Primitives.ReadPrimitive(stream, out uint _);
+
+            len -= 1;
+
+            var encoding = new UTF8Encoding(false, true);
+
+            var buf = new byte[len];
+
+            var l = 0;
+
+            while (l < len)
+            {
+                var r = stream.Read(buf, l, (int) len - l);
+                if (r == 0)
+                    throw new EndOfStreamException();
+                l += r;
+            }
+
+            value = encoding.GetString(buf);
+        }
+
+        private sealed class StringHelper
+        {
+            public StringHelper()
+            {
+                Encoding = new UTF8Encoding(false, true);
+            }
+
+            private Encoder _encoder;
+            private Decoder _decoder;
+
+            private byte[] _byteBuffer;
+            private char[] _charBuffer;
+
+            public UTF8Encoding Encoding { get; private set; }
+            public Encoder Encoder
+            {
+                get
+                {
+                    _encoder ??= Encoding.GetEncoder();
+                    return _encoder;
+                }
+            }
+            public Decoder Decoder
+            {
+                get
+                {
+                    _decoder ??= Encoding.GetDecoder();
+                    return _decoder;
+                }
+            }
+
+            public byte[] ByteBuffer
+            {
+                get
+                {
+                    _byteBuffer ??= new byte[StringByteBufferLength];
+                    return _byteBuffer;
+                }
+            }
+            public char[] CharBuffer
+            {
+                get
+                {
+                    _charBuffer ??= new char[StringCharBufferLength];
+                    return _charBuffer;
+                }
+            }
+        }
+
+        [ThreadStatic]
+        private static StringHelper _stringHelper;
+
+        public static unsafe void WritePrimitiveUnsafe(Stream stream, string value)
+        {
+            if (value == null)
+            {
+                Primitives.WritePrimitive(stream, (uint) 0);
+                return;
+            }
+            else if (value.Length == 0)
+            {
+                Primitives.WritePrimitive(stream, (uint) 1);
+                return;
+            }
+
+            var helper = _stringHelper;
+            if (helper == null)
+                _stringHelper = helper = new StringHelper();
+
+            var encoder = helper.Encoder;
+            var buf = helper.ByteBuffer;
+
+            var totalChars = value.Length;
+            int totalBytes;
+
+            fixed (char* ptr = value)
+                totalBytes = encoder.GetByteCount(ptr, totalChars, true);
+
+            Primitives.WritePrimitive(stream, (uint) totalBytes + 1);
+            Primitives.WritePrimitive(stream, (uint) totalChars);
+
+            var p = 0;
+            var completed = false;
+
+            while (completed == false)
+            {
+                int charsConverted;
+                int bytesConverted;
+
+                fixed (char* src = value)
+                fixed (byte* dst = buf)
+                {
+                    encoder.Convert(src + p, totalChars - p, dst, buf.Length, true,
+                        out charsConverted, out bytesConverted, out completed);
+                }
+
+                stream.Write(buf, 0, bytesConverted);
+
+                p += charsConverted;
+            }
+        }
+
+        public static void ReadPrimitiveUnsafe(Stream stream, out string value)
+        {
+            Primitives.ReadPrimitive(stream, out uint totalBytes);
+
+            if (totalBytes == 0)
+            {
+                value = null;
+                return;
+            }
+            else if (totalBytes == 1)
+            {
+                value = string.Empty;
+                return;
+            }
+
+            totalBytes -= 1;
+
+            Primitives.ReadPrimitive(stream, out uint totalChars);
+
+            var helper = _stringHelper;
+            if (helper == null)
+                _stringHelper = helper = new StringHelper();
+
+            var decoder = helper.Decoder;
+            var buf = helper.ByteBuffer;
+            char[] chars;
+            if (totalChars <= StringCharBufferLength)
+                chars = helper.CharBuffer;
+            else
+                chars = new char[totalChars];
+
+            var streamBytesLeft = (int) totalBytes;
+
+            var cp = 0;
+
+            while (streamBytesLeft > 0)
+            {
+                var bytesInBuffer = stream.Read(buf, 0, Math.Min(buf.Length, streamBytesLeft));
+                if (bytesInBuffer == 0)
+                    throw new EndOfStreamException();
+
+                streamBytesLeft -= bytesInBuffer;
+                var flush = streamBytesLeft == 0;
+
+                var completed = false;
+
+                var p = 0;
+
+                while (completed == false)
+                {
+                    decoder.Convert(
+                        buf,
+                        p,
+                        bytesInBuffer - p,
+                        chars,
+                        cp,
+                        (int) totalChars - cp,
+                        flush,
+                        out var bytesConverted,
+                        out var charsConverted,
+                        out completed
+                    );
+
+                    p += bytesConverted;
+                    cp += charsConverted;
+                }
+            }
+
+            value = new string(chars, 0, (int) totalChars);
+        }
+    }
+}

+ 32 - 0
Content.Benchmarks/Program.cs

@@ -0,0 +1,32 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Running;
+using Content.IntegrationTests;
+using Content.Server.Maps;
+#if DEBUG
+using BenchmarkDotNet.Configs;
+#else
+using Robust.Benchmarks.Configs;
+#endif
+using Robust.Shared.Prototypes;
+
+namespace Content.Benchmarks
+{
+    internal static class Program
+    {
+
+        public static void Main(string[] args)
+        {
+#if DEBUG
+            Console.ForegroundColor = ConsoleColor.Red;
+            Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK");
+            Console.WriteLine("THE DEBUG BUILD IS ONLY GOOD FOR FIXING A CRASHING BENCHMARK\n");
+            BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, new DebugInProcessConfig());
+#else
+            var config = Environment.GetEnvironmentVariable("ROBUST_BENCHMARKS_ENABLE_SQL") != null ? DefaultSQLConfig.Instance : null;
+            BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args, config);
+#endif
+        }
+    }
+}

+ 177 - 0
Content.Benchmarks/PvsBenchmark.cs

@@ -0,0 +1,177 @@
+#nullable enable
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Mind;
+using Content.Server.Warps;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.EntitySerialization;
+using Robust.Shared.EntitySerialization.Systems;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Utility;
+
+namespace Content.Benchmarks;
+
+// This benchmark probably benefits from some accidental cache locality. I,e. the order in which entities in a pvs
+// chunk are sent to players matches the order in which the entities were spawned.
+//
+// in a real mid-late game round, this is probably no longer the case.
+// One way to somewhat offset this is to update the NetEntity assignment to assign random (but still unique) NetEntity uids to entities.
+// This makes the benchmark run noticeably slower.
+
+[Virtual]
+public class PvsBenchmark
+{
+    public const string Map = "Maps/box.yml";
+
+    [Params(1, 8, 80)]
+    public int PlayerCount { get; set; }
+
+    private TestPair _pair = default!;
+    private IEntityManager _entMan = default!;
+    private ICommonSession[] _players = default!;
+    private EntityCoordinates[] _spawns = default!;
+    public int _cycleOffset = 0;
+    private SharedTransformSystem _sys = default!;
+    private EntityCoordinates[] _locations = default!;
+
+    [GlobalSetup]
+    public void Setup()
+    {
+#if !DEBUG
+        ProgramShared.PathOffset = "../../../../";
+#endif
+        PoolManager.Startup();
+
+        _pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
+        _entMan = _pair.Server.ResolveDependency<IEntityManager>();
+        _pair.Server.CfgMan.SetCVar(CVars.NetPVS, true);
+        _pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0);
+        _pair.Server.CfgMan.SetCVar(CVars.NetPvsAsync, false);
+        _sys = _entMan.System<SharedTransformSystem>();
+
+        SetupAsync().Wait();
+    }
+
+    private async Task SetupAsync()
+    {
+        // Spawn the map
+        _pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
+        await _pair.Server.WaitPost(() =>
+        {
+            var path = new ResPath(Map);
+            var opts = DeserializationOptions.Default with {InitializeMaps = true};
+            if (!_entMan.System<MapLoaderSystem>().TryLoadMap(path, out _, out _, opts))
+                throw new Exception("Map load failed");
+        });
+
+        // Get list of ghost warp positions
+        _spawns = _entMan.AllComponentsList<WarpPointComponent>()
+            .OrderBy(x => x.Component.Location)
+            .Select(x => _entMan.GetComponent<TransformComponent>(x.Uid).Coordinates)
+            .ToArray();
+
+        Array.Resize(ref _players, PlayerCount);
+
+        // Spawn "Players"
+        _players = await _pair.Server.AddDummySessions(PlayerCount);
+        await _pair.Server.WaitPost(() =>
+        {
+            var mind = _pair.Server.System<MindSystem>();
+            for (var i = 0; i < PlayerCount; i++)
+            {
+                var pos = _spawns[i % _spawns.Length];
+                var uid =_entMan.SpawnEntity("MobHuman", pos);
+                _pair.Server.ConsoleHost.ExecuteCommand($"setoutfit {_entMan.GetNetEntity(uid)} CaptainGear");
+                mind.ControlMob(_players[i].UserId, uid);
+            }
+        });
+
+        // Repeatedly move players around so that they "explore" the map and see lots of entities.
+        // This will populate their PVS data with out-of-view entities.
+        var rng = new Random(42);
+        ShufflePlayers(rng, 100);
+
+        _pair.Server.PvsTick(_players);
+        _pair.Server.PvsTick(_players);
+
+        var ents = _players.Select(x => x.AttachedEntity!.Value).ToArray();
+        _locations = ents.Select(x => _entMan.GetComponent<TransformComponent>(x).Coordinates).ToArray();
+    }
+
+    private void ShufflePlayers(Random rng, int count)
+    {
+        while (count > 0)
+        {
+            ShufflePlayers(rng);
+            count--;
+        }
+    }
+
+    private void ShufflePlayers(Random rng)
+    {
+        _pair.Server.PvsTick(_players);
+
+        var ents = _players.Select(x => x.AttachedEntity!.Value).ToArray();
+        var locations = ents.Select(x => _entMan.GetComponent<TransformComponent>(x).Coordinates).ToArray();
+
+        // Shuffle locations
+        var n = locations.Length;
+        while (n > 1)
+        {
+            n -= 1;
+            var k = rng.Next(n + 1);
+            (locations[k], locations[n]) = (locations[n], locations[k]);
+        }
+
+        _pair.Server.WaitPost(() =>
+        {
+            for (var i = 0; i < PlayerCount; i++)
+            {
+                _sys.SetCoordinates(ents[i], locations[i]);
+            }
+        }).Wait();
+
+        _pair.Server.PvsTick(_players);
+    }
+
+    /// <summary>
+    /// Basic benchmark for PVS in a static situation where nothing moves or gets dirtied..
+    /// This effectively provides a lower bound on "real" pvs tick time, as it is missing:
+    /// - PVS chunks getting dirtied and needing to be rebuilt
+    /// - Fetching component states for dirty components
+    /// - Compressing & sending network messages
+    /// - Sending PVS leave messages
+    /// </summary>
+    [Benchmark]
+    public void StaticTick()
+    {
+        _pair.Server.PvsTick(_players);
+    }
+
+    /// <summary>
+    /// Basic benchmark for PVS in a situation where players are teleporting all over the place. This isn't very
+    /// realistic, but unlike <see cref="StaticTick"/> this will actually also measure the speed of processing dirty
+    /// chunks and sending PVS leave messages.
+    /// </summary>
+    [Benchmark]
+    public void CycleTick()
+    {
+        _cycleOffset = (_cycleOffset + 1) % _players.Length;
+        _pair.Server.WaitPost(() =>
+        {
+            for (var i = 0; i < PlayerCount; i++)
+            {
+                _sys.SetCoordinates(_players[i].AttachedEntity!.Value, _locations[(i + _cycleOffset) % _players.Length]);
+            }
+        }).Wait();
+        _pair.Server.PvsTick(_players);
+    }
+}

+ 66 - 0
Content.Benchmarks/SpawnEquipDeleteBenchmark.cs

@@ -0,0 +1,66 @@
+using System.Threading.Tasks;
+using BenchmarkDotNet.Attributes;
+using Content.IntegrationTests;
+using Content.IntegrationTests.Pair;
+using Content.Server.Station.Systems;
+using Content.Shared.Roles;
+using Robust.Shared;
+using Robust.Shared.Analyzers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.Benchmarks;
+
+/// <summary>
+/// This benchmarks spawns several humans, gives them captain equipment and then deletes them.
+/// This measures performance for spawning, deletion, containers, and inventory code.
+/// </summary>
+[Virtual, MemoryDiagnoser]
+public class SpawnEquipDeleteBenchmark
+{
+    private TestPair _pair = default!;
+    private StationSpawningSystem _spawnSys = default!;
+    private const string Mob = "MobHuman";
+    private StartingGearPrototype _gear = default!;
+    private EntityUid _entity;
+    private EntityCoordinates _coords;
+
+    [Params(1, 4, 16, 64)]
+    public int N;
+
+    [GlobalSetup]
+    public async Task SetupAsync()
+    {
+        ProgramShared.PathOffset = "../../../../";
+        PoolManager.Startup();
+        _pair = await PoolManager.GetServerClient();
+        var server = _pair.Server;
+
+        var mapData = await _pair.CreateTestMap();
+        _coords = mapData.GridCoords;
+        _spawnSys = server.System<StationSpawningSystem>();
+        _gear = server.ProtoMan.Index<StartingGearPrototype>("CaptainGear");
+    }
+
+    [GlobalCleanup]
+    public async Task Cleanup()
+    {
+        await _pair.DisposeAsync();
+        PoolManager.Shutdown();
+    }
+
+    [Benchmark]
+    public async Task SpawnDeletePlayer()
+    {
+        await _pair.Server.WaitPost(() =>
+        {
+            var server = _pair.Server;
+            for (var i = 0; i < N; i++)
+            {
+                _entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
+                _spawnSys.EquipStartingGear(_entity, _gear);
+                server.EntMan.DeleteEntity(_entity);
+            }
+        });
+    }
+}

+ 72 - 0
Content.Benchmarks/StereoToMonoBenchmark.cs

@@ -0,0 +1,72 @@
+using System.Runtime.Intrinsics.X86;
+using BenchmarkDotNet.Attributes;
+using Robust.Shared.Analyzers;
+
+namespace Content.Benchmarks
+{
+    [Virtual]
+    public class StereoToMonoBenchmark
+    {
+        [Params(128, 256, 512)]
+        public int N { get; set; }
+
+        private short[] _input;
+        private short[] _output;
+
+        [GlobalSetup]
+        public void Setup()
+        {
+            _input = new short[N * 2];
+            _output = new short[N];
+        }
+
+        [Benchmark]
+        public void BenchSimple()
+        {
+            var l = N;
+            for (var j = 0; j < l; j++)
+            {
+                var k = j + l;
+                _output[j] = (short) ((_input[k] + _input[j]) / 2);
+            }
+        }
+
+        [Benchmark]
+        public unsafe void BenchSse()
+        {
+            var l = N;
+            fixed (short* iPtr = _input)
+            fixed (short* oPtr = _output)
+            {
+                for (var j = 0; j < l; j += 8)
+                {
+                    var k = j + l;
+
+                    var jV = Sse2.ShiftRightArithmetic(Sse2.LoadVector128(iPtr + j), 1);
+                    var kV = Sse2.ShiftRightArithmetic(Sse2.LoadVector128(iPtr + k), 1);
+
+                    Sse2.Store(j + oPtr, Sse2.Add(jV, kV));
+                }
+            }
+        }
+
+        [Benchmark]
+        public unsafe void BenchAvx2()
+        {
+            var l = N;
+            fixed (short* iPtr = _input)
+            fixed (short* oPtr = _output)
+            {
+                for (var j = 0; j < l; j += 16)
+                {
+                    var k = j + l;
+
+                    var jV = Avx2.ShiftRightArithmetic(Avx.LoadVector256(iPtr + j), 1);
+                    var kV = Avx2.ShiftRightArithmetic(Avx.LoadVector256(iPtr + k), 1);
+
+                    Avx.Store(j + oPtr, Avx2.Add(jV, kV));
+                }
+            }
+        }
+    }
+}

+ 94 - 0
Content.Client/Access/AccessOverlay.cs

@@ -0,0 +1,94 @@
+using System.Text;
+using Content.Client.Resources;
+using Content.Shared.Access.Components;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Enums;
+
+namespace Content.Client.Access;
+
+public sealed class AccessOverlay : Overlay
+{
+    private const string TextFontPath = "/Fonts/NotoSans/NotoSans-Regular.ttf";
+    private const int TextFontSize = 12;
+
+    private readonly IEntityManager _entityManager;
+    private readonly SharedTransformSystem _transformSystem;
+    private readonly Font _font;
+
+    public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+    public AccessOverlay(IEntityManager entityManager, IResourceCache resourceCache, SharedTransformSystem transformSystem)
+    {
+        _entityManager = entityManager;
+        _transformSystem = transformSystem;
+        _font = resourceCache.GetFont(TextFontPath, TextFontSize);
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        if (args.ViewportControl == null)
+            return;
+
+        var textBuffer = new StringBuilder();
+        var query = _entityManager.EntityQueryEnumerator<AccessReaderComponent, TransformComponent>();
+        while (query.MoveNext(out var uid, out var accessReader, out var transform))
+        {
+            textBuffer.Clear();
+
+            var entityName = _entityManager.ToPrettyString(uid);
+            textBuffer.AppendLine(entityName.Prototype);
+            textBuffer.Append("UID: ");
+            textBuffer.Append(entityName.Uid.Id);
+            textBuffer.Append(", NUID: ");
+            textBuffer.Append(entityName.Nuid.Id);
+            textBuffer.AppendLine();
+
+            if (!accessReader.Enabled)
+            {
+                textBuffer.AppendLine("-Disabled");
+                continue;
+            }
+
+            if (accessReader.AccessLists.Count > 0)
+            {
+                var groupNumber = 0;
+                foreach (var accessList in accessReader.AccessLists)
+                {
+                    groupNumber++;
+                    foreach (var entry in accessList)
+                    {
+                        textBuffer.Append("+Set ");
+                        textBuffer.Append(groupNumber);
+                        textBuffer.Append(": ");
+                        textBuffer.Append(entry.Id);
+                        textBuffer.AppendLine();
+                    }
+                }
+            }
+            else
+            {
+                textBuffer.AppendLine("+Unrestricted");
+            }
+
+            foreach (var key in accessReader.AccessKeys)
+            {
+                textBuffer.Append("+Key ");
+                textBuffer.Append(key.OriginStation);
+                textBuffer.Append(": ");
+                textBuffer.Append(key.Id);
+                textBuffer.AppendLine();
+            }
+
+            foreach (var tag in accessReader.DenyTags)
+            {
+                textBuffer.Append("-Tag ");
+                textBuffer.AppendLine(tag.Id);
+            }
+
+            var accessInfoText = textBuffer.ToString();
+            var screenPos = args.ViewportControl.WorldToScreen(_transformSystem.GetWorldPosition(transform));
+            args.ScreenHandle.DrawString(_font, screenPos, accessInfoText, Color.Gold);
+        }
+    }
+}

+ 11 - 0
Content.Client/Access/AccessOverriderSystem.cs

@@ -0,0 +1,11 @@
+using Content.Shared.Access.Systems;
+using JetBrains.Annotations;
+
+namespace Content.Client.Access
+{
+    [UsedImplicitly]
+    public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
+    {
+
+    }
+}

+ 7 - 0
Content.Client/Access/AccessSystem.cs

@@ -0,0 +1,7 @@
+using Content.Shared.Access.Systems;
+
+namespace Content.Client.Access;
+
+public sealed class AccessSystem : SharedAccessSystem
+{
+}

+ 42 - 0
Content.Client/Access/Commands/ShowAccessReadersCommand.cs

@@ -0,0 +1,42 @@
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Shared.Console;
+
+namespace Content.Client.Access.Commands;
+
+public sealed class ShowAccessReadersCommand : IConsoleCommand
+{
+    public string Command => "showaccessreaders";
+
+    public string Description => "Toggles showing access reader permissions on the map";
+    public string Help => """
+        Overlay Info:
+        -Disabled | The access reader is disabled
+        +Unrestricted | The access reader has no restrictions
+        +Set [Index]: [Tag Name]| A tag in an access set (accessor needs all tags in the set to be allowed by the set)
+        +Key [StationUid]: [StationRecordKeyId] | A StationRecordKey that is allowed
+        -Tag [Tag Name] | A tag that is not allowed (takes priority over other allows)
+        """;
+    public void Execute(IConsoleShell shell, string argStr, string[] args)
+    {
+        var collection = IoCManager.Instance;
+
+        if (collection == null)
+            return;
+
+        var overlay = collection.Resolve<IOverlayManager>();
+
+        if (overlay.RemoveOverlay<AccessOverlay>())
+        {
+            shell.WriteLine($"Set access reader debug overlay to false");
+            return;
+        }
+
+        var entManager = collection.Resolve<IEntityManager>();
+        var cache = collection.Resolve<IResourceCache>();
+        var xform = entManager.System<SharedTransformSystem>();
+
+        overlay.AddOverlay(new AccessOverlay(entManager, cache, xform));
+        shell.WriteLine($"Set access reader debug overlay to true");
+    }
+}

+ 13 - 0
Content.Client/Access/IdCardConsoleSystem.cs

@@ -0,0 +1,13 @@
+using Content.Shared.Access.Systems;
+using JetBrains.Annotations;
+
+namespace Content.Client.Access
+{
+    [UsedImplicitly]
+    public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
+    {
+        // one day, maybe bound user interfaces can be shared too.
+        // then this doesn't have to be like this.
+        // I hate this.
+    }
+}

+ 5 - 0
Content.Client/Access/IdCardSystem.cs

@@ -0,0 +1,5 @@
+using Content.Shared.Access.Systems;
+
+namespace Content.Client.Access;
+
+public sealed class IdCardSystem : SharedIdCardSystem;

+ 4 - 0
Content.Client/Access/UI/AccessLevelControl.xaml

@@ -0,0 +1,4 @@
+<GridContainer xmlns="https://spacestation14.io"
+               Columns="5"
+               HorizontalAlignment="Center">
+</GridContainer>

+ 59 - 0
Content.Client/Access/UI/AccessLevelControl.xaml.cs

@@ -0,0 +1,59 @@
+using System.Linq;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+
+namespace Content.Client.Access.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class AccessLevelControl : GridContainer
+{
+    [Dependency] private readonly ILogManager _logManager = default!;
+
+    private ISawmill _sawmill = default!;
+
+    public readonly Dictionary<ProtoId<AccessLevelPrototype>, Button> ButtonsList = new();
+
+    public AccessLevelControl()
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        _sawmill = _logManager.GetSawmill("accesslevelcontrol");
+    }
+
+    public void Populate(List<ProtoId<AccessLevelPrototype>> accessLevels, IPrototypeManager prototypeManager)
+    {
+        foreach (var access in accessLevels)
+        {
+            if (!prototypeManager.TryIndex(access, out var accessLevel))
+            {
+                _sawmill.Error($"Unable to find accesslevel for {access}");
+                continue;
+            }
+
+            var newButton = new Button
+            {
+                Text = accessLevel.GetAccessLevelName(),
+                ToggleMode = true,
+            };
+            AddChild(newButton);
+            ButtonsList.Add(accessLevel.ID, newButton);
+        }
+    }
+
+    public void UpdateState(
+        List<ProtoId<AccessLevelPrototype>> pressedList,
+        List<ProtoId<AccessLevelPrototype>>? enabledList = null)
+    {
+        foreach (var (accessName, button) in ButtonsList)
+        {
+            button.Pressed = pressedList.Contains(accessName);
+            button.Disabled = !(enabledList?.Contains(accessName) ?? true);
+        }
+    }
+}

+ 77 - 0
Content.Client/Access/UI/AccessOverriderBoundUserInterface.cs

@@ -0,0 +1,77 @@
+using Content.Shared.Access;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Containers.ItemSlots;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+using static Content.Shared.Access.Components.AccessOverriderComponent;
+
+namespace Content.Client.Access.UI
+{
+    public sealed class AccessOverriderBoundUserInterface : BoundUserInterface
+    {
+        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        private readonly SharedAccessOverriderSystem _accessOverriderSystem = default!;
+
+        private AccessOverriderWindow? _window;
+
+        public AccessOverriderBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+        {
+            _accessOverriderSystem = EntMan.System<SharedAccessOverriderSystem>();
+        }
+
+        protected override void Open()
+        {
+            base.Open();
+
+            _window = this.CreateWindow<AccessOverriderWindow>();
+            RefreshAccess();
+            _window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
+            _window.OnSubmit += SubmitData;
+
+            _window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
+        }
+
+        public override void OnProtoReload(PrototypesReloadedEventArgs args)
+        {
+            base.OnProtoReload(args);
+            if (!args.WasModified<AccessLevelPrototype>())
+                return;
+
+            RefreshAccess();
+
+            if (State != null)
+                _window?.UpdateState(_prototypeManager, (AccessOverriderBoundUserInterfaceState) State);
+        }
+
+        private void RefreshAccess()
+        {
+            List<ProtoId<AccessLevelPrototype>> accessLevels;
+
+            if (EntMan.TryGetComponent<AccessOverriderComponent>(Owner, out var accessOverrider))
+            {
+                accessLevels = accessOverrider.AccessLevels;
+                accessLevels.Sort();
+            }
+            else
+            {
+                accessLevels = new List<ProtoId<AccessLevelPrototype>>();
+                _accessOverriderSystem.Log.Error($"No AccessOverrider component found for {EntMan.ToPrettyString(Owner)}!");
+            }
+
+            _window?.SetAccessLevels(_prototypeManager, accessLevels);
+        }
+
+        protected override void UpdateState(BoundUserInterfaceState state)
+        {
+            base.UpdateState(state);
+            var castState = (AccessOverriderBoundUserInterfaceState) state;
+            _window?.UpdateState(_prototypeManager, castState);
+        }
+
+        public void SubmitData(List<ProtoId<AccessLevelPrototype>> newAccessList)
+        {
+            SendMessage(new WriteToTargetAccessReaderIdMessage(newAccessList));
+        }
+    }
+}

+ 23 - 0
Content.Client/Access/UI/AccessOverriderWindow.xaml

@@ -0,0 +1,23 @@
+<DefaultWindow xmlns="https://spacestation14.io"
+            MinSize="650 290">
+    <BoxContainer Orientation="Vertical">
+        <GridContainer Columns="2">
+            <GridContainer Columns="3" HorizontalExpand="True">
+                <Label Text="{Loc 'access-overrider-window-privileged-id'}" />
+                <Button Name="PrivilegedIdButton" Access="Public"/>
+                <Label Name="PrivilegedIdLabel" />
+            </GridContainer>
+        </GridContainer>
+        <Label Name="TargetNameLabel" />
+        <Control MinSize="0 8"/>
+        <GridContainer Name="AccessLevelGrid" Columns="5" HorizontalAlignment="Center">
+
+            <!-- Access level buttons are added here by the C# code -->
+
+        </GridContainer>
+        <Control MinSize="0 8"/>
+        <Label Name="MissingPrivilegesLabel" />
+        <Control MinSize="0 4"/>
+        <Label Name="MissingPrivilegesText" />
+    </BoxContainer>
+</DefaultWindow>

+ 98 - 0
Content.Client/Access/UI/AccessOverriderWindow.xaml.cs

@@ -0,0 +1,98 @@
+using System.Linq;
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using static Content.Shared.Access.Components.AccessOverriderComponent;
+
+namespace Content.Client.Access.UI
+{
+    [GenerateTypedNameReferences]
+    public sealed partial class AccessOverriderWindow : DefaultWindow
+    {
+        private readonly Dictionary<string, Button> _accessButtons = new();
+
+        public event Action<List<ProtoId<AccessLevelPrototype>>>? OnSubmit;
+
+        public AccessOverriderWindow()
+        {
+            RobustXamlLoader.Load(this);
+        }
+
+        public void SetAccessLevels(IPrototypeManager protoManager, List<ProtoId<AccessLevelPrototype>> accessLevels)
+        {
+            _accessButtons.Clear();
+            AccessLevelGrid.DisposeAllChildren();
+
+            foreach (var access in accessLevels)
+            {
+                if (!protoManager.TryIndex(access, out var accessLevel))
+                {
+                    continue;
+                }
+
+                var newButton = new Button
+                {
+                    Text = accessLevel.GetAccessLevelName(),
+                    ToggleMode = true,
+                };
+
+                AccessLevelGrid.AddChild(newButton);
+                _accessButtons.Add(accessLevel.ID, newButton);
+                newButton.OnPressed += _ =>
+                {
+                    OnSubmit?.Invoke(
+                        // Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
+                        _accessButtons.Where(x => x.Value.Pressed).Select(x => new ProtoId<AccessLevelPrototype>(x.Key)).ToList());
+                };
+            }
+        }
+
+        public void UpdateState(IPrototypeManager protoManager, AccessOverriderBoundUserInterfaceState state)
+        {
+            PrivilegedIdLabel.Text = state.PrivilegedIdName;
+            PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
+                ? Loc.GetString("access-overrider-window-eject-button")
+                : Loc.GetString("access-overrider-window-insert-button");
+
+            TargetNameLabel.Text = state.TargetLabel;
+            TargetNameLabel.FontColorOverride = state.TargetLabelColor;
+
+            MissingPrivilegesLabel.Text = "";
+            MissingPrivilegesLabel.FontColorOverride = Color.Yellow;
+
+            MissingPrivilegesText.Text = "";
+            MissingPrivilegesText.FontColorOverride = Color.Yellow;
+
+            if (state.MissingPrivilegesList != null && state.MissingPrivilegesList.Any())
+            {
+                var missingPrivileges = new List<string>();
+
+                foreach (string tag in state.MissingPrivilegesList)
+                {
+                    var privilege = Loc.GetString(protoManager.Index<AccessLevelPrototype>(tag)?.Name ?? "generic-unknown");
+                    missingPrivileges.Add(privilege);
+                }
+
+                MissingPrivilegesLabel.Text = Loc.GetString("access-overrider-window-missing-privileges");
+                MissingPrivilegesText.Text = string.Join(", ", missingPrivileges);
+            }
+
+            var interfaceEnabled = state.IsPrivilegedIdPresent && state.IsPrivilegedIdAuthorized;
+
+            foreach (var (accessName, button) in _accessButtons)
+            {
+                button.Disabled = !interfaceEnabled;
+                if (interfaceEnabled)
+                {
+                    // Explicit cast because Rider gives a false error otherwise.
+                    button.Pressed = state.TargetAccessReaderIdAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName) ?? false;
+                    button.Disabled = (!state.AllowedModifyAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName)) ?? true;
+                }
+            }
+        }
+    }
+}

+ 61 - 0
Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs

@@ -0,0 +1,61 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.StatusIcon;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Access.UI
+{
+    /// <summary>
+    /// Initializes a <see cref="AgentIDCardWindow"/> and updates it when new server messages are received.
+    /// </summary>
+    public sealed class AgentIDCardBoundUserInterface : BoundUserInterface
+    {
+        private AgentIDCardWindow? _window;
+
+        public AgentIDCardBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+        {
+        }
+
+        protected override void Open()
+        {
+            base.Open();
+
+            _window = this.CreateWindow<AgentIDCardWindow>();
+
+            _window.OnNameChanged += OnNameChanged;
+            _window.OnJobChanged += OnJobChanged;
+            _window.OnJobIconChanged += OnJobIconChanged;
+        }
+
+        private void OnNameChanged(string newName)
+        {
+            SendMessage(new AgentIDCardNameChangedMessage(newName));
+        }
+
+        private void OnJobChanged(string newJob)
+        {
+            SendMessage(new AgentIDCardJobChangedMessage(newJob));
+        }
+
+        public void OnJobIconChanged(ProtoId<JobIconPrototype> newJobIconId)
+        {
+            SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
+        }
+
+        /// <summary>
+        /// Update the UI state based on server-sent info
+        /// </summary>
+        /// <param name="state"></param>
+        protected override void UpdateState(BoundUserInterfaceState state)
+        {
+            base.UpdateState(state);
+            if (_window == null || state is not AgentIDCardBoundUserInterfaceState cast)
+                return;
+
+            _window.SetCurrentName(cast.CurrentName);
+            _window.SetCurrentJob(cast.CurrentJob);
+            _window.SetAllowedIcons(cast.CurrentJobIconId);
+        }
+    }
+}

+ 14 - 0
Content.Client/Access/UI/AgentIDCardWindow.xaml

@@ -0,0 +1,14 @@
+<DefaultWindow xmlns="https://spacestation14.io"
+            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+            Title="{Loc agent-id-menu-title}">
+    <BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
+        <Label Name="CurrentName" Text="{Loc 'agent-id-card-current-name'}" />
+        <LineEdit Name="NameLineEdit" />
+        <Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
+        <LineEdit Name="JobLineEdit" />
+        <Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
+        <GridContainer Name="IconGrid" Columns="10">
+            <!-- Job icon buttons are generated in the code -->
+        </GridContainer>
+    </BoxContainer>
+</DefaultWindow>

+ 96 - 0
Content.Client/Access/UI/AgentIDCardWindow.xaml.cs

@@ -0,0 +1,96 @@
+using Content.Client.Stylesheets;
+using Content.Shared.StatusIcon;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Numerics;
+using System.Linq;
+
+namespace Content.Client.Access.UI
+{
+    [GenerateTypedNameReferences]
+    public sealed partial class AgentIDCardWindow : DefaultWindow
+    {
+        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+        private readonly SpriteSystem _spriteSystem;
+
+        private const int JobIconColumnCount = 10;
+
+        public event Action<string>? OnNameChanged;
+        public event Action<string>? OnJobChanged;
+
+        public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged;
+
+        public AgentIDCardWindow()
+        {
+            RobustXamlLoader.Load(this);
+            IoCManager.InjectDependencies(this);
+            _spriteSystem = _entitySystem.GetEntitySystem<SpriteSystem>();
+
+            NameLineEdit.OnTextEntered += e => OnNameChanged?.Invoke(e.Text);
+            NameLineEdit.OnFocusExit += e => OnNameChanged?.Invoke(e.Text);
+
+            JobLineEdit.OnTextEntered += e => OnJobChanged?.Invoke(e.Text);
+            JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
+        }
+
+        public void SetAllowedIcons(string currentJobIconId)
+        {
+            IconGrid.DisposeAllChildren();
+
+            var jobIconButtonGroup = new ButtonGroup();
+            var i = 0;
+            var icons = _prototypeManager.EnumeratePrototypes<JobIconPrototype>().Where(icon => icon.AllowSelection).ToList();
+            icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture));
+            foreach (var jobIcon in icons)
+            {
+                String styleBase = StyleBase.ButtonOpenBoth;
+                var modulo = i % JobIconColumnCount;
+                if (modulo == 0)
+                    styleBase = StyleBase.ButtonOpenRight;
+                else if (modulo == JobIconColumnCount - 1)
+                    styleBase = StyleBase.ButtonOpenLeft;
+
+                // Generate buttons
+                var jobIconButton = new Button
+                {
+                    Access = AccessLevel.Public,
+                    StyleClasses = { styleBase },
+                    MaxSize = new Vector2(42, 28),
+                    Group = jobIconButtonGroup,
+                    Pressed = currentJobIconId == jobIcon.ID,
+                    ToolTip = jobIcon.LocalizedJobName
+                };
+
+                // Generate buttons textures
+                var jobIconTexture = new TextureRect
+                {
+                    Texture = _spriteSystem.Frame0(jobIcon.Icon),
+                    TextureScale = new Vector2(2.5f, 2.5f),
+                    Stretch = TextureRect.StretchMode.KeepCentered,
+                };
+
+                jobIconButton.AddChild(jobIconTexture);
+                jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(jobIcon.ID);
+                IconGrid.AddChild(jobIconButton);
+
+                i++;
+            }
+        }
+
+        public void SetCurrentName(string name)
+        {
+            NameLineEdit.Text = name;
+        }
+
+        public void SetCurrentJob(string job)
+        {
+            JobLineEdit.Text = job;
+        }
+    }
+}

+ 82 - 0
Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs

@@ -0,0 +1,82 @@
+using Content.Shared.Access;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Containers.ItemSlots;
+using Content.Shared.CrewManifest;
+using Robust.Shared.Prototypes;
+using static Content.Shared.Access.Components.IdCardConsoleComponent;
+
+namespace Content.Client.Access.UI
+{
+    public sealed class IdCardConsoleBoundUserInterface : BoundUserInterface
+    {
+        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        private readonly SharedIdCardConsoleSystem _idCardConsoleSystem = default!;
+
+        private IdCardConsoleWindow? _window;
+
+        public IdCardConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+        {
+            _idCardConsoleSystem = EntMan.System<SharedIdCardConsoleSystem>();
+        }
+
+        protected override void Open()
+        {
+            base.Open();
+            List<ProtoId<AccessLevelPrototype>> accessLevels;
+
+            if (EntMan.TryGetComponent<IdCardConsoleComponent>(Owner, out var idCard))
+            {
+                accessLevels = idCard.AccessLevels;
+            }
+            else
+            {
+                accessLevels = new List<ProtoId<AccessLevelPrototype>>();
+                _idCardConsoleSystem.Log.Error($"No IdCardConsole component found for {EntMan.ToPrettyString(Owner)}!");
+            }
+
+            _window = new IdCardConsoleWindow(this, _prototypeManager, accessLevels)
+            {
+                Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName
+            };
+
+            _window.CrewManifestButton.OnPressed += _ => SendMessage(new CrewManifestOpenUiMessage());
+            _window.PrivilegedIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(PrivilegedIdCardSlotId));
+            _window.TargetIdButton.OnPressed += _ => SendMessage(new ItemSlotButtonPressedEvent(TargetIdCardSlotId));
+
+            _window.OnClose += Close;
+            _window.OpenCentered();
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            base.Dispose(disposing);
+            if (!disposing)
+                return;
+
+            _window?.Dispose();
+        }
+
+        protected override void UpdateState(BoundUserInterfaceState state)
+        {
+            base.UpdateState(state);
+            var castState = (IdCardConsoleBoundUserInterfaceState) state;
+            _window?.UpdateState(castState);
+        }
+
+        public void SubmitData(string newFullName, string newJobTitle, List<ProtoId<AccessLevelPrototype>> newAccessList, string newJobPrototype)
+        {
+            if (newFullName.Length > MaxFullNameLength)
+                newFullName = newFullName[..MaxFullNameLength];
+
+            if (newJobTitle.Length > MaxJobTitleLength)
+                newJobTitle = newJobTitle[..MaxJobTitleLength];
+
+            SendMessage(new WriteToTargetIdMessage(
+                newFullName,
+                newJobTitle,
+                newAccessList,
+                newJobPrototype));
+        }
+    }
+}

+ 35 - 0
Content.Client/Access/UI/IdCardConsoleWindow.xaml

@@ -0,0 +1,35 @@
+<DefaultWindow xmlns="https://spacestation14.io"
+            MinSize="650 290">
+    <BoxContainer Orientation="Vertical">
+        <GridContainer Columns="2">
+            <GridContainer Columns="3" HorizontalExpand="True">
+                <Label Text="{Loc 'id-card-console-window-privileged-id'}" />
+                <Button Name="PrivilegedIdButton" Access="Public"/>
+                <Label Name="PrivilegedIdLabel" />
+
+                <Label Text="{Loc 'id-card-console-window-target-id'}" />
+                <Button Name="TargetIdButton" Access="Public"/>
+                <Label Name="TargetIdLabel" />
+            </GridContainer>
+            <BoxContainer Orientation="Vertical">
+                <Button Name="CrewManifestButton" Access="Public" Text="{Loc 'crew-manifest-button-label'}" />
+            </BoxContainer>
+        </GridContainer>
+        <Control MinSize="0 8" />
+        <GridContainer Columns="3" HSeparationOverride="4">
+            <Label Name="FullNameLabel" Text="{Loc 'id-card-console-window-full-name-label'}" />
+            <LineEdit Name="FullNameLineEdit" HorizontalExpand="True" />
+            <Button Name="FullNameSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" />
+
+            <Label Name="JobTitleLabel" Text="{Loc 'id-card-console-window-job-title-label'}" />
+            <LineEdit Name="JobTitleLineEdit" HorizontalExpand="True" />
+            <Button Name="JobTitleSaveButton" Text="{Loc 'id-card-console-window-save-button'}" Disabled="True" />
+        </GridContainer>
+        <Control MinSize="0 8" />
+        <GridContainer Columns="2">
+            <Label Text="{Loc 'id-card-console-window-job-selection-label'}" />
+            <OptionButton Name="JobPresetOptionButton" />
+        </GridContainer>
+        <Control Name="AccessLevelControlContainer" />
+    </BoxContainer>
+</DefaultWindow>

+ 205 - 0
Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs

@@ -0,0 +1,205 @@
+using System.Linq;
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using static Content.Shared.Access.Components.IdCardConsoleComponent;
+
+namespace Content.Client.Access.UI
+{
+    [GenerateTypedNameReferences]
+    public sealed partial class IdCardConsoleWindow : DefaultWindow
+    {
+        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+        [Dependency] private readonly ILogManager _logManager = default!;
+        private readonly ISawmill _logMill = default!;
+
+        private readonly IdCardConsoleBoundUserInterface _owner;
+
+        private AccessLevelControl _accessButtons = new();
+        private readonly List<string> _jobPrototypeIds = new();
+
+        private string? _lastFullName;
+        private string? _lastJobTitle;
+        private string? _lastJobProto;
+
+        // The job that will be picked if the ID doesn't have a job on the station.
+        private static ProtoId<JobPrototype> _defaultJob = "Passenger";
+
+        public IdCardConsoleWindow(IdCardConsoleBoundUserInterface owner, IPrototypeManager prototypeManager,
+            List<ProtoId<AccessLevelPrototype>> accessLevels)
+        {
+            RobustXamlLoader.Load(this);
+            IoCManager.InjectDependencies(this);
+            _logMill = _logManager.GetSawmill(SharedIdCardConsoleSystem.Sawmill);
+
+            _owner = owner;
+
+            FullNameLineEdit.OnTextEntered += _ => SubmitData();
+            FullNameLineEdit.OnTextChanged += _ =>
+            {
+                FullNameSaveButton.Disabled = FullNameSaveButton.Text == _lastFullName;
+            };
+            FullNameSaveButton.OnPressed += _ => SubmitData();
+
+            JobTitleLineEdit.OnTextEntered += _ => SubmitData();
+            JobTitleLineEdit.OnTextChanged += _ =>
+            {
+                JobTitleSaveButton.Disabled = JobTitleLineEdit.Text == _lastJobTitle;
+            };
+            JobTitleSaveButton.OnPressed += _ => SubmitData();
+
+            var jobs = _prototypeManager.EnumeratePrototypes<JobPrototype>().ToList();
+            jobs.Sort((x, y) => string.Compare(x.LocalizedName, y.LocalizedName, StringComparison.CurrentCulture));
+
+            foreach (var job in jobs)
+            {
+                if (!job.OverrideConsoleVisibility.GetValueOrDefault(job.SetPreference))
+                {
+                    continue;
+                }
+
+                _jobPrototypeIds.Add(job.ID);
+                JobPresetOptionButton.AddItem(Loc.GetString(job.Name), _jobPrototypeIds.Count - 1);
+            }
+
+            JobPresetOptionButton.OnItemSelected += SelectJobPreset;
+            _accessButtons.Populate(accessLevels, prototypeManager);
+            AccessLevelControlContainer.AddChild(_accessButtons);
+
+            foreach (var (id, button) in _accessButtons.ButtonsList)
+            {
+                button.OnPressed += _ => SubmitData();
+            }
+        }
+
+        private void ClearAllAccess()
+        {
+            foreach (var button in _accessButtons.ButtonsList.Values)
+            {
+                if (button.Pressed)
+                {
+                    button.Pressed = false;
+                }
+            }
+        }
+
+        private void SelectJobPreset(OptionButton.ItemSelectedEventArgs args)
+        {
+            if (!_prototypeManager.TryIndex(_jobPrototypeIds[args.Id], out JobPrototype? job))
+            {
+                return;
+            }
+
+            JobTitleLineEdit.Text = Loc.GetString(job.Name);
+            args.Button.SelectId(args.Id);
+
+            ClearAllAccess();
+
+            // this is a sussy way to do this
+            foreach (var access in job.Access)
+            {
+                if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
+                {
+                    button.Pressed = true;
+                }
+            }
+
+            foreach (var group in job.AccessGroups)
+            {
+                if (!_prototypeManager.TryIndex(group, out AccessGroupPrototype? groupPrototype))
+                {
+                    continue;
+                }
+
+                foreach (var access in groupPrototype.Tags)
+                {
+                    if (_accessButtons.ButtonsList.TryGetValue(access, out var button) && !button.Disabled)
+                    {
+                        button.Pressed = true;
+                    }
+                }
+            }
+
+            SubmitData();
+        }
+
+        public void UpdateState(IdCardConsoleBoundUserInterfaceState state)
+        {
+            PrivilegedIdButton.Text = state.IsPrivilegedIdPresent
+                ? Loc.GetString("id-card-console-window-eject-button")
+                : Loc.GetString("id-card-console-window-insert-button");
+
+            PrivilegedIdLabel.Text = state.PrivilegedIdName;
+
+            TargetIdButton.Text = state.IsTargetIdPresent
+                ? Loc.GetString("id-card-console-window-eject-button")
+                : Loc.GetString("id-card-console-window-insert-button");
+
+            TargetIdLabel.Text = state.TargetIdName;
+
+            var interfaceEnabled =
+                state.IsPrivilegedIdPresent && state.IsPrivilegedIdAuthorized && state.IsTargetIdPresent;
+
+            var fullNameDirty = _lastFullName != null && FullNameLineEdit.Text != state.TargetIdFullName;
+            var jobTitleDirty = _lastJobTitle != null && JobTitleLineEdit.Text != state.TargetIdJobTitle;
+
+            FullNameLabel.Modulate = interfaceEnabled ? Color.White : Color.Gray;
+            FullNameLineEdit.Editable = interfaceEnabled;
+            if (!fullNameDirty)
+            {
+                FullNameLineEdit.Text = state.TargetIdFullName ?? string.Empty;
+            }
+
+            FullNameSaveButton.Disabled = !interfaceEnabled || !fullNameDirty;
+
+            JobTitleLabel.Modulate = interfaceEnabled ? Color.White : Color.Gray;
+            JobTitleLineEdit.Editable = interfaceEnabled;
+            if (!jobTitleDirty)
+            {
+                JobTitleLineEdit.Text = state.TargetIdJobTitle ?? string.Empty;
+            }
+
+            JobTitleSaveButton.Disabled = !interfaceEnabled || !jobTitleDirty;
+
+            JobPresetOptionButton.Disabled = !interfaceEnabled;
+
+            _accessButtons.UpdateState(state.TargetIdAccessList?.ToList() ??
+                                       new List<ProtoId<AccessLevelPrototype>>(),
+                                       state.AllowedModifyAccessList?.ToList() ??
+                                       new List<ProtoId<AccessLevelPrototype>>());
+
+            var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
+            // If the job index is < 0 that means they don't have a job registered in the station records.
+            // For example, a new ID from a box would have no job index.
+            if (jobIndex < 0)
+            {
+                jobIndex = _jobPrototypeIds.IndexOf(_defaultJob);
+            }
+
+            JobPresetOptionButton.SelectId(jobIndex);
+
+            _lastFullName = state.TargetIdFullName;
+            _lastJobTitle = state.TargetIdJobTitle;
+            _lastJobProto = state.TargetIdJobPrototype;
+        }
+
+        private void SubmitData()
+        {
+            // Don't send this if it isn't dirty.
+            var jobProtoDirty = _lastJobProto != null &&
+                                _jobPrototypeIds[JobPresetOptionButton.SelectedId] != _lastJobProto;
+
+            _owner.SubmitData(
+                FullNameLineEdit.Text,
+                JobTitleLineEdit.Text,
+                // Iterate over the buttons dictionary, filter by `Pressed`, only get key from the key/value pair
+                _accessButtons.ButtonsList.Where(x => x.Value.Pressed).Select(x => x.Key).ToList(),
+                jobProtoDirty ? _jobPrototypeIds[JobPresetOptionButton.SelectedId] : string.Empty);
+        }
+    }
+}

+ 9 - 0
Content.Client/Actions/ActionEvents.cs

@@ -0,0 +1,9 @@
+namespace Content.Client.Actions;
+
+/// <summary>
+///     This event is raised when a user clicks on an empty action slot. Enables other systems to fill this slot.
+/// </summary>
+public sealed class FillActionSlotEvent : EntityEventArgs
+{
+    public EntityUid? Action;
+}

+ 356 - 0
Content.Client/Actions/ActionsSystem.cs

@@ -0,0 +1,356 @@
+using System.IO;
+using System.Linq;
+using Content.Shared.Actions;
+using JetBrains.Annotations;
+using Robust.Client.Player;
+using Robust.Shared.ContentPack;
+using Robust.Shared.GameStates;
+using Robust.Shared.Input.Binding;
+using Robust.Shared.Player;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.Markdown;
+using Robust.Shared.Serialization.Markdown.Mapping;
+using Robust.Shared.Serialization.Markdown.Sequence;
+using Robust.Shared.Serialization.Markdown.Value;
+using Robust.Shared.Utility;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Client.Actions
+{
+    [UsedImplicitly]
+    public sealed class ActionsSystem : SharedActionsSystem
+    {
+        public delegate void OnActionReplaced(EntityUid actionId);
+
+        [Dependency] private readonly IPlayerManager _playerManager = default!;
+        [Dependency] private readonly IResourceManager _resources = default!;
+        [Dependency] private readonly ISerializationManager _serialization = default!;
+        [Dependency] private readonly MetaDataSystem _metaData = default!;
+
+        public event Action<EntityUid>? OnActionAdded;
+        public event Action<EntityUid>? OnActionRemoved;
+        public event Action? ActionsUpdated;
+        public event Action<ActionsComponent>? LinkActions;
+        public event Action? UnlinkActions;
+        public event Action? ClearAssignments;
+        public event Action<List<SlotAssignment>>? AssignSlot;
+
+        private readonly List<EntityUid> _removed = new();
+        private readonly List<(EntityUid, BaseActionComponent?)> _added = new();
+
+        public override void Initialize()
+        {
+            base.Initialize();
+            SubscribeLocalEvent<ActionsComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
+            SubscribeLocalEvent<ActionsComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
+            SubscribeLocalEvent<ActionsComponent, ComponentHandleState>(HandleComponentState);
+
+            SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
+            SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
+            SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
+            SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
+        }
+
+        public override void FrameUpdate(float frameTime)
+        {
+            base.FrameUpdate(frameTime);
+
+            var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
+            while (worldActionQuery.MoveNext(out var uid, out var action))
+            {
+                UpdateAction(uid, action);
+            }
+
+            var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
+            while (instantActionQuery.MoveNext(out var uid, out var action))
+            {
+                UpdateAction(uid, action);
+            }
+
+            var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
+            while (entityActionQuery.MoveNext(out var uid, out var action))
+            {
+                UpdateAction(uid, action);
+            }
+        }
+
+        private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
+        {
+            if (args.Current is not InstantActionComponentState state)
+                return;
+
+            BaseHandleState<InstantActionComponent>(uid, component, state);
+        }
+
+        private void OnEntityTargetHandleState(EntityUid uid, EntityTargetActionComponent component, ref ComponentHandleState args)
+        {
+            if (args.Current is not EntityTargetActionComponentState state)
+                return;
+
+            component.Whitelist = state.Whitelist;
+            component.Blacklist = state.Blacklist;
+            component.CanTargetSelf = state.CanTargetSelf;
+            BaseHandleState<EntityTargetActionComponent>(uid, component, state);
+        }
+
+        private void OnWorldTargetHandleState(EntityUid uid, WorldTargetActionComponent component, ref ComponentHandleState args)
+        {
+            if (args.Current is not WorldTargetActionComponentState state)
+                return;
+
+            BaseHandleState<WorldTargetActionComponent>(uid, component, state);
+        }
+
+        private void OnEntityWorldTargetHandleState(EntityUid uid,
+            EntityWorldTargetActionComponent component,
+            ref ComponentHandleState args)
+        {
+            if (args.Current is not EntityWorldTargetActionComponentState state)
+                return;
+
+            component.Whitelist = state.Whitelist;
+            component.CanTargetSelf = state.CanTargetSelf;
+            BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state);
+        }
+
+        private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
+        {
+            // TODO ACTIONS use auto comp states
+            component.Icon = state.Icon;
+            component.IconOn = state.IconOn;
+            component.IconColor = state.IconColor;
+            component.OriginalIconColor = state.OriginalIconColor;
+            component.DisabledIconColor = state.DisabledIconColor;
+            component.Keywords.Clear();
+            component.Keywords.UnionWith(state.Keywords);
+            component.Enabled = state.Enabled;
+            component.Toggled = state.Toggled;
+            component.Cooldown = state.Cooldown;
+            component.UseDelay = state.UseDelay;
+            component.Charges = state.Charges;
+            component.MaxCharges = state.MaxCharges;
+            component.RenewCharges = state.RenewCharges;
+            component.Container = EnsureEntity<T>(state.Container, uid);
+            component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
+            component.CheckCanInteract = state.CheckCanInteract;
+            component.CheckConsciousness = state.CheckConsciousness;
+            component.ClientExclusive = state.ClientExclusive;
+            component.Priority = state.Priority;
+            component.AttachedEntity = EnsureEntity<T>(state.AttachedEntity, uid);
+            component.RaiseOnUser = state.RaiseOnUser;
+            component.RaiseOnAction = state.RaiseOnAction;
+            component.AutoPopulate = state.AutoPopulate;
+            component.Temporary = state.Temporary;
+            component.ItemIconStyle = state.ItemIconStyle;
+            component.Sound = state.Sound;
+
+            UpdateAction(uid, component);
+        }
+
+        public override void UpdateAction(EntityUid? actionId, BaseActionComponent? action = null)
+        {
+            if (!ResolveActionData(actionId, ref action))
+                return;
+
+            action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor;
+
+            base.UpdateAction(actionId, action);
+            if (_playerManager.LocalEntity != action.AttachedEntity)
+                return;
+
+            ActionsUpdated?.Invoke();
+        }
+
+        private void HandleComponentState(EntityUid uid, ActionsComponent component, ref ComponentHandleState args)
+        {
+            if (args.Current is not ActionsComponentState state)
+                return;
+
+            _added.Clear();
+            _removed.Clear();
+            var stateEnts = EnsureEntitySet<ActionsComponent>(state.Actions, uid);
+            foreach (var act in component.Actions)
+            {
+                if (!stateEnts.Contains(act) && !IsClientSide(act))
+                    _removed.Add(act);
+            }
+            component.Actions.ExceptWith(_removed);
+
+            foreach (var actionId in stateEnts)
+            {
+                if (!actionId.IsValid())
+                    continue;
+
+                if (!component.Actions.Add(actionId))
+                    continue;
+
+                TryGetActionData(actionId, out var action);
+                _added.Add((actionId, action));
+            }
+
+            if (_playerManager.LocalEntity != uid)
+                return;
+
+            foreach (var action in _removed)
+            {
+                OnActionRemoved?.Invoke(action);
+            }
+
+            _added.Sort(ActionComparer);
+
+            foreach (var action in _added)
+            {
+                OnActionAdded?.Invoke(action.Item1);
+            }
+
+            ActionsUpdated?.Invoke();
+        }
+
+        public static int ActionComparer((EntityUid, BaseActionComponent?) a, (EntityUid, BaseActionComponent?) b)
+        {
+            var priorityA = a.Item2?.Priority ?? 0;
+            var priorityB = b.Item2?.Priority ?? 0;
+            if (priorityA != priorityB)
+                return priorityA - priorityB;
+
+            priorityA = a.Item2?.Container?.Id ?? 0;
+            priorityB = b.Item2?.Container?.Id ?? 0;
+            return priorityA - priorityB;
+        }
+
+        protected override void ActionAdded(EntityUid performer, EntityUid actionId, ActionsComponent comp,
+            BaseActionComponent action)
+        {
+            if (_playerManager.LocalEntity != performer)
+                return;
+
+            OnActionAdded?.Invoke(actionId);
+        }
+
+        protected override void ActionRemoved(EntityUid performer, EntityUid actionId, ActionsComponent comp, BaseActionComponent action)
+        {
+            if (_playerManager.LocalEntity != performer)
+                return;
+
+            OnActionRemoved?.Invoke(actionId);
+        }
+
+        public IEnumerable<(EntityUid Id, BaseActionComponent Comp)> GetClientActions()
+        {
+            if (_playerManager.LocalEntity is not { } user)
+                return Enumerable.Empty<(EntityUid, BaseActionComponent)>();
+
+            return GetActions(user);
+        }
+
+        private void OnPlayerAttached(EntityUid uid, ActionsComponent component, LocalPlayerAttachedEvent args)
+        {
+            LinkAllActions(component);
+        }
+
+        private void OnPlayerDetached(EntityUid uid, ActionsComponent component, LocalPlayerDetachedEvent? args = null)
+        {
+            UnlinkAllActions();
+        }
+
+        public void UnlinkAllActions()
+        {
+            UnlinkActions?.Invoke();
+        }
+
+        public void LinkAllActions(ActionsComponent? actions = null)
+        {
+            if (_playerManager.LocalEntity is not { } user ||
+                !Resolve(user, ref actions, false))
+            {
+                return;
+            }
+
+            LinkActions?.Invoke(actions);
+        }
+
+        public override void Shutdown()
+        {
+            base.Shutdown();
+            CommandBinds.Unregister<ActionsSystem>();
+        }
+
+        public void TriggerAction(EntityUid actionId, BaseActionComponent action)
+        {
+            if (_playerManager.LocalEntity is not { } user ||
+                !TryComp(user, out ActionsComponent? actions))
+            {
+                return;
+            }
+
+            if (action is not InstantActionComponent instantAction)
+                return;
+
+            if (action.ClientExclusive)
+            {
+                PerformAction(user, actions, actionId, instantAction, instantAction.Event, GameTiming.CurTime);
+            }
+            else
+            {
+                var request = new RequestPerformActionEvent(GetNetEntity(actionId));
+                EntityManager.RaisePredictiveEvent(request);
+            }
+        }
+
+        /// <summary>
+        ///     Load actions and their toolbar assignments from a file.
+        /// </summary>
+        public void LoadActionAssignments(string path, bool userData)
+        {
+            if (_playerManager.LocalEntity is not { } user)
+                return;
+
+            var file = new ResPath(path).ToRootedPath();
+            TextReader reader = userData
+                ? _resources.UserData.OpenText(file)
+                : _resources.ContentFileReadText(file);
+
+            var yamlStream = new YamlStream();
+            yamlStream.Load(reader);
+
+            if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
+                return;
+
+            ClearAssignments?.Invoke();
+
+            var assignments = new List<SlotAssignment>();
+
+            foreach (var entry in sequence.Sequence)
+            {
+                if (entry is not MappingDataNode map)
+                    continue;
+
+                if (!map.TryGet("action", out var actionNode))
+                    continue;
+
+                var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
+                var actionId = Spawn();
+                AddComp(actionId, action);
+                AddActionDirect(user, actionId);
+
+                if (map.TryGet<ValueDataNode>("name", out var nameNode))
+                    _metaData.SetEntityName(actionId, nameNode.Value);
+
+                if (!map.TryGet("assignments", out var assignmentNode))
+                    continue;
+
+                var nodeAssignments = _serialization.Read<List<(byte Hotbar, byte Slot)>>(assignmentNode, notNullableOverride: true);
+
+                foreach (var index in nodeAssignments)
+                {
+                    var assignment = new SlotAssignment(index.Hotbar, index.Slot, actionId);
+                    assignments.Add(assignment);
+                }
+            }
+
+            AssignSlot?.Invoke(assignments);
+        }
+
+        public record struct SlotAssignment(byte Hotbar, byte Slot, EntityUid ActionId);
+    }
+}

+ 116 - 0
Content.Client/Actions/UI/ActionAlertTooltip.cs

@@ -0,0 +1,116 @@
+using Content.Client.Stylesheets;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+using static Robust.Client.UserInterface.Controls.BoxContainer;
+
+namespace Content.Client.Actions.UI
+{
+    /// <summary>
+    /// Tooltip for actions or alerts because they are very similar.
+    /// </summary>
+    public sealed class ActionAlertTooltip : PanelContainer
+    {
+        private const float TooltipTextMaxWidth = 350;
+
+        private readonly RichTextLabel _cooldownLabel;
+        private readonly IGameTiming _gameTiming;
+
+        /// <summary>
+        /// Current cooldown displayed in this tooltip. Set to null to show no cooldown.
+        /// </summary>
+        public (TimeSpan Start, TimeSpan End)? Cooldown { get; set; }
+
+        public ActionAlertTooltip(FormattedMessage name, FormattedMessage? desc, string? requires = null, FormattedMessage? charges = null)
+        {
+            _gameTiming = IoCManager.Resolve<IGameTiming>();
+
+            SetOnlyStyleClass(StyleNano.StyleClassTooltipPanel);
+
+            BoxContainer vbox;
+            AddChild(vbox = new BoxContainer
+            {
+                Orientation = LayoutOrientation.Vertical,
+                RectClipContent = true
+            });
+            var nameLabel = new RichTextLabel
+            {
+                MaxWidth = TooltipTextMaxWidth,
+                StyleClasses = {StyleNano.StyleClassTooltipActionTitle}
+            };
+            nameLabel.SetMessage(name);
+            vbox.AddChild(nameLabel);
+
+            if (desc != null && !string.IsNullOrWhiteSpace(desc.ToString()))
+            {
+                var description = new RichTextLabel
+                {
+                    MaxWidth = TooltipTextMaxWidth,
+                    StyleClasses = {StyleNano.StyleClassTooltipActionDescription}
+                };
+                description.SetMessage(desc);
+                vbox.AddChild(description);
+            }
+
+            if (charges != null && !string.IsNullOrWhiteSpace(charges.ToString()))
+            {
+                var chargesLabel = new RichTextLabel
+                {
+                    MaxWidth = TooltipTextMaxWidth,
+                    StyleClasses = { StyleNano.StyleClassTooltipActionCharges }
+                };
+                chargesLabel.SetMessage(charges);
+                vbox.AddChild(chargesLabel);
+            }
+
+            vbox.AddChild(_cooldownLabel = new RichTextLabel
+            {
+                MaxWidth = TooltipTextMaxWidth,
+                StyleClasses = {StyleNano.StyleClassTooltipActionCooldown},
+                Visible = false
+            });
+
+            if (!string.IsNullOrWhiteSpace(requires))
+            {
+                var requiresLabel = new RichTextLabel
+                {
+                    MaxWidth = TooltipTextMaxWidth,
+                    StyleClasses = {StyleNano.StyleClassTooltipActionRequirements}
+                };
+
+                if (!FormattedMessage.TryFromMarkup("[color=#635c5c]" + requires + "[/color]", out var markup))
+                    return;
+
+                requiresLabel.SetMessage(markup);
+
+                vbox.AddChild(requiresLabel);
+            }
+        }
+
+        protected override void FrameUpdate(FrameEventArgs args)
+        {
+            base.FrameUpdate(args);
+            if (!Cooldown.HasValue)
+            {
+                _cooldownLabel.Visible = false;
+                return;
+            }
+
+            var timeLeft = Cooldown.Value.End - _gameTiming.CurTime;
+            if (timeLeft > TimeSpan.Zero)
+            {
+                var duration = Cooldown.Value.End - Cooldown.Value.Start;
+
+                if (!FormattedMessage.TryFromMarkup(Loc.GetString("ui-actionslot-duration", ("duration", (int)duration.TotalSeconds), ("timeLeft", (int)timeLeft.TotalSeconds + 1)), out var markup))
+                    return;
+
+                _cooldownLabel.SetMessage(markup);
+                _cooldownLabel.Visible = true;
+            }
+            else
+            {
+                _cooldownLabel.Visible = false;
+            }
+        }
+    }
+}

+ 121 - 0
Content.Client/Administration/AdminNameOverlay.cs

@@ -0,0 +1,121 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.Administration.Systems;
+using Content.Shared.CCVar;
+using Content.Shared.Mind;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Shared.Configuration;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Administration;
+
+internal sealed class AdminNameOverlay : Overlay
+{
+    [Dependency] private readonly IConfigurationManager _config = default!;
+
+    private readonly AdminSystem _system;
+    private readonly IEntityManager _entityManager;
+    private readonly IEyeManager _eyeManager;
+    private readonly EntityLookupSystem _entityLookup;
+    private readonly IUserInterfaceManager _userInterfaceManager;
+    private readonly Font _font;
+
+    //TODO make this adjustable via GUI
+    private readonly ProtoId<RoleTypePrototype>[] _filter =
+        ["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
+    private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
+    private readonly Color _antagColorClassic = Color.OrangeRed;
+
+    public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager)
+    {
+        IoCManager.InjectDependencies(this);
+
+        _system = system;
+        _entityManager = entityManager;
+        _eyeManager = eyeManager;
+        _entityLookup = entityLookup;
+        _userInterfaceManager = userInterfaceManager;
+        ZIndex = 200;
+        _font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
+    }
+
+    public override OverlaySpace Space => OverlaySpace.ScreenSpace;
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        var viewport = args.WorldAABB;
+
+        //TODO make this adjustable via GUI
+        var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
+        var playTime = _config.GetCVar(CCVars.AdminOverlayPlaytime);
+        var startingJob = _config.GetCVar(CCVars.AdminOverlayStartingJob);
+
+        foreach (var playerInfo in _system.PlayerList)
+        {
+            var entity = _entityManager.GetEntity(playerInfo.NetEntity);
+
+            // Otherwise the entity can not exist yet
+            if (entity == null || !_entityManager.EntityExists(entity))
+            {
+                continue;
+            }
+
+            // if not on the same map, continue
+            if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
+            {
+                continue;
+            }
+
+            var aabb = _entityLookup.GetWorldAABB(entity.Value);
+
+            // if not on screen, continue
+            if (!aabb.Intersects(in viewport))
+            {
+                continue;
+            }
+
+            var uiScale = _userInterfaceManager.RootControl.UIScale;
+            var lineoffset = new Vector2(0f, 14f) * uiScale;
+            var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
+                                                              new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
+                                                                  aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
+
+            var currentOffset = Vector2.Zero;
+
+            args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
+            currentOffset += lineoffset;
+
+            args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
+            currentOffset += lineoffset;
+
+            if (!string.IsNullOrEmpty(playerInfo.PlaytimeString) && playTime)
+            {
+                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, playerInfo.PlaytimeString, uiScale, playerInfo.Connected ? Color.Orange : Color.White);
+                currentOffset += lineoffset;
+            }
+
+            if (!string.IsNullOrEmpty(playerInfo.StartingJob) && startingJob)
+            {
+                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, Loc.GetString(playerInfo.StartingJob), uiScale, playerInfo.Connected ? Color.GreenYellow : Color.White);
+                currentOffset += lineoffset;
+            }
+
+            if (classic && playerInfo.Antag)
+            {
+                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, _antagLabelClassic, uiScale, Color.OrangeRed);
+                currentOffset += lineoffset;
+            }
+            else if (!classic && _filter.Contains(playerInfo.RoleProto))
+            {
+                var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
+                var color = playerInfo.RoleProto.Color;
+
+                args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, label, uiScale, color);
+                currentOffset += lineoffset;
+            }
+        }
+    }
+}

+ 10 - 0
Content.Client/Administration/Components/HeadstandComponent.cs

@@ -0,0 +1,10 @@
+using Content.Shared.Administration.Components;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Administration.Components;
+
+[RegisterComponent]
+public sealed partial class HeadstandComponent : SharedHeadstandComponent
+{
+
+}

+ 7 - 0
Content.Client/Administration/Components/KillSignComponent.cs

@@ -0,0 +1,7 @@
+using Content.Shared.Administration.Components;
+using Robust.Shared.GameStates;
+
+namespace Content.Client.Administration.Components;
+
+[RegisterComponent]
+public sealed partial class KillSignComponent : SharedKillSignComponent;

+ 151 - 0
Content.Client/Administration/Managers/ClientAdminManager.cs

@@ -0,0 +1,151 @@
+using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
+using Robust.Client.Console;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.Managers
+{
+    public sealed class ClientAdminManager : IClientAdminManager, IClientConGroupImplementation, IPostInjectInit, ISharedAdminManager
+    {
+        [Dependency] private readonly IPlayerManager _player = default!;
+        [Dependency] private readonly IClientNetManager _netMgr = default!;
+        [Dependency] private readonly IClientConGroupController _conGroup = default!;
+        [Dependency] private readonly IResourceManager _res = default!;
+        [Dependency] private readonly ILogManager _logManager = default!;
+        [Dependency] private readonly IUserInterfaceManager _userInterface = default!;
+
+        private AdminData? _adminData;
+        private readonly HashSet<string> _availableCommands = new();
+
+        private readonly AdminCommandPermissions _localCommandPermissions = new();
+        private ISawmill _sawmill = default!;
+
+        public event Action? AdminStatusUpdated;
+
+        public bool IsActive()
+        {
+            return _adminData?.Active ?? false;
+        }
+
+        public bool HasFlag(AdminFlags flag)
+        {
+            return _adminData?.HasFlag(flag) ?? false;
+        }
+
+        public bool CanCommand(string cmdName)
+        {
+            if (_adminData != null && _adminData.HasFlag(AdminFlags.Host))
+            {
+                // Host can execute all commands when connected.
+                // Kind of a shortcut to avoid pains during development.
+                return true;
+            }
+
+            if (_localCommandPermissions.CanCommand(cmdName, _adminData))
+                return true;
+
+            return _availableCommands.Contains(cmdName);
+        }
+
+        public bool CanViewVar()
+        {
+            return CanCommand("vv");
+        }
+
+        public bool CanAdminPlace()
+        {
+            return _adminData?.CanAdminPlace() ?? false;
+        }
+
+        public bool CanScript()
+        {
+            return _adminData?.CanScript() ?? false;
+        }
+
+        public bool CanAdminMenu()
+        {
+            return _adminData?.CanAdminMenu() ?? false;
+        }
+
+        public void Initialize()
+        {
+            _netMgr.RegisterNetMessage<MsgUpdateAdminStatus>(UpdateMessageRx);
+
+            // Load flags for engine commands, since those don't have the attributes.
+            if (_res.TryContentFileRead(new ResPath("/clientCommandPerms.yml"), out var efs))
+            {
+                _localCommandPermissions.LoadPermissionsFromStream(efs);
+            }
+        }
+
+        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)
+            {
+                if (Attribute.GetCustomAttribute(instance.GetType(), typeof(AnyCommandAttribute)) == null) continue;
+                _availableCommands.Add(command);
+            }
+
+            _availableCommands.UnionWith(message.AvailableCommands);
+            _sawmill.Debug($"Have {message.AvailableCommands.Length} commands available");
+
+            _adminData = message.Admin;
+            if (_adminData != null)
+            {
+                var flagsText = string.Join("|", AdminFlagsHelper.FlagsToNames(_adminData.Flags));
+                _sawmill.Info($"Updated admin status: {_adminData.Active}/{_adminData.Title}/{flagsText}");
+
+                if (_adminData.Active)
+                    _userInterface.DebugMonitors.SetMonitor(DebugMonitor.Coords, true);
+            }
+            else
+            {
+                _sawmill.Info("Updated admin status: Not admin");
+            }
+
+            AdminStatusUpdated?.Invoke();
+            ConGroupUpdated?.Invoke();
+        }
+
+        public event Action? ConGroupUpdated;
+
+        void IPostInjectInit.PostInject()
+        {
+            _conGroup.Implementation = this;
+            _sawmill = _logManager.GetSawmill("admin");
+        }
+
+        public AdminData? GetAdminData(EntityUid uid, bool includeDeAdmin = false)
+        {
+            if (uid == _player.LocalEntity && (_adminData?.Active ?? includeDeAdmin))
+                return _adminData;
+
+            return null;
+        }
+
+        public AdminData? GetAdminData(ICommonSession session, bool includeDeAdmin = false)
+        {
+            if (_player.LocalUser == session.UserId && (_adminData?.Active ?? includeDeAdmin))
+                return _adminData;
+
+            return null;
+        }
+
+        public AdminData? GetAdminData(bool includeDeAdmin = false)
+        {
+            if (_player.LocalSession is { } session)
+                return GetAdminData(session, includeDeAdmin);
+
+            return null;
+        }
+    }
+}

+ 76 - 0
Content.Client/Administration/Managers/IClientAdminManager.cs

@@ -0,0 +1,76 @@
+using Content.Shared.Administration;
+
+namespace Content.Client.Administration.Managers
+{
+    /// <summary>
+    ///     Manages server admin permissions for the local player.
+    /// </summary>
+    public interface IClientAdminManager
+    {
+        /// <summary>
+        ///     Fired when the admin status of the local player changes, such as losing admin privileges.
+        /// </summary>
+        event Action AdminStatusUpdated;
+
+        /// <summary>
+        ///     Gets the admin data for the client, if they are an admin.
+        /// </summary>
+        /// <param name="includeDeAdmin">
+        ///     Whether to return admin data for admins that are current de-adminned.
+        /// </param>
+        /// <returns><see langword="null" /> if the player is not an admin.</returns>
+        AdminData? GetAdminData(bool includeDeAdmin = false);
+
+        /// <summary>
+        ///     Checks whether the local player is an admin.
+        /// </summary>
+        /// <returns>true if the local player is an admin, false otherwise even if they are deadminned.</returns>
+        bool IsActive();
+
+        /// <summary>
+        ///     Checks whether the local player has an admin flag.
+        /// </summary>
+        /// <param name="flag">The flags to check. Multiple flags can be specified, they must all be held.</param>
+        /// <returns>False if the local player is not an admin, inactive, or does not have all the flags specified.</returns>
+        bool HasFlag(AdminFlags flag);
+
+        /// <summary>
+        ///     Check if a player can execute a specified console command.
+        /// </summary>
+        bool CanCommand(string cmdName);
+
+        /// <summary>
+        ///     Check if the local player can open the VV menu.
+        /// </summary>
+        bool CanViewVar();
+
+        /// <summary>
+        ///     Check if the local player can spawn stuff in with the entity/tile spawn panel.
+        /// </summary>
+        bool CanAdminPlace();
+
+        /// <summary>
+        ///     Check if the local player can execute server-side C# scripts.
+        /// </summary>
+        bool CanScript();
+
+        /// <summary>
+        ///     Check if the local player can open the admin menu.
+        /// </summary>
+        bool CanAdminMenu();
+
+        void Initialize();
+
+        /// <summary>
+        ///     Checks if the client is an admin.
+        /// </summary>
+        /// <param name="includeDeAdmin">
+        ///     Whether to return admin data for admins that are current de-adminned.
+        /// </param>
+        /// <returns>true if the player is an admin, false otherwise.</returns>
+        bool IsAdmin(bool includeDeAdmin = false)
+        {
+            return GetAdminData(includeDeAdmin) != null;
+        }
+    }
+}

+ 37 - 0
Content.Client/Administration/QuickDialogSystem.cs

@@ -0,0 +1,37 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Administration;
+
+namespace Content.Client.Administration;
+
+/// <summary>
+/// This handles the client portion of quick dialogs.
+/// </summary>
+public sealed class QuickDialogSystem : EntitySystem
+{
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeNetworkEvent<QuickDialogOpenEvent>(OpenDialog);
+    }
+
+    private void OpenDialog(QuickDialogOpenEvent ev)
+    {
+        var ok = (ev.Buttons & QuickDialogButtonFlag.OkButton) != 0;
+        var cancel = (ev.Buttons & QuickDialogButtonFlag.CancelButton) != 0;
+        var window = new DialogWindow(ev.Title, ev.Prompts, ok: ok, cancel: cancel);
+
+        window.OnConfirmed += responses =>
+        {
+            RaiseNetworkEvent(new QuickDialogResponseEvent(ev.DialogId,
+                responses,
+                QuickDialogButtonFlag.OkButton));
+        };
+
+        window.OnCancelled += () =>
+        {
+            RaiseNetworkEvent(new QuickDialogResponseEvent(ev.DialogId,
+                new(),
+                QuickDialogButtonFlag.CancelButton));
+        };
+    }
+}

+ 7 - 0
Content.Client/Administration/Systems/AdminFrozenSystem.cs

@@ -0,0 +1,7 @@
+using Content.Shared.Administration;
+
+namespace Content.Client.Administration.Systems;
+
+public sealed class AdminFrozenSystem : SharedAdminFrozenSystem
+{
+}

+ 52 - 0
Content.Client/Administration/Systems/AdminSystem.Overlay.cs

@@ -0,0 +1,52 @@
+using Content.Client.Administration.Managers;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Shared.Configuration;
+
+namespace Content.Client.Administration.Systems
+{
+    public sealed partial class AdminSystem
+    {
+        [Dependency] private readonly IOverlayManager _overlayManager = default!;
+        [Dependency] private readonly IResourceCache _resourceCache = default!;
+        [Dependency] private readonly IClientAdminManager _adminManager = default!;
+        [Dependency] private readonly IEyeManager _eyeManager = default!;
+        [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+        [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
+
+        private AdminNameOverlay _adminNameOverlay = default!;
+
+        public event Action? OverlayEnabled;
+        public event Action? OverlayDisabled;
+
+        private void InitializeOverlay()
+        {
+            _adminNameOverlay = new AdminNameOverlay(this, EntityManager, _eyeManager, _resourceCache, _entityLookup, _userInterfaceManager);
+            _adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
+        }
+
+        private void ShutdownOverlay()
+        {
+            _adminManager.AdminStatusUpdated -= OnAdminStatusUpdated;
+        }
+
+        private void OnAdminStatusUpdated()
+        {
+            AdminOverlayOff();
+        }
+
+        public void AdminOverlayOn()
+        {
+            if (_overlayManager.HasOverlay<AdminNameOverlay>()) return;
+            _overlayManager.AddOverlay(_adminNameOverlay);
+            OverlayEnabled?.Invoke();
+        }
+
+        public void AdminOverlayOff()
+        {
+            _overlayManager.RemoveOverlay<AdminNameOverlay>();
+            OverlayDisabled?.Invoke();
+        }
+    }
+}

+ 55 - 0
Content.Client/Administration/Systems/AdminSystem.cs

@@ -0,0 +1,55 @@
+using System.Linq;
+using Content.Shared.Administration;
+using Content.Shared.Administration.Events;
+using Content.Shared.GameTicking;
+using Robust.Shared.Network;
+
+namespace Content.Client.Administration.Systems
+{
+    public sealed partial class AdminSystem : EntitySystem
+    {
+        public event Action<List<PlayerInfo>>? PlayerListChanged;
+
+        private Dictionary<NetUserId, PlayerInfo>? _playerList;
+        public IReadOnlyList<PlayerInfo> PlayerList
+        {
+            get
+            {
+                if (_playerList != null) return _playerList.Values.ToList();
+
+                return new List<PlayerInfo>();
+            }
+        }
+
+        public override void Initialize()
+        {
+            base.Initialize();
+
+            InitializeOverlay();
+            SubscribeNetworkEvent<FullPlayerListEvent>(OnPlayerListChanged);
+            SubscribeNetworkEvent<PlayerInfoChangedEvent>(OnPlayerInfoChanged);
+        }
+
+        public override void Shutdown()
+        {
+            base.Shutdown();
+            ShutdownOverlay();
+        }
+
+        private void OnPlayerInfoChanged(PlayerInfoChangedEvent ev)
+        {
+            if(ev.PlayerInfo == null) return;
+
+            if (_playerList == null) _playerList = new();
+
+            _playerList[ev.PlayerInfo.SessionId] = ev.PlayerInfo;
+            PlayerListChanged?.Invoke(_playerList.Values.ToList());
+        }
+
+        private void OnPlayerListChanged(FullPlayerListEvent msg)
+        {
+            _playerList = msg.PlayersInfo.ToDictionary(x => x.SessionId, x => x);
+            PlayerListChanged?.Invoke(msg.PlayersInfo);
+        }
+    }
+}

+ 61 - 0
Content.Client/Administration/Systems/AdminVerbSystem.cs

@@ -0,0 +1,61 @@
+using Content.Shared.Administration;
+using Content.Shared.Administration.Managers;
+using Content.Shared.Mind.Components;
+using Content.Shared.Verbs;
+using Robust.Client.Console;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.Systems
+{
+    /// <summary>
+    ///     Client-side admin verb system. These usually open some sort of UIs.
+    /// </summary>
+    sealed class AdminVerbSystem : EntitySystem
+    {
+        [Dependency] private readonly IClientConGroupController _clientConGroupController = default!;
+        [Dependency] private readonly IClientConsoleHost _clientConsoleHost = default!;
+        [Dependency] private readonly ISharedAdminManager _admin = default!;
+
+        public override void Initialize()
+        {
+            SubscribeLocalEvent<GetVerbsEvent<Verb>>(AddAdminVerbs);
+
+        }
+
+        private void AddAdminVerbs(GetVerbsEvent<Verb> args)
+        {
+            // Currently this is only the ViewVariables verb, but more admin-UI related verbs can be added here.
+
+            // View variables verbs
+            if (_clientConGroupController.CanViewVar())
+            {
+                var verb = new VvVerb()
+                {
+                    Text = Loc.GetString("view-variables"),
+                    Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
+                    Act = () => _clientConsoleHost.ExecuteCommand($"vv {GetNetEntity(args.Target)}"),
+                    ClientExclusive = true // opening VV window is client-side. Don't ask server to run this verb.
+                };
+                args.Verbs.Add(verb);
+            }
+
+            if (!_admin.IsAdmin(args.User))
+                return;
+
+            if (_admin.HasAdminFlag(args.User, AdminFlags.Admin))
+                args.ExtraCategories.Add(VerbCategory.Admin);
+
+            if (_admin.HasAdminFlag(args.User, AdminFlags.Fun) && HasComp<MindContainerComponent>(args.Target))
+                args.ExtraCategories.Add(VerbCategory.Antag);
+
+            if (_admin.HasAdminFlag(args.User, AdminFlags.Debug))
+                args.ExtraCategories.Add(VerbCategory.Debug);
+
+            if (_admin.HasAdminFlag(args.User, AdminFlags.Fun))
+                args.ExtraCategories.Add(VerbCategory.Smite);
+
+            if (_admin.HasAdminFlag(args.User, AdminFlags.Admin))
+                args.ExtraCategories.Add(VerbCategory.Tricks);
+        }
+    }
+}

+ 42 - 0
Content.Client/Administration/Systems/BwoinkSystem.cs

@@ -0,0 +1,42 @@
+#nullable enable
+using Content.Shared.Administration;
+using JetBrains.Annotations;
+using Robust.Shared.Network;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Administration.Systems
+{
+    [UsedImplicitly]
+    public sealed class BwoinkSystem : SharedBwoinkSystem
+    {
+        [Dependency] private readonly IGameTiming _timing = default!;
+
+        public event EventHandler<BwoinkTextMessage>? OnBwoinkTextMessageRecieved;
+        private (TimeSpan Timestamp, bool Typing) _lastTypingUpdateSent;
+
+        protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs)
+        {
+            OnBwoinkTextMessageRecieved?.Invoke(this, message);
+        }
+
+        public void Send(NetUserId channelId, string text, bool playSound, bool adminOnly)
+        {
+            // Reuse the channel ID as the 'true sender'.
+            // Server will ignore this and if someone makes it not ignore this (which is bad, allows impersonation!!!), that will help.
+            RaiseNetworkEvent(new BwoinkTextMessage(channelId, channelId, text, playSound: playSound, adminOnly: adminOnly));
+            SendInputTextUpdated(channelId, false);
+        }
+
+        public void SendInputTextUpdated(NetUserId channel, bool typing)
+        {
+            if (_lastTypingUpdateSent.Typing == typing &&
+                _lastTypingUpdateSent.Timestamp + TimeSpan.FromSeconds(1) > _timing.RealTime)
+            {
+                return;
+            }
+
+            _lastTypingUpdateSent = (_timing.RealTime, typing);
+            RaiseNetworkEvent(new BwoinkClientTypingUpdated(channel, typing));
+        }
+    }
+}

+ 35 - 0
Content.Client/Administration/Systems/HeadstandSystem.cs

@@ -0,0 +1,35 @@
+using Content.Client.Administration.Components;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Administration.Systems;
+
+public sealed class HeadstandSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<HeadstandComponent, ComponentStartup>(OnHeadstandAdded);
+        SubscribeLocalEvent<HeadstandComponent, ComponentShutdown>(OnHeadstandRemoved);
+    }
+
+    private void OnHeadstandAdded(EntityUid uid, HeadstandComponent component, ComponentStartup args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        foreach (var layer in sprite.AllLayers)
+        {
+            layer.Rotation += Angle.FromDegrees(180.0f);
+        }
+    }
+
+    private void OnHeadstandRemoved(EntityUid uid, HeadstandComponent component, ComponentShutdown args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        foreach (var layer in sprite.AllLayers)
+        {
+            layer.Rotation -= Angle.FromDegrees(180.0f);
+        }
+    }
+}

+ 48 - 0
Content.Client/Administration/Systems/KillSignSystem.cs

@@ -0,0 +1,48 @@
+using System.Numerics;
+using Content.Client.Administration.Components;
+using Robust.Client.GameObjects;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.Systems;
+
+public sealed class KillSignSystem : EntitySystem
+{
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<KillSignComponent, ComponentStartup>(KillSignAdded);
+        SubscribeLocalEvent<KillSignComponent, ComponentShutdown>(KillSignRemoved);
+    }
+
+    private void KillSignRemoved(EntityUid uid, KillSignComponent component, ComponentShutdown args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        if (!sprite.LayerMapTryGet(KillSignKey.Key, out var layer))
+            return;
+
+        sprite.RemoveLayer(layer);
+    }
+
+    private void KillSignAdded(EntityUid uid, KillSignComponent component, ComponentStartup args)
+    {
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+
+        if (sprite.LayerMapTryGet(KillSignKey.Key, out var _))
+            return;
+
+        var adj = sprite.Bounds.Height / 2 + ((1.0f/32) * 6.0f);
+
+        var layer = sprite.AddLayer(new SpriteSpecifier.Rsi(new ResPath("Objects/Misc/killsign.rsi"), "sign"));
+        sprite.LayerMapSet(KillSignKey.Key, layer);
+
+        sprite.LayerSetOffset(layer, new Vector2(0.0f, adj));
+        sprite.LayerSetShader(layer, "unshaded");
+    }
+
+    private enum KillSignKey
+    {
+        Key,
+    }
+}

+ 18 - 0
Content.Client/Administration/UI/AdminAnnounceWindow.xaml

@@ -0,0 +1,18 @@
+<DefaultWindow
+    xmlns="https://spacestation14.io"
+    Title="{Loc admin-announce-title}"
+    MinWidth="500">
+    <GridContainer Columns="1">
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+            <LineEdit Name="Announcer" Access="Public" PlaceHolder="{Loc admin-announce-announcer-placeholder}" Text="{Loc admin-announce-announcer-default}" HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
+            <Control HorizontalExpand="True" SizeFlagsStretchRatio="1" />
+            <OptionButton Name="AnnounceMethod" Access="Public" HorizontalExpand="True" SizeFlagsStretchRatio="2"/>
+        </BoxContainer>
+        <TextEdit Name="Announcement" Access="Public" VerticalExpand="True" MinHeight="100" />
+
+        <GridContainer Rows="1">
+            <CheckBox Name="KeepWindowOpen" Access="Public" Text="{Loc 'admin-announce-keep-open'}" />
+            <Button Name="AnnounceButton" Access="Public" Disabled="True" Text="{Loc admin-announce-button}" HorizontalAlignment="Center"/>
+        </GridContainer>
+    </GridContainer>
+</DefaultWindow>

+ 41 - 0
Content.Client/Administration/UI/AdminAnnounceWindow.xaml.cs

@@ -0,0 +1,41 @@
+using Content.Shared.Administration;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI
+{
+    [GenerateTypedNameReferences]
+    public sealed partial class AdminAnnounceWindow : DefaultWindow
+    {
+        [Dependency] private readonly ILocalizationManager _localization = default!;
+
+        public AdminAnnounceWindow()
+        {
+            RobustXamlLoader.Load(this);
+            IoCManager.InjectDependencies(this);
+
+            Announcement.Placeholder = new Rope.Leaf(_localization.GetString("admin-announce-announcement-placeholder"));
+            AnnounceMethod.AddItem(_localization.GetString("admin-announce-type-station"));
+            AnnounceMethod.SetItemMetadata(0, AdminAnnounceType.Station);
+            AnnounceMethod.AddItem(_localization.GetString("admin-announce-type-server"));
+            AnnounceMethod.SetItemMetadata(1, AdminAnnounceType.Server);
+            AnnounceMethod.OnItemSelected += AnnounceMethodOnOnItemSelected;
+            Announcement.OnKeyBindUp += AnnouncementOnOnTextChanged;
+        }
+
+        private void AnnouncementOnOnTextChanged(GUIBoundKeyEventArgs args)
+        {
+            AnnounceButton.Disabled = Rope.Collapse(Announcement.TextRope).TrimStart() == "";
+        }
+
+        private void AnnounceMethodOnOnItemSelected(OptionButton.ItemSelectedEventArgs args)
+        {
+            AnnounceMethod.SelectId(args.Id);
+            Announcer.Editable = ((AdminAnnounceType?)args.Button.SelectedMetadata ?? AdminAnnounceType.Station) == AdminAnnounceType.Station;
+        }
+    }
+}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor