diff options
Diffstat (limited to 'Impostor-dev/src')
336 files changed, 21087 insertions, 0 deletions
diff --git a/Impostor-dev/src/.editorconfig b/Impostor-dev/src/.editorconfig new file mode 100644 index 0000000..ef7041d --- /dev/null +++ b/Impostor-dev/src/.editorconfig @@ -0,0 +1,230 @@ +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +charset = utf-8 +end_of_line = crlf +insert_final_newline = false +indent_style = space + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:refactoring +dotnet_style_qualification_for_property = false:refactoring +dotnet_style_qualification_for_method = false:refactoring +dotnet_style_qualification_for_event = false:refactoring + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static readonly fields are PascalCase +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static, readonly + +dotnet_naming_style.static_field_style.capitalization = pascal_case + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Async methods should have "Async" suffix +dotnet_naming_rule.async_methods_end_in_async.symbols = any_async_methods +dotnet_naming_rule.async_methods_end_in_async.style = end_in_async +dotnet_naming_rule.async_methods_end_in_async.severity = warning + +dotnet_naming_symbols.any_async_methods.applicable_kinds = method +dotnet_naming_symbols.any_async_methods.applicable_accessibilities = * +dotnet_naming_symbols.any_async_methods.required_modifiers = async + +dotnet_naming_style.end_in_async.required_prefix = +dotnet_naming_style.end_in_async.required_suffix = Async +dotnet_naming_style.end_in_async.capitalization = pascal_case +dotnet_naming_style.end_in_async.word_separator = + +# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}' +dotnet_diagnostic.RS2008.severity = none + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# 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_switch_labels = true +csharp_indent_labels = flush_left + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# 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 = do_not_ignore +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 + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# warning RS0037: PublicAPI.txt is missing '#nullable enable' +dotnet_diagnostic.RS0037.severity = none diff --git a/Impostor-dev/src/.gitattributes b/Impostor-dev/src/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/Impostor-dev/src/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# 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 + +############################################################################### +# 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 diff --git a/Impostor-dev/src/.gitignore b/Impostor-dev/src/.gitignore new file mode 100644 index 0000000..e0838ba --- /dev/null +++ b/Impostor-dev/src/.gitignore @@ -0,0 +1,263 @@ +config.*.json + +## 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 + +# 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 + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs b/Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs new file mode 100644 index 0000000..b31d2d1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Attributes/EventListenerAttribute.cs @@ -0,0 +1,34 @@ +using System; + +namespace Impostor.Api.Events +{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] + public class EventListenerAttribute : Attribute + { + public EventListenerAttribute(EventPriority priority = EventPriority.Normal) + { + Priority = priority; + } + + public EventListenerAttribute(Type @event, EventPriority priority = EventPriority.Normal) + { + Priority = priority; + Event = @event; + } + + /// <summary> + /// The priority of the event listener. + /// </summary> + public EventPriority Priority { get; set; } + + /// <summary> + /// The events that the listener is listening to. + /// </summary> + public Type? Event { get; set; } + + /// <summary> + /// If set to true, the listener will be called regardless of the <see cref="IEventCancelable.IsCancelled"/>. + /// </summary> + public bool IgnoreCancelled { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/EventPriority.cs b/Impostor-dev/src/Impostor.Api/Events/EventPriority.cs new file mode 100644 index 0000000..7acdbeb --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/EventPriority.cs @@ -0,0 +1,12 @@ +namespace Impostor.Api.Events +{ + public enum EventPriority + { + Lowest = 0, + Low = 1, + Normal = 2, + High = 3, + Highest = 4, + Monitor = 5, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs new file mode 100644 index 0000000..18956ea --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameAlterEvent.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Events +{ + public interface IGameAlterEvent : IGameEvent + { + bool IsPublic { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs new file mode 100644 index 0000000..ef45f1d --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameCreatedEvent.cs @@ -0,0 +1,11 @@ +using Impostor.Api.Games; + +namespace Impostor.Api.Events +{ + /// <summary> + /// Called whenever a new <see cref="IGame"/> is created. + /// </summary> + public interface IGameCreatedEvent : IGameEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs new file mode 100644 index 0000000..7ff7b46 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameDestroyedEvent.cs @@ -0,0 +1,11 @@ +using Impostor.Api.Games; + +namespace Impostor.Api.Events +{ + /// <summary> + /// Called whenever a new <see cref="IGame"/> is destroyed. + /// </summary> + public interface IGameDestroyedEvent : IGameEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs new file mode 100644 index 0000000..d8ae159 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEndedEvent.cs @@ -0,0 +1,9 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Events +{ + public interface IGameEndedEvent : IGameEvent + { + public GameOverReason GameOverReason { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs new file mode 100644 index 0000000..c9ae579 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameEvent.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Games; + +namespace Impostor.Api.Events +{ + public interface IGameEvent : IEvent + { + /// <summary> + /// Gets the <see cref="IGame"/> this event belongs to. + /// </summary> + IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs new file mode 100644 index 0000000..921568e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerJoinedEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events +{ + public interface IGamePlayerJoinedEvent : IGameEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs new file mode 100644 index 0000000..21d8b7c --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGamePlayerLeftEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events +{ + public interface IGamePlayerLeftEvent : IGameEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs new file mode 100644 index 0000000..b6e5111 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartedEvent.cs @@ -0,0 +1,9 @@ +namespace Impostor.Api.Events +{ + /// <summary> + /// The game is started here and players have been initialized. + /// </summary> + public interface IGameStartedEvent : IGameEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs new file mode 100644 index 0000000..5998bf2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/IGameStartingEvent.cs @@ -0,0 +1,11 @@ +namespace Impostor.Api.Events +{ + /// <summary> + /// Called when the game is going to start. + /// When this is called, not all players are initialized properly yet. + /// If you want to get correct player states, use <see cref="IGameStartedEvent"/>. + /// </summary> + public interface IGameStartingEvent : IGameEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs new file mode 100644 index 0000000..a217580 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEndedEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events.Meeting +{ + public interface IMeetingEndedEvent : IMeetingEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs new file mode 100644 index 0000000..4461318 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingEvent.cs @@ -0,0 +1,9 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Meeting +{ + public interface IMeetingEvent : IGameEvent + { + IInnerMeetingHud MeetingHud { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs new file mode 100644 index 0000000..a237fff --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Meeting/IMeetingStartedEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events.Meeting +{ + public interface IMeetingStartedEvent : IMeetingEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs new file mode 100644 index 0000000..52efe96 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerChatEvent.cs @@ -0,0 +1,10 @@ +namespace Impostor.Api.Events.Player +{ + public interface IPlayerChatEvent : IPlayerEvent + { + /// <summary> + /// Gets the message sent by the player. + /// </summary> + string Message { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs new file mode 100644 index 0000000..78ccd2d --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerCompletedTaskEvent.cs @@ -0,0 +1,10 @@ +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Player +{ + public interface IPlayerCompletedTaskEvent : IPlayerEvent + { + ITaskInfo Task { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs new file mode 100644 index 0000000..ac80d64 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerDestroyedEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events.Player +{ + public interface IPlayerDestroyedEvent : IPlayerEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs new file mode 100644 index 0000000..247fe64 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Player +{ + public interface IPlayerEvent : IGameEvent + { + /// <summary> + /// Gets the <see cref="IClientPlayer"/> that triggered this <see cref="IPlayerEvent"/>. + /// </summary> + IClientPlayer ClientPlayer { get; } + + /// <summary> + /// Gets the networked <see cref="IInnerPlayerControl"/> that triggered this <see cref="IPlayerEvent"/>. + /// This <see cref="IInnerPlayerControl"/> belongs to the <see cref="IClientPlayer"/>. + /// </summary> + IInnerPlayerControl PlayerControl { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs new file mode 100644 index 0000000..65ade4e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerExileEvent.cs @@ -0,0 +1,9 @@ +namespace Impostor.Api.Events.Player +{ + /// <summary> + /// Called whenever a player gets exiled (voted out). + /// </summary> + public interface IPlayerExileEvent : IPlayerEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs new file mode 100644 index 0000000..c47c00b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerMurderEvent.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Player +{ + public interface IPlayerMurderEvent : IPlayerEvent + { + /// <summary> + /// Gets the player who got murdered. + /// </summary> + IInnerPlayerControl Victim { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs new file mode 100644 index 0000000..c03d782 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSetStartCounterEvent.cs @@ -0,0 +1,10 @@ +namespace Impostor.Api.Events.Player +{ + public interface IPlayerSetStartCounterEvent : IPlayerEvent + { + /// <summary> + /// Gets the current time of the start counter. + /// </summary> + byte SecondsLeft { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs new file mode 100644 index 0000000..a3be654 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerSpawnedEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events.Player +{ + public interface IPlayerSpawnedEvent : IPlayerEvent + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs new file mode 100644 index 0000000..1a28115 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerStartMeetingEvent.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Events.Player +{ + public interface IPlayerStartMeetingEvent : IPlayerEvent + { + /// <summary> + /// Gets the player who's body got reported. Is null when the meeting started by Emergency call button + /// </summary> + IInnerPlayerControl? Body { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs new file mode 100644 index 0000000..81f178b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Game/Player/IPlayerVentEvent.cs @@ -0,0 +1,17 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Events.Player +{ + public interface IPlayerVentEvent : IPlayerEvent + { + /// <summary> + /// Gets get the id of the used vent. + /// </summary> + public VentLocation VentId { get; } + + /// <summary> + /// Gets a value indicating whether the vent was entered or exited. + /// </summary> + public bool VentEnter { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Events/IEvent.cs b/Impostor-dev/src/Impostor.Api/Events/IEvent.cs new file mode 100644 index 0000000..796898e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/IEvent.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events +{ + public interface IEvent + { + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs b/Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs new file mode 100644 index 0000000..319f02a --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/IEventCancelable.cs @@ -0,0 +1,10 @@ +namespace Impostor.Api.Events +{ + public interface IEventCancelable : IEvent + { + /// <summary> + /// True if the event was cancelled. + /// </summary> + bool IsCancelled { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/IEventListener.cs b/Impostor-dev/src/Impostor.Api/Events/IEventListener.cs new file mode 100644 index 0000000..76392fc --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/IEventListener.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Events +{ + public interface IEventListener + { + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs b/Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs new file mode 100644 index 0000000..b5c140e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/IManualEventListener.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Impostor.Api.Events +{ + public interface IManualEventListener : IEventListener + { + public bool CanExecute<T>(); + + public ValueTask Execute(IEvent @event); + + EventPriority Priority { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs b/Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs new file mode 100644 index 0000000..07a7f7c --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Events/Managers/IEventManager.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; + +namespace Impostor.Api.Events.Managers +{ + public interface IEventManager + { + /// <summary> + /// Register a temporary event listener. + /// </summary> + /// <param name="listener">Event listener.</param> + /// <param name="invoker">Middleware between the events, which can be used to swap to the correct thread dispatcher.</param> + /// <returns>Disposable that unregisters the callback from the event manager.</returns> + /// <typeparam name="TListener">Type of the event listener.</typeparam> + IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, Task>? invoker = null) + where TListener : IEventListener; + + /// <summary> + /// Returns true if an event with the type <see cref="TEvent"/> is registered. + /// </summary> + /// <returns>True if the <see cref="TEvent"/> is registered.</returns> + /// <typeparam name="TEvent">Type of the event.</typeparam> + bool IsRegistered<TEvent>() + where TEvent : IEvent; + + /// <summary> + /// Call all the event listeners for the type <see cref="TEvent"/>. + /// </summary> + /// <param name="event">The event argument.</param> + /// <typeparam name="TEvent">Type of the event.</typeparam> + /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns> + ValueTask CallAsync<TEvent>(TEvent @event) + where TEvent : IEvent; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs new file mode 100644 index 0000000..8eb72f8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorCheatException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Impostor.Api +{ + public class ImpostorCheatException : ImpostorException + { + public ImpostorCheatException() + { + } + + protected ImpostorCheatException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public ImpostorCheatException(string? message) : base(message) + { + } + + public ImpostorCheatException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs new file mode 100644 index 0000000..1e59a9b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorConfigException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Impostor.Api +{ + public class ImpostorConfigException : ImpostorException + { + public ImpostorConfigException() + { + } + + protected ImpostorConfigException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public ImpostorConfigException(string? message) : base(message) + { + } + + public ImpostorConfigException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs new file mode 100644 index 0000000..188c50e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Impostor.Api +{ + public class ImpostorException : Exception + { + public ImpostorException() + { + } + + protected ImpostorException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public ImpostorException(string? message) : base(message) + { + } + + public ImpostorException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs new file mode 100644 index 0000000..864602d --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Exceptions/ImpostorProtocolException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Impostor.Api +{ + public class ImpostorProtocolException : ImpostorException + { + public ImpostorProtocolException() + { + } + + protected ImpostorProtocolException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public ImpostorProtocolException(string? message) : base(message) + { + } + + public ImpostorProtocolException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs b/Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs new file mode 100644 index 0000000..c103ee7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Extensions/SpanReaderExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace Impostor.Api +{ + /// <summary> + /// Priovides a StreamReader-like api throught extensions + /// </summary> + public static class SpanReaderExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadByte(this ref ReadOnlySpan<byte> input) + { + var original = Advance<byte>(ref input); + return original[0]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadInt32(this ref ReadOnlySpan<byte> input) + { + var original = Advance<int>(ref input); + return BinaryPrimitives.ReadInt32LittleEndian(original); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32(this ref ReadOnlySpan<byte> input) + { + var original = Advance<uint>(ref input); + return BinaryPrimitives.ReadUInt32LittleEndian(original); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ReadSingle(this ref ReadOnlySpan<byte> input) + { + var original = Advance<float>(ref input); + + // BitConverter.Int32BitsToSingle + // Doesn't exist in net 2.0 for some reason + return Int32BitsToSingle(BinaryPrimitives.ReadInt32LittleEndian(original)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadBoolean(this ref ReadOnlySpan<byte> input) + { + return input.ReadByte() != 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe float Int32BitsToSingle(int value) + { + return *((float*)&value); + } + + /// <summary> + /// Advances the position of <see cref="input"/> by the size of <see cref="T"/>. + /// </summary> + /// <typeparam name="T">Type that will be read.</typeparam> + /// <param name="input">input "stream"/span.</param> + /// <returns>The original input</returns> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ReadOnlySpan<byte> Advance<T>(ref ReadOnlySpan<byte> input) + where T : unmanaged + { + var original = input; + input = input.Slice(sizeof(T)); + return original; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs b/Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs new file mode 100644 index 0000000..68f1efd --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Extensions/SystemTypesExtensions.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api +{ + public static class SystemTypesExtensions + { + public static string GetFriendlyName(this SystemTypes type) + { + return SystemTypeHelpers.Names[(int)type]; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs new file mode 100644 index 0000000..a736978 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Games +{ + public static class GameExtensions + { + public static ValueTask SendToAllExceptAsync(this IGame game, IMessageWriter writer, LimboStates states, int? id) + { + return id.HasValue + ? game.SendToAllExceptAsync(writer, id.Value, states) + : game.SendToAllAsync(writer, states); + } + + public static ValueTask SendToAllExceptAsync(this IGame game, IMessageWriter writer, LimboStates states, IClient client) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + return game.SendToAllExceptAsync(writer, client.Id, states); + } + + public static ValueTask SendToAsync(this IGame game, IMessageWriter writer, IClient client) + { + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + + return game.SendToAsync(writer, client.Id); + } + + public static ValueTask SendToAsync(this IGame game, IMessageWriter writer, IClientPlayer player) + { + return game.SendToAsync(writer, player.Client); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs new file mode 100644 index 0000000..9a5a2b4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/Extensions/GameManagerExtensions.cs @@ -0,0 +1,14 @@ +using System.Linq; +using Impostor.Api.Games.Managers; +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Games +{ + public static class GameManagerExtensions + { + public static int GetGameCount(this IGameManager manager, MapFlags map) + { + return manager.Games.Count(game => map.HasFlag((MapFlags)(1 << game.Options.MapId))); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/GameCode.cs b/Impostor-dev/src/Impostor.Api/Games/GameCode.cs new file mode 100644 index 0000000..2c30e7e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/GameCode.cs @@ -0,0 +1,74 @@ +using System; +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Games +{ + public readonly struct GameCode : IEquatable<GameCode> + { + public GameCode(int value) + { + Value = value; + Code = GameCodeParser.IntToGameName(value); + } + + public GameCode(string code) + { + Value = GameCodeParser.GameNameToInt(code); + Code = code; + } + + public string Code { get; } + + public int Value { get; } + + public static implicit operator string(GameCode code) => code.Code; + + public static implicit operator int(GameCode code) => code.Value; + + public static implicit operator GameCode(string code) => From(code); + + public static implicit operator GameCode(int value) => From(value); + + public static bool operator ==(GameCode left, GameCode right) + { + return left.Equals(right); + } + + public static bool operator !=(GameCode left, GameCode right) + { + return !left.Equals(right); + } + + public static GameCode Create() + { + return new GameCode(GameCodeParser.GenerateCode(6)); + } + + public static GameCode From(int value) => new GameCode(value); + + public static GameCode From(string value) => new GameCode(value); + + /// <inheritdoc/> + public bool Equals(GameCode other) + { + return Code == other.Code && Value == other.Value; + } + + /// <inheritdoc/> + public override bool Equals(object? obj) + { + return obj is GameCode other && Equals(other); + } + + /// <inheritdoc/> + public override int GetHashCode() + { + return HashCode.Combine(Code, Value); + } + + public override string ToString() + { + return Code; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs b/Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs new file mode 100644 index 0000000..4889ea9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/GameJoinError.cs @@ -0,0 +1,48 @@ +namespace Impostor.Api.Games +{ + public enum GameJoinError + { + /// <summary> + /// No error occured while joining the game. + /// </summary> + None, + + /// <summary> + /// The client is not registered in the client manager. + /// </summary> + InvalidClient, + + /// <summary> + /// The client has been banned from the game. + /// </summary> + Banned, + + /// <summary> + /// The game is full. + /// </summary> + GameFull, + + /// <summary> + /// The limbo state of the player is incorrect. + /// </summary> + InvalidLimbo, + + /// <summary> + /// The game is already started. + /// </summary> + GameStarted, + + /// <summary> + /// The game has been destroyed. + /// </summary> + GameDestroyed, + + /// <summary> + /// Custom error by a plugin. + /// </summary> + /// <remarks> + /// A custom message can be set in <see cref="GameJoinResult.Message"/>. + /// </remarks> + Custom, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs b/Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs new file mode 100644 index 0000000..b33a2b6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/GameJoinResult.cs @@ -0,0 +1,48 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Impostor.Api.Net; + +namespace Impostor.Api.Games +{ + public readonly struct GameJoinResult + { + private GameJoinResult(GameJoinError error, string? message = null, IClientPlayer? player = null) + { + Error = error; + Message = message; + Player = player; + } + + public GameJoinError Error { get; } + + public bool IsSuccess => Error == GameJoinError.None; + + public bool IsCustomError => Error == GameJoinError.Custom; + + [MemberNotNullWhen(true, nameof(IsCustomError))] + public string? Message { get; } + + [MemberNotNullWhen(true, nameof(IsSuccess))] + public IClientPlayer? Player { get; } + + public static GameJoinResult CreateCustomError(string message) + { + return new GameJoinResult(GameJoinError.Custom, message); + } + + public static GameJoinResult CreateSuccess(IClientPlayer player) + { + return new GameJoinResult(GameJoinError.None, player: player); + } + + public static GameJoinResult FromError(GameJoinError error) + { + if (error == GameJoinError.Custom) + { + throw new InvalidOperationException($"Custom errors should provide a message, use {nameof(CreateCustomError)} instead."); + } + + return new GameJoinResult(error); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/IGame.cs b/Impostor-dev/src/Impostor.Api/Games/IGame.cs new file mode 100644 index 0000000..ad71986 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/IGame.cs @@ -0,0 +1,88 @@ +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Games +{ + public interface IGame + { + GameOptionsData Options { get; } + + GameCode Code { get; } + + GameStates GameState { get; } + + IGameNet GameNet { get; } + + IEnumerable<IClientPlayer> Players { get; } + + IPEndPoint PublicIp { get; } + + int PlayerCount { get; } + + IClientPlayer Host { get; } + + bool IsPublic { get; } + + IDictionary<object, object> Items { get; } + + int HostId { get; } + + IClientPlayer GetClientPlayer(int clientId); + + /// <summary> + /// Adds an <see cref="IPAddress"/> to the ban list of this game. + /// Prevents all future joins from this <see cref="IPAddress"/>. + /// + /// This does not kick the player with that <see cref="IPAddress"/> from the lobby. + /// </summary> + /// <param name="ipAddress"> + /// The <see cref="IPAddress"/> to ban. + /// </param> + void BanIp(IPAddress ipAddress); + + /// <summary> + /// Syncs the internal <see cref="GameOptionsData"/> to all players. + /// Necessary to do if you modified it, otherwise it won't be used. + /// </summary> + /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns> + ValueTask SyncSettingsAsync(); + + /// <summary> + /// Sets the specified list as Impostor on all connected players. + /// </summary> + /// <param name="players">List of players to be Impostor.</param> + /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns> + ValueTask SetInfectedAsync(IEnumerable<IInnerPlayerControl> players); + + /// <summary> + /// Send the message to all players. + /// </summary> + /// <param name="writer">Message to send.</param> + /// <param name="states">Required limbo state of the player.</param> + /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns> + ValueTask SendToAllAsync(IMessageWriter writer, LimboStates states = LimboStates.NotLimbo); + + /// <summary> + /// Send the message to all players except one. + /// </summary> + /// <param name="writer">Message to send.</param> + /// <param name="senderId">The player to exclude from sending the message.</param> + /// <param name="states">Required limbo state of the player.</param> + /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns> + ValueTask SendToAllExceptAsync(IMessageWriter writer, int senderId, LimboStates states = LimboStates.NotLimbo); + + /// <summary> + /// Send a message to a specific player. + /// </summary> + /// <param name="writer">Message to send.</param> + /// <param name="id">ID of the client.</param> + /// <returns>A <see cref="ValueTask"/> representing the asynchronous operation.</returns> + ValueTask SendToAsync(IMessageWriter writer, int id); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs b/Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs new file mode 100644 index 0000000..f264fe0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/IGameCodeFactory.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Games +{ + public interface IGameCodeFactory + { + GameCode Create(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs b/Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs new file mode 100644 index 0000000..10a05f0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Games/Managers/IGameManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Impostor.Api.Games.Managers +{ + public interface IGameManager + { + IEnumerable<IGame> Games { get; } + + IGame? Find(GameCode code); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj new file mode 100644 index 0000000..8ad2582 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj @@ -0,0 +1,38 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet> + <LangVersion>9</LangVersion> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <Nullable>enable</Nullable> + <Version>1.0.0</Version> + <IncludeSymbols>true</IncludeSymbols> + <SymbolPackageFormat>snupkg</SymbolPackageFormat> + <AssemblyName>Impostor.Api</AssemblyName> + <AssemblyTitle>Impostor.Api</AssemblyTitle> + <Authors>AeonLucid</Authors> + <Description>An api library for Impostor, an Among Us private server. You need this package to write plugins for Impostor.</Description> + <PackageId>Impostor.Api</PackageId> + <PackageTags>Among Us;Impostor;Impostor Plugin</PackageTags> + <PackageIconUrl>https://raw.githubusercontent.com/Impostor/Impostor/dev/docs/images/logo_64.png</PackageIconUrl> + <PackageProjectUrl>https://github.com/Impostor/Impostor</PackageProjectUrl> + <RepositoryType>git</RepositoryType> + <RepositoryUrl>https://github.com/Impostor/Impostor</RepositoryUrl> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Bcl.HashCode" Version="1.1.0" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" /> + <PackageReference Include="Nullable" Version="1.3.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + +</Project>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings new file mode 100644 index 0000000..2df7445 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Impostor.Api.csproj.DotSettings @@ -0,0 +1,8 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=attributes/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cattributes/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=exceptions/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=games_005Cextensions/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=net_005Cextensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs b/Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs new file mode 100644 index 0000000..46d1b2e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/AlterGameTags.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Innersloth +{ + public enum AlterGameTags : byte + { + ChangePrivacy = 1, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs new file mode 100644 index 0000000..c163601 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/ChatNoteType.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Innersloth +{ + public enum ChatNoteType : byte + { + DidVote = 0, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs new file mode 100644 index 0000000..fc7be4c --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/ColorType.cs @@ -0,0 +1,18 @@ +namespace Impostor.Api.Innersloth.Customization +{ + public enum ColorType : byte + { + Red = 0, + Blue = 1, + Green = 2, + Pink = 3, + Orange = 4, + Yellow = 5, + Black = 6, + White = 7, + Purple = 8, + Brown = 9, + Cyan = 10, + Lime = 11, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs new file mode 100644 index 0000000..5e0a3ef --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/HatType.cs @@ -0,0 +1,100 @@ +namespace Impostor.Api.Innersloth.Customization +{ + public enum HatType + { + NoHat = 0, + Astronaut = 1, + BaseballCap = 2, + BrainSlug = 3, + BushHat = 4, + CaptainsHat = 5, + DoubleTopHat = 6, + Flowerpot = 7, + Goggles = 8, + HardHat = 9, + Military = 10, + PaperHat = 11, + PartyHat = 12, + Police = 13, + Stethescope = 14, + TopHat = 15, + TowelWizard = 16, + Ushanka = 17, + Viking = 18, + WallCap = 19, + Snowman = 20, + Reindeer = 21, + Lights = 22, + Santa = 23, + Tree = 24, + Present = 25, + Candycanes = 26, + ElfHat = 27, + NewYears2018 = 28, + WhiteHat = 29, + Crown = 30, + Eyebrows = 31, + HaloHat = 32, + HeroCap = 33, + PipCap = 34, + PlungerHat = 35, + ScubaHat = 36, + StickminHat = 37, + StrawHat = 38, + TenGallonHat = 39, + ThirdEyeHat = 40, + ToiletPaperHat = 41, + Toppat = 42, + Fedora = 43, + Goggles2 = 44, + Headphones = 45, + MaskHat = 46, + PaperMask = 47, + Security = 48, + StrapHat = 49, + Banana = 50, + Beanie = 51, + Bear = 52, + Cheese = 53, + Cherry = 54, + Egg = 55, + Fedora2 = 56, + Flamingo = 57, + FlowerPin = 58, + Helmet = 59, + Plant = 60, + BatEyes = 61, + BatWings = 62, + Horns = 63, + Mohawk = 64, + Pumpkin = 65, + ScaryBag = 66, + Witch = 67, + Wolf = 68, + Pirate = 69, + Plague = 70, + Machete = 71, + Fred = 72, + MinerCap = 73, + WinterHat = 74, + Archae = 75, + Antenna = 76, + Balloon = 77, + BirdNest = 78, + BlackBelt = 79, + Caution = 80, + Chef = 81, + CopHat = 82, + DoRag = 83, + DumSticker = 84, + Fez = 85, + GeneralHat = 86, + GreyThing = 87, + HunterCap = 88, + JungleHat = 89, + MiniCrewmate = 90, + NinjaMask = 91, + RamHorns = 92, + Snowman2 = 93, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs new file mode 100644 index 0000000..456e327 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/PetType.cs @@ -0,0 +1,18 @@ +namespace Impostor.Api.Innersloth.Customization +{ + public enum PetType + { + NoPet = 0, + Alien = 1, + Crewmate = 2, + Doggy = 3, + Stickmin = 4, + Hamster = 5, + Robot = 6, + Ufo = 7, + Ellie = 8, + Squig = 9, + Bedcrab = 10, + Glitch = 11, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs new file mode 100644 index 0000000..35da312 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/Customization/SkinType.cs @@ -0,0 +1,22 @@ +namespace Impostor.Api.Innersloth.Customization +{ + public enum SkinType : byte + { + None = 0, + Astro = 1, + Capt = 2, + Mech = 3, + Military = 4, + Police = 5, + Science = 6, + SuitB = 7, + SuitW = 8, + Wall = 9, + Hazmat = 10, + Security = 11, + Tarmac = 12, + Miner = 13, + Winter = 14, + Archae = 15, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs b/Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs new file mode 100644 index 0000000..a07ac05 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/DeathReason.cs @@ -0,0 +1,9 @@ +namespace Impostor.Api.Innersloth +{ + public enum DeathReason + { + Exile = 0, + Kill = 1, + Disconnect = 2, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs b/Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs new file mode 100644 index 0000000..9526f58 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/DisconnectReason.cs @@ -0,0 +1,54 @@ +namespace Impostor.Api.Innersloth +{ + public enum DisconnectReason + { + ExitGame = 0, + // The game you tried to join is full. + // Check with the host to see if you can join next round. + GameFull = 1, + // The game you tried to join already started. + // Check with the host to see if you can join next round. + GameStarted = 2, + // Could not find the game you're looking for. + GameMissing = 3, + IncorrectGame = 18, + // For these a message can be given, specifying an empty message shows + // "An unknown error disconnected you from the server." + CustomMessage1 = 4, + Custom = 8, + // CustomMessage3 = 11, + // CustomMessage4 = 12, + // CustomMessage5 = 13, + // CustomMessage6 = 14, + // CustomMessage7 = 15, + // You are running an older version of the game. + // Please update to play with others. + IncorrectVersion = 5, + // You cannot rejoin that room. + // You were banned + Banned = 6, + // You can rejoin if the room hasn't started + // You were kicked + Kicked = 7, + // You were banned for hacking. + // Please stop. + Hacking = 10, + Destroy = 16, + // You disconnected from the host. + // If this happens often, check your WiFi strength. + // + // You disconnected from the server. + // If this happens often, check your network strength. + // This may also be a server issue. + Error = 17, + // The server stopped this game. Possibly due to inactivity. + ServerRequest = 19, + // The Among Us servers are overloaded. + // Sorry! Please try again later! + ServerFull = 20, + FocusLostBackground = 207, + IntentionalLeaving = 208, + FocusLost = 209, + NewConnection = 210, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs b/Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs new file mode 100644 index 0000000..c8a0824 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/FloatRange.cs @@ -0,0 +1,22 @@ +using Impostor.Api.Unity; + +namespace Impostor.Api.Innersloth +{ + public class FloatRange + { + private readonly float _min; + private readonly float _max; + + public FloatRange(float min, float max) + { + _min = min; + _max = max; + } + + public float Width => _max - _min; + + public float Lerp(float v) => Mathf.Lerp(_min, _max, v); + + public float ReverseLerp(float t) => Mathf.Clamp((t - _min) / Width, 0.0f, 1f); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs new file mode 100644 index 0000000..9717cff --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameCodeParser.cs @@ -0,0 +1,142 @@ +using System; +using System.Buffers.Binary; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Impostor.Api.Innersloth +{ + public static class GameCodeParser + { + private const string V2 = "QWXRTYLPESDFGHUJKZOCVBINMA"; + private static readonly int[] V2Map = { + 25, + 21, + 19, + 10, + 8, + 11, + 12, + 13, + 22, + 15, + 16, + 6, + 24, + 23, + 18, + 7, + 0, + 3, + 9, + 4, + 14, + 20, + 1, + 2, + 5, + 17 + }; + private static readonly RNGCryptoServiceProvider Random = new RNGCryptoServiceProvider(); + + public static string IntToGameName(int input) + { + // V2. + if (input < -1) + { + return IntToGameNameV2(input); + } + + // V1. + Span<byte> code = stackalloc byte[4]; + BinaryPrimitives.WriteInt32LittleEndian(code, input); +#if NETSTANDARD2_0 + return Encoding.UTF8.GetString(code.Slice(0, 4).ToArray()); +#else + return Encoding.UTF8.GetString(code.Slice(0, 4)); +#endif + } + + private static string IntToGameNameV2(int input) + { + var a = input & 0x3FF; + var b = (input >> 10) & 0xFFFFF; + + return new string(new [] + { + V2[a % 26], + V2[a / 26], + V2[b % 26], + V2[b / 26 % 26], + V2[b / (26 * 26) % 26], + V2[b / (26 * 26 * 26) % 26] + }); + } + + public static int GameNameToInt(string code) + { + var upper = code.ToUpperInvariant(); + if (upper.Any(x => !char.IsLetter(x))) + { + return -1; + } + + var len = code.Length; + if (len == 6) + { + return GameNameToIntV2(upper); + } + + if (len == 4) + { + return code[0] | ((code[1] | ((code[2] | (code[3] << 8)) << 8)) << 8); + } + + return -1; + } + + private static int GameNameToIntV2(string code) + { + var a = V2Map[code[0] - 65]; + var b = V2Map[code[1] - 65]; + var c = V2Map[code[2] - 65]; + var d = V2Map[code[3] - 65]; + var e = V2Map[code[4] - 65]; + var f = V2Map[code[5] - 65]; + + var one = (a + 26 * b) & 0x3FF; + var two = (c + 26 * (d + 26 * (e + 26 * f))); + + return (int) (one | ((two << 10) & 0x3FFFFC00) | 0x80000000); + } + + public static int GenerateCode(int len) + { + if (len != 4 && len != 6) + { + throw new ArgumentException("should be 4 or 6", nameof(len)); + } + + // Generate random bytes. +#if NETSTANDARD2_0 + var data = new byte[len]; +#else + Span<byte> data = stackalloc byte[len]; +#endif + Random.GetBytes(data); + + // Convert to their char representation. + Span<char> dataChar = stackalloc char[len]; + for (var i = 0; i < len; i++) + { + dataChar[i] = V2[V2Map[data[i] % 26]]; + } + +#if NETSTANDARD2_0 + return GameNameToInt(new string(dataChar.ToArray())); +#else + return GameNameToInt(new string(dataChar)); +#endif + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs new file mode 100644 index 0000000..bbb463f --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameKeywords.cs @@ -0,0 +1,19 @@ +using System; + +namespace Impostor.Api.Innersloth +{ + [Flags] + public enum GameKeywords : uint + { + All = 0, + Other = 1, + Spanish = 2, + Korean = 4, + Russian = 8, + Portuguese = 16, + Arabic = 32, + Filipone = 64, + Polish = 128, + English = 256, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs new file mode 100644 index 0000000..d18efd8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameOptionsData.cs @@ -0,0 +1,261 @@ +using System; +using System.IO; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Innersloth +{ + public class GameOptionsData + { + /// <summary> + /// The latest major version of the game client. + /// </summary> + public const int LatestVersion = 4; + + /// <summary> + /// Gets or sets host's version of the game. + /// </summary> + public byte Version { get; set; } + + /// <summary> + /// Gets or sets the maximum amount of players for this lobby. + /// </summary> + public byte MaxPlayers { get; set; } + + /// <summary> + /// Gets or sets the language of the lobby as per <see cref="GameKeywords"/> enum. + /// </summary> + public GameKeywords Keywords { get; set; } + + /// <summary> + /// Gets or sets the MapId selected for this lobby + /// </summary> + /// <remarks> + /// Skeld = 0, MiraHQ = 1, Polus = 2. + /// </remarks> + internal byte MapId { get; set; } + + /// <summary> + /// Gets or sets the map selected for this lobby + /// </summary> + public MapTypes Map + { + get => (MapTypes)MapId; + set => MapId = (byte)value; + } + + /// <summary> + /// Gets or sets the Player speed modifier. + /// </summary> + public float PlayerSpeedMod { get; set; } + + /// <summary> + /// Gets or sets the Light modifier for the players that are members of the crew as a multiplier value. + /// </summary> + public float CrewLightMod { get; set; } + + /// <summary> + /// Gets or sets the Light modifier for the players that are Impostors as a multiplier value. + /// </summary> + public float ImpostorLightMod { get; set; } + + /// <summary> + /// Gets or sets the Impostor cooldown to kill in seconds. + /// </summary> + public float KillCooldown { get; set; } + + /// <summary> + /// Gets or sets the number of common tasks. + /// </summary> + public int NumCommonTasks { get; set; } + + /// <summary> + /// Gets or sets the number of long tasks. + /// </summary> + public int NumLongTasks { get; set; } + + /// <summary> + /// Gets or sets the number of short tasks. + /// </summary> + public int NumShortTasks { get; set; } + + /// <summary> + /// Gets or sets the maximum amount of emergency meetings each player can call during the game in seconds. + /// </summary> + public int NumEmergencyMeetings { get; set; } + + /// <summary> + /// Gets or sets the cooldown between each time any player can call an emergency meeting in seconds. + /// </summary> + public int EmergencyCooldown { get; set; } + + /// <summary> + /// Gets or sets the number of impostors for this lobby. + /// </summary> + public int NumImpostors { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether ghosts (dead crew members) can do tasks. + /// </summary> + public bool GhostsDoTasks { get; set; } + + /// <summary> + /// Gets or sets the Kill as per values in <see cref="KillDistances"/>. + /// </summary> + /// <remarks> + /// Short = 0, Normal = 1, Long = 2. + /// </remarks> + public KillDistances KillDistance { get; set; } + + /// <summary> + /// Gets or sets the time for discussion before voting time in seconds. + /// </summary> + public int DiscussionTime { get; set; } + + /// <summary> + /// Gets or sets the time for voting in seconds. + /// </summary> + public int VotingTime { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether an ejected player is an impostor or not. + /// </summary> + public bool ConfirmImpostor { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether players are able to see tasks being performed by other players. + /// </summary> + /// <remarks> + /// By being set to true, tasks such as Empty Garbage, Submit Scan, Clear asteroids, Prime shields execution will be visible to other players. + /// </remarks> + public bool VisualTasks { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the vote is anonymous. + /// </summary> + public bool AnonymousVotes { get; set; } + + /// <summary> + /// Gets or sets the task bar update mode as per values in <see cref="Innersloth.TaskBarUpdate"/>. + /// </summary> + public TaskBarUpdate TaskBarUpdate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether the GameOptions are the default ones. + /// </summary> + public bool IsDefaults { get; set; } + + /// <summary> + /// Deserialize a packet/message to a new GameOptionsData object. + /// </summary> + /// <param name="reader">Message reader object containing the raw message.</param> + /// <returns>GameOptionsData object.</returns> + public static GameOptionsData DeserializeCreate(IMessageReader reader) + { + var options = new GameOptionsData(); + options.Deserialize(reader.ReadBytesAndSize()); + return options; + } + + /// <summary> + /// Serializes this instance of GameOptionsData object to a specified BinaryWriter. + /// </summary> + /// <param name="writer">The stream to write the message to.</param> + /// <param name="version">The version of the game.</param> + public void Serialize(BinaryWriter writer, byte version) + { + writer.Write((byte)version); + writer.Write((byte)MaxPlayers); + writer.Write((uint)Keywords); + writer.Write((byte)MapId); + writer.Write((float)PlayerSpeedMod); + writer.Write((float)CrewLightMod); + writer.Write((float)ImpostorLightMod); + writer.Write((float)KillCooldown); + writer.Write((byte)NumCommonTasks); + writer.Write((byte)NumLongTasks); + writer.Write((byte)NumShortTasks); + writer.Write((int)NumEmergencyMeetings); + writer.Write((byte)NumImpostors); + writer.Write((byte)KillDistance); + writer.Write((uint)DiscussionTime); + writer.Write((uint)VotingTime); + writer.Write((bool)IsDefaults); + + if (version > 1) + { + writer.Write((byte)EmergencyCooldown); + } + + if (version > 2) + { + writer.Write((bool)ConfirmImpostor); + writer.Write((bool)VisualTasks); + } + + if (version > 3) + { + writer.Write((bool)AnonymousVotes); + writer.Write((byte)TaskBarUpdate); + } + + if (version > 4) + { + throw new ImpostorException($"Unknown GameOptionsData version {Version}."); + } + } + + /// <summary> + /// Deserialize a ReadOnlyMemory object to this instance of the GameOptionsData object. + /// </summary> + /// <param name="memory">Memory containing the message/packet.</param> + public void Deserialize(ReadOnlyMemory<byte> memory) + { + var bytes = memory.Span; + + Version = bytes.ReadByte(); + MaxPlayers = bytes.ReadByte(); + Keywords = (GameKeywords)bytes.ReadUInt32(); + MapId = bytes.ReadByte(); + PlayerSpeedMod = bytes.ReadSingle(); + + CrewLightMod = bytes.ReadSingle(); + ImpostorLightMod = bytes.ReadSingle(); + KillCooldown = bytes.ReadSingle(); + + NumCommonTasks = bytes.ReadByte(); + NumLongTasks = bytes.ReadByte(); + NumShortTasks = bytes.ReadByte(); + + NumEmergencyMeetings = bytes.ReadInt32(); + + NumImpostors = bytes.ReadByte(); + KillDistance = (KillDistances)bytes.ReadByte(); + DiscussionTime = bytes.ReadInt32(); + VotingTime = bytes.ReadInt32(); + + IsDefaults = bytes.ReadBoolean(); + + if (Version > 1) + { + EmergencyCooldown = bytes.ReadByte(); + } + + if (Version > 2) + { + ConfirmImpostor = bytes.ReadBoolean(); + VisualTasks = bytes.ReadBoolean(); + } + + if (Version > 3) + { + AnonymousVotes = bytes.ReadBoolean(); + TaskBarUpdate = (TaskBarUpdate)bytes.ReadByte(); + } + + if (Version > 4) + { + throw new ImpostorException($"Unknown GameOptionsData version {Version}."); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs new file mode 100644 index 0000000..6a95d37 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameOverReason.cs @@ -0,0 +1,15 @@ +namespace Impostor.Api.Innersloth +{ + public enum GameOverReason : byte + { + HumansByVote = 0, + HumansByTask = 1, + ImpostorByVote = 2, + ImpostorByKill = 3, + ImpostorBySabotage = 4, + + // Unused (?) + ImpostorDisconnect = 5, + HumansDisconnect = 6, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs new file mode 100644 index 0000000..f5aaa9c --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameStates.cs @@ -0,0 +1,11 @@ +namespace Impostor.Api.Innersloth +{ + public enum GameStates : byte + { + NotStarted = 0, + Starting = 1, + Started = 2, + Ended = 3, + Destroyed = 4, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs b/Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs new file mode 100644 index 0000000..c11933e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/GameVersion.cs @@ -0,0 +1,10 @@ +namespace Impostor.Api.Innersloth +{ + public class GameVersion + { + public static int GetVersion(int year, int month, int day, int rev = 0) + { + return (year * 25000) + (month * 1800) + (day * 50) + rev; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs b/Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs new file mode 100644 index 0000000..b2ee5d9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/KillDistances.cs @@ -0,0 +1,12 @@ +using System; + +namespace Impostor.Api.Innersloth +{ + [Flags] + public enum KillDistances : byte + { + Short = 0, + Normal = 1, + Long = 2, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs b/Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs new file mode 100644 index 0000000..ad462a2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/MapFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace Impostor.Api.Innersloth +{ + [Flags] + public enum MapFlags + { + Skeld = 1, + MiraHQ = 2, + Polus = 4, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs b/Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs new file mode 100644 index 0000000..8dc07b5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/MapTypes.cs @@ -0,0 +1,9 @@ +namespace Impostor.Api.Innersloth +{ + public enum MapTypes + { + Skeld = 0, + MiraHQ = 1, + Polus = 2, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs b/Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs new file mode 100644 index 0000000..c78978b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/RegionInfo.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; + +namespace Impostor.Api.Innersloth +{ + public class RegionInfo + { + public RegionInfo(string name, string ping, IReadOnlyList<ServerInfo> servers) + { + Name = name; + Ping = ping; + Servers = servers; + } + + public string Name { get; } + public string Ping { get; } + public IReadOnlyList<ServerInfo> Servers { get; } + + public void Serialize(BinaryWriter writer) + { + writer.Write(0); + writer.Write(Name); + writer.Write(Ping); + writer.Write(Servers.Count); + + foreach (var server in Servers) + { + server.Serialize(writer); + } + } + + public static RegionInfo Deserialize(BinaryReader reader) + { + var unknown = reader.ReadInt32(); + var name = reader.ReadString(); + var ping = reader.ReadString(); + var servers = new List<ServerInfo>(); + var serverCount = reader.ReadInt32(); + + for (var i = 0; i < serverCount; i++) + { + servers.Add(ServerInfo.Deserialize(reader)); + } + + return new RegionInfo(name, ping, servers); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs b/Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs new file mode 100644 index 0000000..7785823 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/ServerInfo.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Net; + +namespace Impostor.Api.Innersloth +{ + public class ServerInfo + { + public string Name { get; } + public string Ip { get; } + public ushort Port { get; } + + public ServerInfo(string name, string ip, ushort port) + { + Name = name; + Ip = ip; + Port = port; + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(Name); + writer.Write(IPAddress.Parse(Ip).GetAddressBytes()); + writer.Write(Port); + writer.Write(0); + } + + public static ServerInfo Deserialize(BinaryReader reader) + { + var name = reader.ReadString(); + var ip = new IPAddress(reader.ReadBytes(4)).ToString(); + var port = reader.ReadUInt16(); + var unknown = reader.ReadInt32(); + + return new ServerInfo(name, ip, port); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs new file mode 100644 index 0000000..ad88c28 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypeHelpers.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; + +namespace Impostor.Api.Innersloth +{ + internal class SystemTypeHelpers + { + public static readonly SystemTypes[] AllTypes; + public static readonly string[] Names; + + static SystemTypeHelpers() + { + AllTypes = Enum.GetValues(typeof(SystemTypes)).Cast<SystemTypes>().ToArray(); + Names = AllTypes.Select(x => + { + return x switch + { + SystemTypes.UpperEngine => "Upper Engine", + SystemTypes.Nav => "Navigations", + SystemTypes.LifeSupp => "O2", + SystemTypes.LowerEngine => "Lower Engine", + SystemTypes.LockerRoom => "Locker Room", + _ => x.ToString() + }; + }).ToArray(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs new file mode 100644 index 0000000..7f91718 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/SystemTypes.cs @@ -0,0 +1,42 @@ +namespace Impostor.Api.Innersloth +{ + public enum SystemTypes : byte + { + Hallway = 0, + Storage = 1, + Cafeteria = 2, + Reactor = 3, + UpperEngine = 4, + Nav = 5, + Admin = 6, + Electrical = 7, + LifeSupp = 8, + Shields = 9, + MedBay = 10, + Security = 11, + Weapons = 12, + LowerEngine = 13, + Comms = 14, + ShipTasks = 15, + Doors = 16, + Sabotage = 17, + /// <summary> + /// Decontam on Mira and bottom decontam on Polus + /// </summary> + Decontamination = 18, + Launchpad = 19, + LockerRoom = 20, + Laboratory = 21, + Balcony = 22, + Office = 23, + Greenhouse = 24, + Dropship = 25, + /// <summary> + /// Top decontam on Polus + /// </summary> + Decontamination2 = 26, + Outside = 27, + Specimens = 28, + BoilerRoom = 29 + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs b/Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs new file mode 100644 index 0000000..f4d7c1f --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/TaskBarUpdate.cs @@ -0,0 +1,9 @@ +namespace Impostor.Api.Innersloth +{ + public enum TaskBarUpdate : byte + { + Always = 0, + Meetings = 1, + Never = 2 + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs b/Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs new file mode 100644 index 0000000..8b15354 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/TaskTypes.cs @@ -0,0 +1,49 @@ +namespace Impostor.Api.Innersloth +{ + public enum TaskTypes : uint + { + SubmitScan = 0, + PrimeShields = 1, + FuelEngines = 2, + ChartCourse = 3, + StartReactor = 4, + SwipeCard = 5, + ClearAsteroids = 6, + UploadData = 7, + InspectSample = 8, + EmptyChute = 9, + EmptyGarbage = 10, + AlignEngineOutput = 11, + FixWiring = 12, + CalibrateDistributor = 13, + DivertPower = 14, + UnlockManifolds = 15, + ResetReactor = 16, + FixLights = 17, + Filter = 18, + FixComms = 19, + RestoreOxy = 20, + StabilizeSteering = 21, + AssembleArtifact = 22, + SortSamples = 23, + MeasureWeather = 24, + EnterIdCode = 25, + BuyBeverage = 26, + ProcessData = 27, + RunDiagnostics = 28, + WaterPlants = 29, + MonitorOxygen = 30, + StoreArtifact = 31, + FillCanisters = 32, + ActivateWeatherNodes = 33, + InsertKeys = 34, + ResetSeismic = 35, + ScanBoardingPass = 36, + OpenWaterways = 37, + ReplaceWaterJug = 38, + RepairDrill = 39, + AlignTelescope = 40, + RecordTemperature = 41, + RebootWifi = 42, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs b/Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs new file mode 100644 index 0000000..9533d83 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/TextBox.cs @@ -0,0 +1,10 @@ +namespace Impostor.Api.Innersloth +{ + public static class TextBox + { + public static bool IsCharAllowed(char i) + { + return i == ' ' || (i >= 'A' && i <= 'Z') || (i >= 'a' && i <= 'z') || (i >= '0' && i <= '9') || (i >= 'À' && i <= 'ÿ') || (i >= 'Ѐ' && i <= 'џ') || (i >= 'ㄱ' && i <= 'ㆎ') || (i >= '가' && i <= '힣'); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs b/Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs new file mode 100644 index 0000000..f9b8567 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Innersloth/VentLocation.cs @@ -0,0 +1,48 @@ +namespace Impostor.Api.Innersloth +{ + public enum VentLocation : uint + { + // Skeld + SkeldAdmin = 0, + SkeldRightHallway = 1, + SkeldCafeteria = 2, + SkeldElectrical = 3, + SkeldUpperEngine = 4, + SkeldSecurity = 5, + SkeldMedbay = 6, + SkeldWeapons = 7, + SkeldLowerReactor = 8, + SkeldLowerEngine = 9, + SkeldShields = 10, + SkeldUpperReactor = 11, + SkeldUpperNavigation = 12, + SkeldLowerNavigation = 13, + + // Mira HQ + MiraBalcony = 1, + MiraCafeteria = 2, + MiraReactor = 3, + MiraLaboratory = 4, + MiraOffice = 5, + MiraAdmin = 6, + MiraGreenhouse = 7, + MiraMedbay = 8, + MiraDecontamination = 9, + MiraLockerRoom = 10, + MiraLaunchpad = 11, + + // Polus + PolusSecurity = 0, + PolusElectrical = 1, + PolusO2 = 2, + PolusCommunications = 3, + PolusOffice = 4, + PolusAdmin = 5, + PolusLaboratory = 6, + PolusLava = 7, + PolusStorage = 8, + PolusRightStabilizer = 9, + PolusLeftStabilizer = 10, + PolusOutsideAdmin = 11, + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/IClient.cs b/Impostor-dev/src/Impostor.Api/Net/IClient.cs new file mode 100644 index 0000000..48efeda --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/IClient.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Net +{ + /// <summary> + /// Represents a connected game client. + /// </summary> + public interface IClient + { + /// <summary> + /// Gets or sets the unique ID of the client. + /// </summary> + /// <remarks> + /// This ID is generated when the client is registered in the client manager and should not be used + /// to store persisted data. + /// </remarks> + int Id { get; set; } + + /// <summary> + /// Gets the name that was provided by the player in the client. + /// </summary> + /// <remarks> + /// The name is provided by the player and should not be used to store persisted data. + /// </remarks> + string Name { get; } + + /// <summary> + /// Gets the connection of the client. + /// </summary> + /// <remarks> + /// Null when the client was not registered by the matchmaker. + /// </remarks> + IHazelConnection? Connection { get; } + + /// <summary> + /// Gets a key/value collection that can be used to share data between messages. + /// </summary> + /// <remarks> + /// <para> + /// The stored data will not be saved. + /// After the connection has been closed all data will be lost. + /// </para> + /// <para> + /// Note that the values will not be disposed after the connection has been closed. + /// This has to be implemented by the plugin. + /// </para> + /// </remarks> + IDictionary<object, object> Items { get; } + + /// <summary> + /// Gets or sets the current game data of the <see cref="IClient"/>. + /// </summary> + IClientPlayer? Player { get; } + + ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType); + + ValueTask HandleDisconnectAsync(string reason); + + /// <summary> + /// Disconnect the client with a <see cref="DisconnectReason"/>. + /// </summary> + /// <param name="reason"> + /// The message to show to the player. + /// </param> + /// <param name="message"> + /// Only used when <see cref="reason"/> is set to <see cref="DisconnectReason.Custom"/>. + /// </param> + /// <returns> + /// A <see cref="ValueTask"/> representing the asynchronous operation. + /// </returns> + ValueTask DisconnectAsync(DisconnectReason reason, string? message = null); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs b/Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs new file mode 100644 index 0000000..6070210 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/IClientPlayer.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Net +{ + /// <summary> + /// Represents a player in <see cref="IGame"/>. + /// </summary> + public interface IClientPlayer + { + /// <summary> + /// Gets the client that belongs to the player. + /// </summary> + IClient Client { get; } + + /// <summary> + /// Gets the game where the <see cref="IClientPlayer"/> belongs to. + /// </summary> + IGame Game { get; } + + /// <summary> + /// Gets or sets the current limbo state of the player. + /// </summary> + LimboStates Limbo { get; set; } + + IInnerPlayerControl? Character { get; } + + public bool IsHost { get; } + + /// <summary> + /// Checks if the specified <see cref="IInnerNetObject"/> is owned by <see cref="IClientPlayer"/>. + /// </summary> + /// <param name="netObject">The <see cref="IInnerNetObject"/>.</param> + /// <returns>Returns true if owned by <see cref="IClientPlayer"/>.</returns> + bool IsOwner(IInnerNetObject netObject); + + ValueTask KickAsync(); + + ValueTask BanAsync(); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/IConnection.cs b/Impostor-dev/src/Impostor.Api/Net/IConnection.cs new file mode 100644 index 0000000..94f9b8b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/IConnection.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Net +{ + public interface IConnection + { + + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs b/Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs new file mode 100644 index 0000000..4e6c4b3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/IHazelConnection.cs @@ -0,0 +1,41 @@ +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Net +{ + /// <summary> + /// Represents the connection of the client. + /// </summary> + public interface IHazelConnection + { + /// <summary> + /// Gets the IP endpoint of the client. + /// </summary> + IPEndPoint EndPoint { get; } + + /// <summary> + /// Gets a value indicating whether the client is connected to the server. + /// </summary> + bool IsConnected { get; } + + /// <summary> + /// Gets the client of the connection. + /// </summary> + IClient? Client { get; set; } + + /// <summary> + /// Sends a message writer to the connection. + /// </summary> + /// <param name="writer">The message.</param> + /// <returns></returns> + ValueTask SendAsync(IMessageWriter writer); + + /// <summary> + /// Disconnects the client and invokes the disconnect handler. + /// </summary> + /// <param name="reason">A reason.</param> + /// <returns></returns> + ValueTask DisconnectAsync(string? reason); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs new file mode 100644 index 0000000..933a4de --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/IGameNet.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Api.Net.Inner +{ + /// <summary> + /// Holds all data that is serialized over the network through GameData packets. + /// </summary> + public interface IGameNet + { + IInnerLobbyBehaviour LobbyBehaviour { get; } + + IInnerGameData GameData { get; } + + IInnerVoteBanSystem VoteBan { get; } + + IInnerShipStatus ShipStatus { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs new file mode 100644 index 0000000..c171377 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/IInnerNetObject.cs @@ -0,0 +1,9 @@ +namespace Impostor.Api.Net.Inner +{ + public interface IInnerNetObject + { + public uint NetId { get; } + + public int OwnerId { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs new file mode 100644 index 0000000..6d867e7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerCustomNetworkTransform.cs @@ -0,0 +1,15 @@ +using System.Numerics; +using System.Threading.Tasks; + +namespace Impostor.Api.Net.Inner.Objects.Components +{ + public interface IInnerCustomNetworkTransform : IInnerNetObject + { + /// <summary> + /// Snaps the current to the given position <see cref="IInnerPlayerControl"/>. + /// </summary> + /// <param name="position">The target position.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SnapToAsync(Vector2 position); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs new file mode 100644 index 0000000..9378c5b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/Components/IInnerPlayerPhysics.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Net.Inner.Objects.Components +{ + public interface IInnerPlayerPhysics : IInnerNetObject + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs new file mode 100644 index 0000000..6e41020 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerGameData.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerGameData : IInnerNetObject + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs new file mode 100644 index 0000000..f05f4cf --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerLobbyBehaviour.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerLobbyBehaviour : IInnerNetObject + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs new file mode 100644 index 0000000..9c89d05 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerMeetingHud.cs @@ -0,0 +1,6 @@ +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerMeetingHud + { + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs new file mode 100644 index 0000000..04558b9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerControl.cs @@ -0,0 +1,116 @@ +using System.Threading.Tasks; +using Impostor.Api.Innersloth.Customization; +using Impostor.Api.Net.Inner.Objects.Components; + +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerPlayerControl : IInnerNetObject + { + /// <summary> + /// Gets the <see cref="PlayerId"/> assigned by the client of the host of the game. + /// </summary> + byte PlayerId { get; } + + /// <summary> + /// Gets the <see cref="IInnerPlayerPhysics"/> of the <see cref="IInnerPlayerControl"/>. + /// Contains vent logic. + /// </summary> + IInnerPlayerPhysics Physics { get; } + + /// <summary> + /// Gets the <see cref="IInnerCustomNetworkTransform"/> of the <see cref="IInnerPlayerControl"/>. + /// Contains position data about the player. + /// </summary> + IInnerCustomNetworkTransform NetworkTransform { get; } + + /// <summary> + /// Gets the <see cref="IInnerPlayerInfo"/> of the <see cref="IInnerPlayerControl"/>. + /// Contains metadata about the player. + /// </summary> + IInnerPlayerInfo PlayerInfo { get; } + + /// <summary> + /// Sets the name of the current <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// <param name="name">A name for the player.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SetNameAsync(string name); + + /// <summary> + /// Sets the color of the current <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// <param name="colorId">A color for the player.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SetColorAsync(byte colorId); + + /// <param name="colorType">A color for the player.</param> + /// <inheritdoc cref="SetColorAsync(byte)" /> + ValueTask SetColorAsync(ColorType colorType); + + /// <summary> + /// Sets the hat of the current <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// <param name="hatId">An hat for the player.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SetHatAsync(uint hatId); + + /// <param name="hatType">An hat for the player.</param> + /// <inheritdoc cref="SetHatAsync(uint)" /> + ValueTask SetHatAsync(HatType hatType); + + /// <summary> + /// Sets the pet of the current <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// <param name="petId">A pet for the player.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SetPetAsync(uint petId); + + /// <param name="petType">A pet for the player.</param> + /// <inheritdoc cref="SetPetAsync(uint)" /> + ValueTask SetPetAsync(PetType petType); + + /// <summary> + /// Sets the skin of the current <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// <param name="skinId">A skin for the player.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SetSkinAsync(uint skinId); + + /// <param name="skinType">A skin for the player.</param> + /// <inheritdoc cref="SetSkinAsync(uint)" /> + ValueTask SetSkinAsync(SkinType skinType); + + /// <summary> + /// Send a chat message as the current <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// <param name="text">The message to send.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SendChatAsync(string text); + + /// <summary> + /// Send a chat message as the current <see cref="IInnerPlayerControl"/>. + /// Visible to only the current. + /// </summary> + /// <param name="text">The message to send.</param> + /// <param name="player"> + /// The player that should receive this chat message. + /// When left as null, will send message to self. + /// </param> + /// <returns>Task that must be awaited.</returns> + ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null); + + /// <summary> + /// Sets the current to be murdered by an impostor <see cref="IInnerPlayerControl"/>. + /// Visible to all players. + /// </summary> + /// /// <param name="impostor">The Impostor who kill.</param> + /// <returns>Task that must be awaited.</returns> + ValueTask SetMurderedByAsync(IClientPlayer impostor); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs new file mode 100644 index 0000000..6cb3302 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerPlayerInfo.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerPlayerInfo + { + /// <summary> + /// Gets the name of the player as decided by the host. + /// </summary> + string PlayerName { get; } + + /// <summary> + /// Gets the color of the player. + /// </summary> + byte ColorId { get; } + + /// <summary> + /// Gets the hat of the player. + /// </summary> + uint HatId { get; } + + /// <summary> + /// Gets the pet of the player. + /// </summary> + uint PetId { get; } + + /// <summary> + /// Gets the skin of the player. + /// </summary> + uint SkinId { get; } + + /// <summary> + /// Gets a value indicating whether the player is an impostor. + /// </summary> + bool IsImpostor { get; } + + /// <summary> + /// Gets a value indicating whether the player is a dead in the current game. + /// </summary> + bool IsDead { get; } + + /// <summary> + /// Gets the reason why the player is dead in the current game. + /// </summary> + DeathReason LastDeathReason { get; } + + IEnumerable<ITaskInfo> Tasks { get; } + + DateTimeOffset LastMurder { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs new file mode 100644 index 0000000..c0a05ae --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerShipStatus.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerShipStatus : IInnerNetObject + { + + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs new file mode 100644 index 0000000..d0a816d --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/IInnerVoteBanSystem.cs @@ -0,0 +1,7 @@ +namespace Impostor.Api.Net.Inner.Objects +{ + public interface IInnerVoteBanSystem : IInnerNetObject + { + + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs new file mode 100644 index 0000000..2b6dd86 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Inner/Objects/ITaskInfo.cs @@ -0,0 +1,14 @@ +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; + +namespace Impostor.Api.Net.Inner.Objects +{ + public interface ITaskInfo + { + uint Id { get; } + + TaskTypes Type { get; } + + bool Complete { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/LimboStates.cs b/Impostor-dev/src/Impostor.Api/Net/LimboStates.cs new file mode 100644 index 0000000..44c493e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/LimboStates.cs @@ -0,0 +1,13 @@ +using System; + +namespace Impostor.Api.Net +{ + [Flags] + public enum LimboStates + { + PreSpawn = 1, + NotLimbo = 2, + WaitingForHost = 4, + All = PreSpawn | NotLimbo | WaitingForHost, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs b/Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs new file mode 100644 index 0000000..92bf89f --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Manager/IClientManager.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Impostor.Api.Net.Manager +{ + public interface IClientManager + { + IEnumerable<IClient> Clients { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs new file mode 100644 index 0000000..4f5b39c --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message00HostGameC2S.cs @@ -0,0 +1,27 @@ +using System.IO; +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.C2S +{ + public static class Message00HostGameC2S + { + public static void Serialize(IMessageWriter writer, GameOptionsData gameOptionsData) + { + writer.StartMessage(MessageFlags.HostGame); + + using (var memory = new MemoryStream()) + using (var writerBin = new BinaryWriter(memory)) + { + gameOptionsData.Serialize(writerBin, GameOptionsData.LatestVersion); + writer.WriteBytesAndSize(memory.ToArray()); + } + + writer.EndMessage(); + } + + public static GameOptionsData Deserialize(IMessageReader reader) + { + return GameOptionsData.DeserializeCreate(reader); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs new file mode 100644 index 0000000..f121b97 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message01JoinGameC2S.cs @@ -0,0 +1,20 @@ +using System; + +namespace Impostor.Api.Net.Messages.C2S +{ + public static class Message01JoinGameC2S + { + public static void Serialize(IMessageWriter writer) + { + throw new System.NotImplementedException(); + } + + public static void Deserialize(IMessageReader reader, out int gameCode, out byte unknown) + { + var slice = reader.ReadBytes(sizeof(Int32) + sizeof(byte)).Span; + + gameCode = slice.ReadInt32(); + unknown = slice.ReadByte(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs new file mode 100644 index 0000000..99cdcfa --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message04RemovePlayerC2S.cs @@ -0,0 +1,16 @@ +namespace Impostor.Api.Net.Messages.C2S +{ + public class Message04RemovePlayerC2S + { + public static void Serialize(IMessageWriter writer) + { + throw new System.NotImplementedException(); + } + + public static void Deserialize(IMessageReader reader, out int playerId, out byte reason) + { + playerId = reader.ReadPackedInt32(); + reason = reader.ReadByte(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs new file mode 100644 index 0000000..7ca5e3a --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message08EndGameC2S.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.C2S +{ + public class Message08EndGameC2S + { + public static void Serialize(IMessageWriter writer) + { + throw new System.NotImplementedException(); + } + + public static void Deserialize(IMessageReader reader, out GameOverReason gameOverReason) + { + gameOverReason = (GameOverReason)reader.ReadByte(); + reader.ReadBoolean(); // showAd + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs new file mode 100644 index 0000000..330f3b5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message10AlterGameC2S.cs @@ -0,0 +1,20 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.C2S +{ + public class Message10AlterGameC2S + { + public static void Serialize(IMessageWriter writer) + { + throw new System.NotImplementedException(); + } + + public static void Deserialize(IMessageReader reader, out AlterGameTags gameTag, out bool isPublic) + { + var slice = reader.ReadBytes(sizeof(byte) + sizeof(byte)).Span; + + gameTag = (AlterGameTags)slice.ReadByte(); + isPublic = slice.ReadBoolean(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs new file mode 100644 index 0000000..7c5b8b9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message11KickPlayerC2S.cs @@ -0,0 +1,16 @@ +namespace Impostor.Api.Net.Messages.C2S +{ + public class Message11KickPlayerC2S + { + public static void Serialize(IMessageWriter writer) + { + throw new System.NotImplementedException(); + } + + public static void Deserialize(IMessageReader reader, out int playerId, out bool isBan) + { + playerId = reader.ReadPackedInt32(); + isBan = reader.ReadBoolean(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs new file mode 100644 index 0000000..2b7e12a --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/C2S/Message16GetGameListC2S.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.C2S +{ + public class Message16GetGameListC2S + { + public static void Serialize(IMessageWriter writer) + { + throw new System.NotImplementedException(); + } + + public static void Deserialize(IMessageReader reader, out GameOptionsData options) + { + reader.ReadPackedInt32(); // Hardcoded 0. + options = GameOptionsData.DeserializeCreate(reader); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs new file mode 100644 index 0000000..87c06c4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageReader.cs @@ -0,0 +1,68 @@ +using System; + +namespace Impostor.Api.Net.Messages +{ + public interface IMessageReader : IDisposable + { + /// <summary> + /// Gets the tag of the message. + /// </summary> + byte Tag { get; } + + /// <summary> + /// Gets the buffer of the message. + /// </summary> + byte[] Buffer { get; } + + /// <summary> + /// Gets the offset of our current <see cref="IMessageReader"/> in the entire <see cref="Buffer"/>. + /// </summary> + int Offset { get; } + + /// <summary> + /// Gets the current position of the reader. + /// </summary> + int Position { get; } + + /// <summary> + /// Gets the length of the buffer. + /// </summary> + int Length { get; } + + IMessageReader ReadMessage(); + + bool ReadBoolean(); + + sbyte ReadSByte(); + + byte ReadByte(); + + ushort ReadUInt16(); + + short ReadInt16(); + + uint ReadUInt32(); + + int ReadInt32(); + + float ReadSingle(); + + string ReadString(); + + ReadOnlyMemory<byte> ReadBytesAndSize(); + + ReadOnlyMemory<byte> ReadBytes(int length); + + int ReadPackedInt32(); + + uint ReadPackedUInt32(); + + void CopyTo(IMessageWriter writer); + + void Seek(int position); + + void RemoveMessage(IMessageReader message); + + IMessageReader Copy(int offset = 0); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs new file mode 100644 index 0000000..4f6765b --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriter.cs @@ -0,0 +1,127 @@ +using System; +using System.Net; +using Impostor.Api.Games; + +namespace Impostor.Api.Net.Messages +{ + /// <summary> + /// Base message writer. + /// </summary> + public interface IMessageWriter : IDisposable + { + public byte[] Buffer { get; } + + public int Length { get; set; } + + public int Position { get; set; } + + public MessageType SendOption { get; } + + /// <summary> + /// Writes a boolean to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(bool value); + + /// <summary> + /// Writes a sbyte to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(sbyte value); + + /// <summary> + /// Writes a byte to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(byte value); + + /// <summary> + /// Writes a short to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(short value); + + /// <summary> + /// Writes an ushort to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(ushort value); + + /// <summary> + /// Writes an uint to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(uint value); + + /// <summary> + /// Writes an int to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(int value); + + /// <summary> + /// Writes a float to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(float value); + + /// <summary> + /// Writes a string to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(string value); + + /// <summary> + /// Writes a <see cref="IPAddress"/> to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(IPAddress value); + + /// <summary> + /// Writes an packed int to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void WritePacked(int value); + + /// <summary> + /// Writes an packed uint to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void WritePacked(uint value); + + /// <summary> + /// Writes raw bytes to the message. + /// </summary> + /// <param name="data">Bytes to write.</param> + void Write(ReadOnlyMemory<byte> data); + + /// <summary> + /// Writes a game code to the message. + /// </summary> + /// <param name="value">Value to write.</param> + void Write(GameCode value); + + void WriteBytesAndSize(byte[] bytes); + + void WriteBytesAndSize(byte[] bytes, int length); + + void WriteBytesAndSize(byte[] bytes, int offset, int length); + + /// <summary> + /// Starts a new message. + /// </summary> + /// <param name="typeFlag">Message flag header.</param> + void StartMessage(byte typeFlag); + + /// <summary> + /// Mark the end of the message. + /// </summary> + void EndMessage(); + + /// <summary> + /// Clear the message writer. + /// </summary> + /// <param name="type">New type of the message.</param> + void Clear(MessageType type); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs new file mode 100644 index 0000000..f398939 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/IMessageWriterProvider.cs @@ -0,0 +1,16 @@ +namespace Impostor.Api.Net.Messages +{ + public interface IMessageWriterProvider + { + /// <summary> + /// Retrieves a <see cref="IMessageWriter"/> from the internal pool. + /// Make sure to call <see cref="IMessageWriter.Dispose"/> when you are done! + /// </summary> + /// <param name="sendOption"> + /// Whether to send the message as <see cref="MessageType.Reliable"/> or <see cref="MessageType.Unreliable"/>. + /// Reliable packets will ensure delivery while unreliable packets may be lost. + /// </param> + /// <returns>A <see cref="IMessageWriter"/> from the pool.</returns> + IMessageWriter Get(MessageType sendOption = MessageType.Unreliable); + } +} diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs new file mode 100644 index 0000000..aea0c60 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageFlags.cs @@ -0,0 +1,22 @@ +namespace Impostor.Api.Net.Messages +{ + public static class MessageFlags + { + public const byte HostGame = 0; + public const byte JoinGame = 1; + public const byte StartGame = 2; + public const byte RemoveGame = 3; + public const byte RemovePlayer = 4; + public const byte GameData = 5; + public const byte GameDataTo = 6; + public const byte JoinedGame = 7; + public const byte EndGame = 8; + public const byte AlterGame = 10; + public const byte KickPlayer = 11; + public const byte WaitForHost = 12; + public const byte Redirect = 13; + public const byte ReselectServer = 14; + public const byte GetGameList = 9; + public const byte GetGameListV2 = 16; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs new file mode 100644 index 0000000..1604358 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/MessageType.cs @@ -0,0 +1,32 @@ +using System; + +namespace Impostor.Api.Net.Messages +{ + /// <summary> + /// Specifies how a message should be sent between connections. + /// </summary> + [Flags] + public enum MessageType : byte + { + /// <summary> + /// Requests unreliable delivery with no fragmentation. + /// </summary> + /// <remarks> + /// Sending data using unreliable delivery means that data is not guaranteed to arrive at it's destination nor is + /// it guaranteed to arrive only once. However, unreliable delivery can be faster than other methods and it + /// typically requires a smaller number of protocol bytes than other methods. There is also typically less + /// processing involved and less memory needed as packets are not stored once sent. + /// </remarks> + Unreliable, + + /// <summary> + /// Requests data be sent reliably but with no fragmentation. + /// </summary> + /// <remarks> + /// Sending data reliably means that data is guaranteed to arrive and to arrive only once. Reliable delivery + /// typically requires more processing, more memory (as packets need to be stored in case they need resending), + /// a larger number of protocol bytes and can be slower than unreliable delivery. + /// </remarks> + Reliable, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs new file mode 100644 index 0000000..8402d10 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message00HostGameS2C.cs @@ -0,0 +1,20 @@ +using System; +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.S2C +{ + public static class Message00HostGameS2C + { + public static void Serialize(IMessageWriter writer, int gameCode) + { + writer.StartMessage(MessageFlags.HostGame); + writer.Write(gameCode); + writer.EndMessage(); + } + + public static GameOptionsData Deserialize(IMessageReader reader) + { + throw new NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs new file mode 100644 index 0000000..c455201 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message01JoinGameS2C.cs @@ -0,0 +1,50 @@ +using System; +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.S2C +{ + public class Message01JoinGameS2C + { + public static void SerializeJoin(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.JoinGame); + writer.Write(gameCode); + writer.Write(playerId); + writer.Write(hostId); + writer.EndMessage(); + } + + public static void SerializeError(IMessageWriter writer, bool clear, DisconnectReason reason, string? message = null) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.JoinGame); + writer.Write((int)reason); + + if (reason == DisconnectReason.Custom) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + + writer.Write(message); + } + + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs new file mode 100644 index 0000000..77b447d --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message04RemovePlayerS2C.cs @@ -0,0 +1,29 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.S2C +{ + public class Message04RemovePlayerS2C + { + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, DisconnectReason reason) + { + // Only a subset of DisconnectReason shows an unique message. + // ExitGame, Banned and Kicked. + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.RemovePlayer); + writer.Write(gameCode); + writer.Write(playerId); + writer.Write(hostId); + writer.Write((byte)reason); + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs new file mode 100644 index 0000000..da6eb40 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message07JoinedGameS2C.cs @@ -0,0 +1,31 @@ +namespace Impostor.Api.Net.Messages.S2C +{ + public static class Message07JoinedGameS2C + { + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, int hostId, int[] otherPlayerIds) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.JoinedGame); + writer.Write(gameCode); + writer.Write(playerId); + writer.Write(hostId); + writer.WritePacked(otherPlayerIds.Length); + + foreach (var id in otherPlayerIds) + { + writer.WritePacked(id); + } + + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs new file mode 100644 index 0000000..fa155df --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message10AlterGameS2C.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Innersloth; + +namespace Impostor.Api.Net.Messages.S2C +{ + public static class Message10AlterGameS2C + { + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, bool isPublic) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.AlterGame); + writer.Write(gameCode); + writer.Write((byte)AlterGameTags.ChangePrivacy); + writer.Write(isPublic); + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs new file mode 100644 index 0000000..1e2b6ef --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message11KickPlayerS2C.cs @@ -0,0 +1,24 @@ +namespace Impostor.Api.Net.Messages.S2C +{ + public class Message11KickPlayerS2C + { + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId, bool isBan) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.KickPlayer); + writer.Write(gameCode); + writer.WritePacked(playerId); + writer.Write(isBan); + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs new file mode 100644 index 0000000..5964b1c --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message12WaitForHostS2C.cs @@ -0,0 +1,23 @@ +namespace Impostor.Api.Net.Messages.S2C +{ + public class Message12WaitForHostS2C + { + public static void Serialize(IMessageWriter writer, bool clear, int gameCode, int playerId) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.WaitForHost); + writer.Write(gameCode); + writer.Write(playerId); + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs new file mode 100644 index 0000000..4b93b0e --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message13RedirectS2C.cs @@ -0,0 +1,25 @@ +using System.Net; + +namespace Impostor.Api.Net.Messages.S2C +{ + public class Message13RedirectS2C + { + public static void Serialize(IMessageWriter writer, bool clear, IPEndPoint ipEndPoint) + { + if (clear) + { + writer.Clear(MessageType.Reliable); + } + + writer.StartMessage(MessageFlags.Redirect); + writer.Write(ipEndPoint.Address); + writer.Write((ushort)ipEndPoint.Port); + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs new file mode 100644 index 0000000..93386d7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Net/Messages/S2C/Message16GetGameListS2C.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using Impostor.Api.Games; + +namespace Impostor.Api.Net.Messages.S2C +{ + public class Message16GetGameListS2C + { + public static void Serialize(IMessageWriter writer, int skeldGameCount, int miraHqGameCount, int polusGameCount, IEnumerable<IGame> games) + { + writer.StartMessage(MessageFlags.GetGameListV2); + + // Count + writer.StartMessage(1); + writer.Write(skeldGameCount); // The Skeld + writer.Write(miraHqGameCount); // Mira HQ + writer.Write(polusGameCount); // Polus + writer.EndMessage(); + + // Listing + writer.StartMessage(0); + + foreach (var game in games) + { + writer.StartMessage(0); + writer.Write(game.PublicIp.Address); + writer.Write((ushort)game.PublicIp.Port); + writer.Write(game.Code); + writer.Write(game.Host.Client.Name); + writer.Write((byte)game.PlayerCount); + writer.WritePacked(1); // TODO: What does Age do? + writer.Write((byte)game.Options.MapId); + writer.Write((byte)game.Options.NumImpostors); + writer.Write((byte)game.Options.MaxPlayers); + writer.EndMessage(); + } + + writer.EndMessage(); + writer.EndMessage(); + } + + public static void Deserialize(IMessageReader reader) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs b/Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs new file mode 100644 index 0000000..fd0f38f --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Plugins/IPlugin.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Api.Plugins +{ + public interface IPlugin : IEventListener + { + ValueTask EnableAsync(); + + ValueTask DisableAsync(); + + ValueTask ReloadAsync(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs b/Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs new file mode 100644 index 0000000..aa6a35f --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Plugins/IPluginStartup.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Api.Plugins +{ + public interface IPluginStartup + { + void ConfigureHost(IHostBuilder host); + + void ConfigureServices(IServiceCollection services); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs b/Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs new file mode 100644 index 0000000..b31bd47 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Plugins/ImpostorPluginAttribute.cs @@ -0,0 +1,24 @@ +using System; + +namespace Impostor.Api.Plugins +{ + [AttributeUsage(AttributeTargets.Class)] + public class ImpostorPluginAttribute : Attribute + { + public ImpostorPluginAttribute(string package, string name, string author, string version) + { + Package = package; + Name = name; + Author = author; + Version = version; + } + + public string Package { get; } + + public string Name { get; } + + public string Author { get; } + + public string Version { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs b/Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs new file mode 100644 index 0000000..0384363 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Plugins/PluginBase.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; + +namespace Impostor.Api.Plugins +{ + public class PluginBase : IPlugin + { + public virtual ValueTask EnableAsync() + { + return default; + } + + public virtual ValueTask DisableAsync() + { + return default; + } + + public virtual ValueTask ReloadAsync() + { + return default; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/ProjectRules.ruleset b/Impostor-dev/src/Impostor.Api/ProjectRules.ruleset new file mode 100644 index 0000000..4ba23c2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/ProjectRules.ruleset @@ -0,0 +1,17 @@ +<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0"> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules"> + <Rule Id="SA1200" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules"> + <Rule Id="SA1600" Action="None" /> + <Rule Id="SA1601" Action="None" /> + <Rule Id="SA1602" Action="None" /> + <Rule Id="SA1633" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules"> + <Rule Id="SA1101" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules"> + <Rule Id="SA1309" Action="None" /> + </Rules> +</RuleSet>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs b/Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c02e44a --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Impostor.Server")]
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Api/Unity/Mathf.cs b/Impostor-dev/src/Impostor.Api/Unity/Mathf.cs new file mode 100644 index 0000000..4b03417 --- /dev/null +++ b/Impostor-dev/src/Impostor.Api/Unity/Mathf.cs @@ -0,0 +1,54 @@ +namespace Impostor.Api.Unity +{ + public static class Mathf + { + /// <summary> + /// <para>Clamps the given value between the given minimum float and maximum float values. Returns the given value if it is within the min and max range.</para> + /// </summary> + /// <param name="value">The floating point value to restrict inside the range defined by the min and max values.</param> + /// <param name="min">The minimum floating point value to compare against.</param> + /// <param name="max">The maximum floating point value to compare against.</param> + /// <returns> + /// <para>The float result between the min and max values.</para> + /// </returns> + public static float Clamp(float value, float min, float max) + { + if (value < (double)min) + { + value = min; + } + else if (value > (double)max) + { + value = max; + } + + return value; + } + + /// <summary> + /// <para>Clamps value between 0 and 1 and returns value.</para> + /// </summary> + /// <param name="value">Value.</param> + /// <returns>Clamped value.</returns> + public static float Clamp01(float value) + { + if (value < 0.0) + { + return 0.0f; + } + + return (double)value > 1.0 ? 1f : value; + } + + /// <summary> + /// <para>Linearly interpolates between a and b by t.</para> + /// </summary> + /// <param name="a">The start value.</param> + /// <param name="b">The end value.</param> + /// <param name="t">The interpolation value between the two floats.</param> + /// <returns> + /// <para>The interpolated float result between the two float values.</para> + /// </returns> + public static float Lerp(float a, float b, float t) => a + ((b - a) * Clamp01(t)); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Benchmarks/.gitignore b/Impostor-dev/src/Impostor.Benchmarks/.gitignore new file mode 100644 index 0000000..1c2dac6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/.gitignore @@ -0,0 +1 @@ +BenchmarkDotNet.Artifacts
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs new file mode 100644 index 0000000..2aba724 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes.cs @@ -0,0 +1,172 @@ +using System; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Impostor.Benchmarks.Data +{ + public class MessageReader_Bytes + { + public byte Tag { get; } + public byte[] Buffer { get; } + public int Position { get; set; } + public int Length { get; set; } + public int BytesRemaining => this.Length - this.Position; + + public MessageReader_Bytes(byte[] buffer, int position = 0, int length = 0) + { + Tag = byte.MaxValue; + Buffer = buffer; + Position = position; + Length = length; + } + + public MessageReader_Bytes(byte tag, byte[] buffer, int position = 0, int length = 0) + { + Tag = tag; + Buffer = buffer; + Position = position; + Length = length; + } + + public MessageReader_Bytes ReadMessage() + { + var length = ReadUInt16(); + var tag = FastByte(); + var pos = Position; + + Position += length; + return new MessageReader_Bytes(tag, Buffer, pos, length); + } + + public bool ReadBoolean() + { + byte val = FastByte(); + return val != 0; + } + + public sbyte ReadSByte() + { + return (sbyte)FastByte(); + } + + public byte ReadByte() + { + return FastByte(); + } + + public ushort ReadUInt16() + { + return (ushort)(this.FastByte() | + this.FastByte() << 8); + } + + public short ReadInt16() + { + return (short)(this.FastByte() | + this.FastByte() << 8); + } + + public uint ReadUInt32() + { + return this.FastByte() + | (uint)this.FastByte() << 8 + | (uint)this.FastByte() << 16 + | (uint)this.FastByte() << 24; + } + + public int ReadInt32() + { + return this.FastByte() + | this.FastByte() << 8 + | this.FastByte() << 16 + | this.FastByte() << 24; + } + + public unsafe float ReadSingle() + { + float output = 0; + fixed (byte* bufPtr = &this.Buffer[Position]) + { + byte* outPtr = (byte*)&output; + + *outPtr = *bufPtr; + *(outPtr + 1) = *(bufPtr + 1); + *(outPtr + 2) = *(bufPtr + 2); + *(outPtr + 3) = *(bufPtr + 3); + } + + this.Position += 4; + return output; + } + + public string ReadString() + { + var len = this.ReadPackedInt32(); + + if (this.BytesRemaining < len) + { + throw new InvalidDataException($"Read length is longer than message length: {len} of {this.BytesRemaining}"); + } + + var output = Encoding.UTF8.GetString(this.Buffer, Position, len); + this.Position += len; + return output; + } + + public Span<byte> ReadBytesAndSize() + { + var len = ReadPackedInt32(); + return ReadBytes(len); + } + + public Span<byte> ReadBytes(int length) + { + var output = Buffer.AsSpan(Position, length); + Position += length; + return output; + } + + public int ReadPackedInt32() + { + return (int)ReadPackedUInt32(); + } + + public uint ReadPackedUInt32() + { + bool readMore = true; + int shift = 0; + uint output = 0; + + while (readMore) + { + byte b = FastByte(); + if (b >= 0x80) + { + readMore = true; + b ^= 0x80; + } + else + { + readMore = false; + } + + output |= (uint)(b << shift); + shift += 7; + } + + return output; + } + + public MessageReader_Bytes Slice(int start, int length) + { + return new MessageReader_Bytes(Tag, Buffer, start, length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte FastByte() + { + return Buffer[Position++]; + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs new file mode 100644 index 0000000..1103336 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Impostor.Benchmarks.Data +{ + public class MessageReader_Bytes_Pooled + { + private static ConcurrentQueue<MessageReader_Bytes_Pooled> _readers; + + static MessageReader_Bytes_Pooled() + { + var instances = new List<MessageReader_Bytes_Pooled>(); + + for (var i = 0; i < 10000; i++) + { + instances.Add(new MessageReader_Bytes_Pooled()); + } + + _readers = new ConcurrentQueue<MessageReader_Bytes_Pooled>(instances); + } + + public byte Tag { get; set; } + public byte[] Buffer { get; set; } + public int Position { get; set; } + public int Length { get; set; } + public int BytesRemaining => this.Length - this.Position; + + public void Update(byte[] buffer, int position = 0, int length = 0) + { + Tag = byte.MaxValue; + Buffer = buffer; + Position = position; + Length = length; + } + + public void Update(byte tag, byte[] buffer, int position = 0, int length = 0) + { + Tag = tag; + Buffer = buffer; + Position = position; + Length = length; + } + + public MessageReader_Bytes_Pooled ReadMessage() + { + var length = ReadUInt16(); + var tag = FastByte(); + var pos = Position; + + Position += length; + + if (!_readers.TryDequeue(out var result)) + { + throw new Exception("Failed to get pooled instance"); + } + + result.Update(tag, Buffer, pos, length); + + return result; + } + + public bool ReadBoolean() + { + byte val = FastByte(); + return val != 0; + } + + public sbyte ReadSByte() + { + return (sbyte)FastByte(); + } + + public byte ReadByte() + { + return FastByte(); + } + + public ushort ReadUInt16() + { + return (ushort)(this.FastByte() | + this.FastByte() << 8); + } + + public short ReadInt16() + { + return (short)(this.FastByte() | + this.FastByte() << 8); + } + + public uint ReadUInt32() + { + return this.FastByte() + | (uint)this.FastByte() << 8 + | (uint)this.FastByte() << 16 + | (uint)this.FastByte() << 24; + } + + public int ReadInt32() + { + return this.FastByte() + | this.FastByte() << 8 + | this.FastByte() << 16 + | this.FastByte() << 24; + } + + public unsafe float ReadSingle() + { + float output = 0; + fixed (byte* bufPtr = &this.Buffer[Position]) + { + byte* outPtr = (byte*)&output; + + *outPtr = *bufPtr; + *(outPtr + 1) = *(bufPtr + 1); + *(outPtr + 2) = *(bufPtr + 2); + *(outPtr + 3) = *(bufPtr + 3); + } + + this.Position += 4; + return output; + } + + public string ReadString() + { + var len = this.ReadPackedInt32(); + + if (this.BytesRemaining < len) + { + throw new InvalidDataException($"Read length is longer than message length: {len} of {this.BytesRemaining}"); + } + + var output = Encoding.UTF8.GetString(this.Buffer, Position, len); + this.Position += len; + return output; + } + + public Span<byte> ReadBytesAndSize() + { + var len = ReadPackedInt32(); + return ReadBytes(len); + } + + public Span<byte> ReadBytes(int length) + { + var output = Buffer.AsSpan(Position, length); + Position += length; + return output; + } + + public int ReadPackedInt32() + { + return (int)ReadPackedUInt32(); + } + + public uint ReadPackedUInt32() + { + bool readMore = true; + int shift = 0; + uint output = 0; + + while (readMore) + { + byte b = FastByte(); + if (b >= 0x80) + { + readMore = true; + b ^= 0x80; + } + else + { + readMore = false; + } + + output |= (uint)(b << shift); + shift += 7; + } + + return output; + } + + public MessageReader_Bytes_Pooled Slice(int start, int length) + { + if (!_readers.TryDequeue(out var result)) + { + throw new Exception("Failed to get pooled instance"); + } + + result.Update(Tag, Buffer, start, length); + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte FastByte() + { + return Buffer[Position++]; + } + + public static MessageReader_Bytes_Pooled Get(byte[] data) + { + if (!_readers.TryDequeue(out var result)) + { + throw new Exception("Failed to get pooled instance"); + } + + result.Update(data); + + return result; + } + + public static void Return(MessageReader_Bytes_Pooled instance) + { + _readers.Enqueue(instance); + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs new file mode 100644 index 0000000..0066847 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageReader_Bytes_Pooled_Improved.cs @@ -0,0 +1,90 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Benchmarks.Data +{ + public class MessageReader_Bytes_Pooled_Improved : IDisposable + { + private readonly ObjectPool<MessageReader_Bytes_Pooled_Improved> _pool; + + public MessageReader_Bytes_Pooled_Improved(ObjectPool<MessageReader_Bytes_Pooled_Improved> pool) + { + _pool = pool; + } + + public byte Tag { get; set; } + public byte[] Buffer { get; set; } + public int Position { get; set; } + public int Length { get; set; } + public int BytesRemaining => this.Length - this.Position; + + public void Update(byte[] buffer, int position = 0, int length = 0) + { + Tag = byte.MaxValue; + Buffer = buffer; + Position = position; + Length = length; + } + + public void Update(byte tag, byte[] buffer, int position = 0, int length = 0) + { + Tag = tag; + Buffer = buffer; + Position = position; + Length = length; + } + + public MessageReader_Bytes_Pooled_Improved ReadMessage() + { + var length = ReadUInt16(); + var tag = ReadByte(); + var pos = Position; + + Position += length; + + var result = _pool.Get(); + result.Update(tag, Buffer, pos, length); + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte ReadByte() + { + return Buffer[Position++]; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ushort ReadUInt16() + { + var res = BinaryPrimitives.ReadUInt16LittleEndian(Buffer.AsSpan(Position)); + Position += sizeof(ushort); + return res; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadInt32() + { + var res = BinaryPrimitives.ReadInt32LittleEndian(Buffer.AsSpan(Position)); + Position += sizeof(int); + return res; + } + + public MessageReader_Bytes_Pooled_Improved Slice(int start, int length) + { + var result = _pool.Get(); + result.Update(Tag, Buffer, start, length); + return result; + } + + public void Dispose() + { + _pool.Return(this); + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs new file mode 100644 index 0000000..94ff441 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/MessageWriter.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; +using Impostor.Api.Games; +using Impostor.Api.Net.Messages; + +namespace Impostor.Benchmarks.Data +{ + public class MessageWriter + { + private static int BufferSize = 64000; + + public MessageType SendOption { get; private set; } + + private Stack<int> messageStarts = new Stack<int>(); + + public MessageWriter(byte[] buffer) + { + this.Buffer = buffer; + this.Length = this.Buffer.Length; + } + + public MessageWriter(int bufferSize) + { + this.Buffer = new byte[bufferSize]; + } + + public byte[] Buffer { get; } + public int Length { get; set; } + public int Position { get; set; } + + public byte[] ToByteArray(bool includeHeader) + { + if (includeHeader) + { + byte[] output = new byte[this.Length]; + System.Buffer.BlockCopy(this.Buffer, 0, output, 0, this.Length); + return output; + } + else + { + switch (this.SendOption) + { + case MessageType.Reliable: + { + byte[] output = new byte[this.Length - 3]; + System.Buffer.BlockCopy(this.Buffer, 3, output, 0, this.Length - 3); + return output; + } + case MessageType.Unreliable: + { + byte[] output = new byte[this.Length - 1]; + System.Buffer.BlockCopy(this.Buffer, 1, output, 0, this.Length - 1); + return output; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + throw new NotImplementedException(); + } + + public bool HasBytes(int expected) + { + if (this.SendOption == MessageType.Unreliable) + { + return this.Length > 1 + expected; + } + + return this.Length > 3 + expected; + } + + public void Write(GameCode value) + { + this.Write(value.Value); + } + + /// + public void StartMessage(byte typeFlag) + { + messageStarts.Push(this.Position); + this.Position += 2; // Skip for size + this.Write(typeFlag); + } + + /// + public void EndMessage() + { + var lastMessageStart = messageStarts.Pop(); + ushort length = (ushort)(this.Position - lastMessageStart - 3); // Minus length and type byte + this.Buffer[lastMessageStart] = (byte)length; + this.Buffer[lastMessageStart + 1] = (byte)(length >> 8); + } + + /// + public void CancelMessage() + { + this.Position = this.messageStarts.Pop(); + this.Length = this.Position; + } + + public void Clear(MessageType sendOption) + { + this.messageStarts.Clear(); + this.SendOption = sendOption; + this.Buffer[0] = (byte)sendOption; + switch (sendOption) + { + default: + case MessageType.Unreliable: + this.Length = this.Position = 1; + break; + + case MessageType.Reliable: + this.Length = this.Position = 3; + break; + } + } + + #region WriteMethods + + public void Write(bool value) + { + this.Buffer[this.Position++] = (byte)(value ? 1 : 0); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(sbyte value) + { + this.Buffer[this.Position++] = (byte)value; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte value) + { + this.Buffer[this.Position++] = value; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(short value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(ushort value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(uint value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + this.Buffer[this.Position++] = (byte)(value >> 16); + this.Buffer[this.Position++] = (byte)(value >> 24); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(int value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + this.Buffer[this.Position++] = (byte)(value >> 16); + this.Buffer[this.Position++] = (byte)(value >> 24); + if (this.Position > this.Length) this.Length = this.Position; + } + + public unsafe void Write(float value) + { + fixed (byte* ptr = &this.Buffer[this.Position]) + { + byte* valuePtr = (byte*)&value; + + *ptr = *valuePtr; + *(ptr + 1) = *(valuePtr + 1); + *(ptr + 2) = *(valuePtr + 2); + *(ptr + 3) = *(valuePtr + 3); + } + + this.Position += 4; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(string value) + { + var bytes = UTF8Encoding.UTF8.GetBytes(value); + this.WritePacked(bytes.Length); + this.Write(bytes); + } + + public void Write(IPAddress value) + { + this.Write(value.GetAddressBytes()); + } + + public void WriteBytesAndSize(byte[] bytes) + { + this.WritePacked((uint)bytes.Length); + this.Write(bytes); + } + + public void WriteBytesAndSize(byte[] bytes, int length) + { + this.WritePacked((uint)length); + this.Write(bytes, length); + } + + public void WriteBytesAndSize(byte[] bytes, int offset, int length) + { + this.WritePacked((uint)length); + this.Write(bytes, offset, length); + } + + public void Write(ReadOnlyMemory<byte> data) + { + Write(data.Span); + } + + public void Write(ReadOnlySpan<byte> bytes) + { + bytes.CopyTo(this.Buffer.AsSpan(this.Position, bytes.Length)); + + this.Position += bytes.Length; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte[] bytes) + { + Array.Copy(bytes, 0, this.Buffer, this.Position, bytes.Length); + this.Position += bytes.Length; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte[] bytes, int offset, int length) + { + Array.Copy(bytes, offset, this.Buffer, this.Position, length); + this.Position += length; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte[] bytes, int length) + { + Array.Copy(bytes, 0, this.Buffer, this.Position, length); + this.Position += length; + if (this.Position > this.Length) this.Length = this.Position; + } + + /// + public void WritePacked(int value) + { + this.WritePacked((uint)value); + } + + /// + public void WritePacked(uint value) + { + do + { + byte b = (byte)(value & 0xFF); + if (value >= 0x80) + { + b |= 0x80; + } + + this.Write(b); + value >>= 7; + } while (value > 0); + } + + #endregion WriteMethods + + public void Write(MessageWriter msg, bool includeHeader) + { + int offset = 0; + if (!includeHeader) + { + switch (msg.SendOption) + { + case MessageType.Unreliable: + offset = 1; + break; + + case MessageType.Reliable: + offset = 3; + break; + } + } + + this.Write(msg.Buffer, offset, msg.Length - offset); + } + + public unsafe static bool IsLittleEndian() + { + byte b; + unsafe + { + int i = 1; + byte* bp = (byte*)&i; + b = *bp; + } + + return b == 1; + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs new file mode 100644 index 0000000..802fcaa --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/Pool/MessageReader_Bytes_Pooled_ImprovedPolicy.cs @@ -0,0 +1,26 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Benchmarks.Data.Pool +{ + public class MessageReader_Bytes_Pooled_ImprovedPolicy : IPooledObjectPolicy<MessageReader_Bytes_Pooled_Improved> + { + private readonly IServiceProvider _serviceProvider; + + public MessageReader_Bytes_Pooled_ImprovedPolicy(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public MessageReader_Bytes_Pooled_Improved Create() + { + return new MessageReader_Bytes_Pooled_Improved(_serviceProvider.GetRequiredService<ObjectPool<MessageReader_Bytes_Pooled_Improved>>()); + } + + public bool Return(MessageReader_Bytes_Pooled_Improved obj) + { + return true; + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs new file mode 100644 index 0000000..402fbaf --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReaderOwner.cs @@ -0,0 +1,22 @@ +using System; + +namespace Impostor.Benchmarks.Data.Span +{ + public class MessageReaderOwner + { + private readonly Memory<byte> _data; + + public MessageReaderOwner(Memory<byte> data) + { + _data = data; + } + + public int Position { get; internal set; } + public int Length => _data.Length; + + public MessageReader_Span CreateReader() + { + return new MessageReader_Span(this, byte.MaxValue, _data.Span.Slice(Position)); + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs new file mode 100644 index 0000000..5636dbe --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Data/Span/MessageReader_Span.cs @@ -0,0 +1,50 @@ +using System; +using System.Buffers.Binary; +using Impostor.Hazel; + +namespace Impostor.Benchmarks.Data.Span +{ + public ref struct MessageReader_Span + { + private readonly MessageReaderOwner _owner; + private readonly byte _tag; + private readonly Span<byte> _data; + + public MessageReader_Span(MessageReaderOwner owner, byte tag, Span<byte> data) + { + _owner = owner; + _tag = tag; + _data = data; + } + + public MessageReader_Span ReadMessage() + { + var length = ReadUInt16(); + var tag = ReadByte(); + var pos = _owner.Position; + + _owner.Position += length; + + return new MessageReader_Span(_owner, tag, _data.Slice(3, length)); + } + + public byte ReadByte() + { + return _data[_owner.Position++]; + } + + public ushort ReadUInt16() + { + var output = BinaryPrimitives.ReadUInt16LittleEndian(_data.Slice(_owner.Position)); + _owner.Position += sizeof(ushort); + return output; + } + + public int ReadInt32() + { + var output = BinaryPrimitives.ReadInt32LittleEndian(_data.Slice(_owner.Position)); + _owner.Position += sizeof(int); + return output; + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs b/Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs new file mode 100644 index 0000000..8fbceeb --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Extensions/SpanExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; + +namespace Impostor.Benchmarks.Extensions +{ + public static class SpanExtensions + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span<byte> ReadMessage(this Span<byte> input) + { + var length = BinaryPrimitives.ReadUInt16LittleEndian(input); + var tag = input[2]; + + return input.Slice(3, length); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadUInt16(this ref ReadOnlySpan<byte> input) + { + return BinaryPrimitives.ReadUInt16LittleEndian(input); + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj b/Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj new file mode 100644 index 0000000..6f19bda --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Impostor.Benchmarks.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="BenchmarkDotNet" Version="0.12.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Benchmarks/Program.cs b/Impostor-dev/src/Impostor.Benchmarks/Program.cs new file mode 100644 index 0000000..3ce7383 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Running; +using Impostor.Benchmarks.Tests; + +namespace Impostor.Benchmarks +{ + internal static class Program + { + private static void Main(string[] args) + { + // BenchmarkRunner.Run<EventManagerBenchmark>( + // DefaultConfig.Instance + // .AddDiagnoser(MemoryDiagnoser.Default) + // ); + + BenchmarkRunner.Run<MessageReaderBenchmark>( + DefaultConfig.Instance + .AddDiagnoser(MemoryDiagnoser.Default) + ); + } + } +} diff --git a/Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs b/Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs new file mode 100644 index 0000000..0675080 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Tests/EventManagerBenchmark.cs @@ -0,0 +1,77 @@ +// using System.Threading.Tasks; +// using BenchmarkDotNet.Attributes; +// using Impostor.Api.Events; +// using Impostor.Api.Events.Managers; +// using Impostor.Server.Events; +// using Microsoft.Extensions.DependencyInjection; +// +// namespace Impostor.Benchmarks.Tests +// { +// public class EventManagerBenchmark +// { +// private IEventManager _eventManager; +// private IGameEvent _event; +// +// [GlobalSetup] +// public void Setup() +// { +// var services = new ServiceCollection(); +// +// services.AddLogging(); +// services.AddSingleton<IEventManager, EventManager>(); +// +// _event = new GameStartedEvent(null); +// _eventManager = services.BuildServiceProvider().GetRequiredService<IEventManager>(); +// _eventManager.RegisterListener(new EventListener()); +// _eventManager.RegisterListener(new EventListener()); +// _eventManager.RegisterListener(new EventListener()); +// _eventManager.RegisterListener(new EventListener()); +// _eventManager.RegisterListener(new EventListener()); +// } +// +// [Benchmark] +// public async Task Run_1() +// { +// for (var i = 0; i < 1; i++) +// { +// await _eventManager.CallAsync(_event); +// } +// } +// +// [Benchmark] +// public async Task Run_1000() +// { +// for (var i = 0; i < 1000; i++) +// { +// await _eventManager.CallAsync(_event); +// } +// } +// +// [Benchmark] +// public async Task Run_10000() +// { +// for (var i = 0; i < 10000; i++) +// { +// await _eventManager.CallAsync(_event); +// } +// } +// +// [Benchmark] +// public async Task Run_100000() +// { +// for (var i = 0; i < 100000; i++) +// { +// await _eventManager.CallAsync(_event); +// } +// } +// +// private class EventListener : IEventListener +// { +// [EventListener] +// public void OnGameStarted(IGameStartedEvent e) +// { +// +// } +// } +// } +// } diff --git a/Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs b/Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs new file mode 100644 index 0000000..abf1642 --- /dev/null +++ b/Impostor-dev/src/Impostor.Benchmarks/Tests/MessageReaderBenchmark.cs @@ -0,0 +1,132 @@ +using System; +using System.Buffers.Binary; +using BenchmarkDotNet.Attributes; +using Impostor.Benchmarks.Data; +using Impostor.Benchmarks.Data.Pool; +using Impostor.Benchmarks.Extensions; +using Impostor.Hazel; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using MessageWriter = Impostor.Benchmarks.Data.MessageWriter; + +namespace Impostor.Benchmarks.Tests +{ + public class MessageReaderBenchmark + { + private byte[] _data; + private ObjectPool<MessageReader_Bytes_Pooled_Improved> _pool; + + [GlobalSetup] + public void Setup() + { + var message = new MessageWriter(1024); + + message.StartMessage(1); + message.Write((ushort)3100); + message.Write((byte)100); + message.Write((int) int.MaxValue); + message.WritePacked(int.MaxValue); + message.EndMessage(); + + _data = message.ToByteArray(true); + + MessageReader_Bytes_Pooled.Return(MessageReader_Bytes_Pooled.Get(_data)); + + // Services + var services = new ServiceCollection(); + + services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider()); + services.AddSingleton(serviceProvider => + { + var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>(); + var policy = new MessageReader_Bytes_Pooled_ImprovedPolicy(serviceProvider); + return provider.Create(policy); + }); + + _pool = services + .BuildServiceProvider() + .GetRequiredService<ObjectPool<MessageReader_Bytes_Pooled_Improved>>(); + } + + [Benchmark] + public void Span_Run_1_000_000() + { + for (var i = 0; i < 1_000_000; i++) + { + var span = _data.AsSpan(); + var inner = span.ReadMessage(); + + _ = BinaryPrimitives.ReadUInt16LittleEndian(inner); + _ = inner[2]; + _ = BinaryPrimitives.ReadInt32LittleEndian(inner.Slice(3)); + } + } + + // [Benchmark] + // public void Normal_Run_1_000_000() + // { + // for (var i = 0; i < 1_000_000; i++) + // { + // var reader = new MessageReader(_data); + // var inner = reader.ReadMessage(); + // + // _ = inner.ReadUInt16(); + // _ = inner.ReadByte(); + // _ = inner.ReadInt32(); + // // inner.ReadPackedInt32(); + // } + // } + + [Benchmark] + public void Bytes_Run_1_000_000() + { + for (var i = 0; i < 1_000_000; i++) + { + var reader = new MessageReader_Bytes(_data); + var inner = reader.ReadMessage(); + + _ = inner.ReadUInt16(); + _ = inner.ReadByte(); + _ = inner.ReadInt32(); + // inner.ReadPackedInt32(); + } + } + + [Benchmark] + public void Pooled_Bytes_Run_1_000_000() + { + for (var i = 0; i < 1_000_000; i++) + { + var reader = MessageReader_Bytes_Pooled.Get(_data); + var inner = reader.ReadMessage(); + + _ = inner.ReadUInt16(); + _ = inner.ReadByte(); + _ = inner.ReadInt32(); + // inner.ReadPackedInt32(); + + MessageReader_Bytes_Pooled.Return(inner); + MessageReader_Bytes_Pooled.Return(reader); + } + } + + [Benchmark] + public void Improved_Pooled_Bytes_Run_1_000_000() + { + using (var reader = _pool.Get()) + { + for (var i = 0; i < 1_000_000; i++) + { + reader.Update(_data); + + using (var inner = reader.ReadMessage()) + { + _ = inner.ReadUInt16(); + _ = inner.ReadByte(); + _ = inner.ReadInt32(); + } + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj b/Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj new file mode 100644 index 0000000..886e26e --- /dev/null +++ b/Impostor-dev/src/Impostor.Client.App/Impostor.Client.App.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Client\Impostor.Client.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Client.App/Program.cs b/Impostor-dev/src/Impostor.Client.App/Program.cs new file mode 100644 index 0000000..aa8866a --- /dev/null +++ b/Impostor-dev/src/Impostor.Client.App/Program.cs @@ -0,0 +1,75 @@ +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Hazel; +using Impostor.Hazel.Udp; +using Serilog; + +namespace Impostor.Client.App +{ + internal static class Program + { + private static readonly ManualResetEvent QuitEvent = new ManualResetEvent(false); + + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .WriteTo.Console() + .CreateLogger(); + + var writeHandshake = MessageWriter.Get(MessageType.Reliable); + + writeHandshake.Write(50516550); + writeHandshake.Write("AeonLucid"); + + var writeGameCreate = MessageWriter.Get(MessageType.Reliable); + + Message00HostGameC2S.Serialize(writeGameCreate, new GameOptionsData + { + MaxPlayers = 4, + NumImpostors = 2 + }); + + // TODO: ObjectPool for MessageReaders + using (var connection = new UdpClientConnection(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 22023), null)) + { + var e = new ManualResetEvent(false); + + // Register events. + connection.DataReceived = DataReceived; + connection.Disconnected = Disconnected; + + // Connect and send handshake. + await connection.ConnectAsync(writeHandshake.ToByteArray(false)); + Log.Information("Connected."); + + // Create a game. + await connection.SendAsync(writeGameCreate); + Log.Information("Requested game creation."); + + // Recycle. + writeHandshake.Recycle(); + writeGameCreate.Recycle(); + + e.WaitOne(); + } + } + + private static ValueTask DataReceived(DataReceivedEventArgs e) + { + Log.Information("Received data."); + return default; + } + + private static ValueTask Disconnected(DisconnectedEventArgs e) + { + Log.Information("Disconnected: " + e.Reason); + QuitEvent.Set(); + return default; + } + } +} diff --git a/Impostor-dev/src/Impostor.Client/Impostor.Client.csproj b/Impostor-dev/src/Impostor.Client/Impostor.Client.csproj new file mode 100644 index 0000000..28b6ed3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Client/Impostor.Client.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" /> + <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Hazel/Connection.cs b/Impostor-dev/src/Impostor.Hazel/Connection.cs new file mode 100644 index 0000000..dec8cfe --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Connection.cs @@ -0,0 +1,249 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Serilog; + +namespace Impostor.Hazel +{ + /// <summary> + /// Base class for all connections. + /// </summary> + /// <remarks> + /// <para> + /// Connection is the base class for all connections that Hazel can make. It provides common functionality and a + /// standard interface to allow connections to be swapped easily. + /// </para> + /// <para> + /// Any class inheriting from Connection should provide the 3 standard guarantees that Hazel provides: + /// <list type="bullet"> + /// <item> + /// <description>Thread Safe</description> + /// </item> + /// <item> + /// <description>Connection Orientated</description> + /// </item> + /// <item> + /// <description>Packet/Message Based</description> + /// </item> + /// </list> + /// </para> + /// </remarks> + /// <threadsafety static="true" instance="true"/> + public abstract class Connection : IDisposable + { + private static readonly ILogger Logger = Log.ForContext<Connection>(); + + /// <summary> + /// Called when a message has been received. + /// </summary> + /// <remarks> + /// <para> + /// DataReceived is invoked everytime a message is received from the end point of this connection, the message + /// that was received can be found in the <see cref="DataReceivedEventArgs"/> alongside other information from the + /// event. + /// </para> + /// <include file="DocInclude/common.xml" path="docs/item[@name='Event_Thread_Safety_Warning']/*" /> + /// </remarks> + /// <example> + /// <code language="C#" source="DocInclude/TcpClientExample.cs"/> + /// </example> + public Func<DataReceivedEventArgs, ValueTask> DataReceived; + + public int TestLagMs = -1; + public int TestDropRate = 0; + protected int testDropCount = 0; + + /// <summary> + /// Called when the end point disconnects or an error occurs. + /// </summary> + /// <remarks> + /// <para> + /// Disconnected is invoked when the connection is closed due to an exception occuring or because the remote + /// end point disconnected. If it was invoked due to an exception occuring then the exception is available + /// in the <see cref="DisconnectedEventArgs"/> passed with the event. + /// </para> + /// <include file="DocInclude/common.xml" path="docs/item[@name='Event_Thread_Safety_Warning']/*" /> + /// </remarks> + /// <example> + /// <code language="C#" source="DocInclude/TcpClientExample.cs"/> + /// </example> + public Func<DisconnectedEventArgs, ValueTask> Disconnected; + + /// <summary> + /// The remote end point of this Connection. + /// </summary> + /// <remarks> + /// This is the end point that this connection is connected to (i.e. the other device). This returns an abstract + /// <see cref="ConnectionEndPoint"/> which can then be cast to an appropriate end point depending on the + /// connection type. + /// </remarks> + public IPEndPoint EndPoint { get; protected set; } + + public IPMode IPMode { get; protected set; } + + /// <summary> + /// The traffic statistics about this Connection. + /// </summary> + /// <remarks> + /// Contains statistics about the number of messages and bytes sent and received by this connection. + /// </remarks> + public ConnectionStatistics Statistics { get; protected set; } + + /// <summary> + /// The state of this connection. + /// </summary> + /// <remarks> + /// All implementers should be aware that when this is set to ConnectionState.Connected it will + /// release all threads that are blocked on <see cref="WaitOnConnect"/>. + /// </remarks> + public ConnectionState State + { + get + { + return this._state; + } + + protected set + { + this._state = value; + this.SetState(value); + } + } + + protected ConnectionState _state; + protected virtual void SetState(ConnectionState state) { } + + /// <summary> + /// Constructor that initializes the ConnecitonStatistics object. + /// </summary> + /// <remarks> + /// This constructor initialises <see cref="Statistics"/> with empty statistics and sets <see cref="State"/> to + /// <see cref="ConnectionState.NotConnected"/>. + /// </remarks> + protected Connection() + { + this.Statistics = new ConnectionStatistics(); + this.State = ConnectionState.NotConnected; + } + + /// <summary> + /// Sends a number of bytes to the end point of the connection using the specified <see cref="MessageType"/>. + /// </summary> + /// <param name="msg">The message to send.</param> + /// <remarks> + /// <include file="DocInclude/common.xml" path="docs/item[@name='Connection_SendBytes_General']/*" /> + /// <para> + /// The messageType parameter is only a request to use those options and the actual method used to send the + /// data is up to the implementation. There are circumstances where this parameter may be ignored but in + /// general any implementer should aim to always follow the user's request. + /// </para> + /// </remarks> + public abstract ValueTask SendAsync(IMessageWriter msg); + + /// <summary> + /// Sends a number of bytes to the end point of the connection using the specified <see cref="MessageType"/>. + /// </summary> + /// <param name="bytes">The bytes of the message to send.</param> + /// <param name="messageType">The option specifying how the message should be sent.</param> + /// <remarks> + /// <include file="DocInclude/common.xml" path="docs/item[@name='Connection_SendBytes_General']/*" /> + /// <para> + /// The messageType parameter is only a request to use those options and the actual method used to send the + /// data is up to the implementation. There are circumstances where this parameter may be ignored but in + /// general any implementer should aim to always follow the user's request. + /// </para> + /// </remarks> + public abstract ValueTask SendBytes(byte[] bytes, MessageType messageType = MessageType.Unreliable); + + /// <summary> + /// Connects the connection to a server and begins listening. + /// This method does not block. + /// </summary> + /// <param name="bytes">The bytes of data to send in the handshake.</param> + public abstract ValueTask ConnectAsync(byte[] bytes = null); + + /// <summary> + /// Invokes the DataReceived event. + /// </summary> + /// <param name="msg">The bytes received.</param> + /// <param name="messageType">The <see cref="MessageType"/> the message was received with.</param> + /// <remarks> + /// Invokes the <see cref="DataReceived"/> event on this connection to alert subscribers a new message has been + /// received. The bytes and the send option that the message was sent with should be passed in to give to the + /// subscribers. + /// </remarks> + protected async ValueTask InvokeDataReceived(IMessageReader msg, MessageType messageType) + { + // Make a copy to avoid race condition between null check and invocation + var handler = DataReceived; + if (handler != null) + { + try + { + await handler(new DataReceivedEventArgs(this, msg, messageType)); + } + catch (Exception e) + { + Logger.Error(e, "Invoking data received failed"); + await Disconnect("Invoking data received failed"); + } + } + } + + /// <summary> + /// Invokes the Disconnected event. + /// </summary> + /// <param name="e">The exception, if any, that occurred to cause this.</param> + /// <param name="reader">Extra disconnect data</param> + /// <remarks> + /// Invokes the <see cref="Disconnected"/> event to alert subscribres this connection has been disconnected either + /// by the end point or because an error occurred. If an error occurred the error should be passed in in order to + /// pass to the subscribers, otherwise null can be passed in. + /// </remarks> + protected async ValueTask InvokeDisconnected(string e, IMessageReader reader) + { + // Make a copy to avoid race condition between null check and invocation + var handler = Disconnected; + if (handler != null) + { + try + { + await handler(new DisconnectedEventArgs(e, reader)); + } + catch (Exception ex) + { + Logger.Error(ex, "Error in InvokeDisconnected"); + } + } + } + + /// <summary> + /// For times when you want to force the disconnect handler to fire as well as close it. + /// If you only want to close it, just use Dispose. + /// </summary> + public abstract ValueTask Disconnect(string reason, MessageWriter writer = null); + + /// <summary> + /// Disposes of this NetworkConnection. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Disposes of this NetworkConnection. + /// </summary> + /// <param name="disposing">Are we currently disposing?</param> + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.DataReceived = null; + this.Disconnected = null; + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs b/Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs new file mode 100644 index 0000000..116f657 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/ConnectionListener.cs @@ -0,0 +1,100 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Serilog; + +namespace Impostor.Hazel +{ + /// <summary> + /// Base class for all connection listeners. + /// </summary> + /// <remarks> + /// <para> + /// ConnectionListeners are server side objects that listen for clients and create matching server side connections + /// for each client in a similar way to TCP does. These connections should be ready for communication immediately. + /// </para> + /// <para> + /// Each time a client connects the <see cref="NewConnection"/> event will be invoked to alert all subscribers to + /// the new connection. A disconnected event is then present on the <see cref="Connection"/> that is passed to the + /// subscribers. + /// </para> + /// </remarks> + /// <threadsafety static="true" instance="true"/> + public abstract class ConnectionListener : IAsyncDisposable + { + private static readonly ILogger Logger = Log.ForContext<ConnectionListener>(); + + /// <summary> + /// Invoked when a new client connects. + /// </summary> + /// <remarks> + /// <para> + /// NewConnection is invoked each time a client connects to the listener. The + /// <see cref="NewConnectionEventArgs"/> contains the new <see cref="Connection"/> for communication with this + /// client. + /// </para> + /// <para> + /// Hazel may or may not store connections so it is your responsibility to keep track and properly Dispose of + /// connections to your server. + /// </para> + /// <include file="DocInclude/common.xml" path="docs/item[@name='Event_Thread_Safety_Warning']/*" /> + /// </remarks> + /// <example> + /// <code language="C#" source="DocInclude/TcpListenerExample.cs"/> + /// </example> + public Func<NewConnectionEventArgs, ValueTask> NewConnection; + + /// <summary> + /// Makes this connection listener begin listening for connections. + /// </summary> + /// <remarks> + /// <para> + /// This instructs the listener to begin listening for new clients connecting to the server. When a new client + /// connects the <see cref="NewConnection"/> event will be invoked containing the connection to the new client. + /// </para> + /// <para> + /// To stop listening you should call <see cref="DisposeAsync()"/>. + /// </para> + /// </remarks> + /// <example> + /// <code language="C#" source="DocInclude/TcpListenerExample.cs"/> + /// </example> + public abstract Task StartAsync(); + + /// <summary> + /// Invokes the NewConnection event with the supplied connection. + /// </summary> + /// <param name="msg">The user sent bytes that were received as part of the handshake.</param> + /// <param name="connection">The connection to pass in the arguments.</param> + /// <remarks> + /// Implementers should call this to invoke the <see cref="NewConnection"/> event before data is received so that + /// subscribers do not miss any data that may have been sent immediately after connecting. + /// </remarks> + internal async Task InvokeNewConnection(IMessageReader msg, Connection connection) + { + // Make a copy to avoid race condition between null check and invocation + var handler = NewConnection; + if (handler != null) + { + try + { + await handler(new NewConnectionEventArgs(msg, connection)); + } + catch (Exception e) + { + Logger.Error(e, "Accepting connection failed"); + await connection.Disconnect("Accepting connection failed"); + } + } + } + + /// <summary> + /// Call to dispose of the connection listener. + /// </summary> + public virtual ValueTask DisposeAsync() + { + this.NewConnection = null; + return ValueTask.CompletedTask; + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/ConnectionState.cs b/Impostor-dev/src/Impostor.Hazel/ConnectionState.cs new file mode 100644 index 0000000..5dd7c6a --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/ConnectionState.cs @@ -0,0 +1,23 @@ +namespace Impostor.Hazel +{ + /// <summary> + /// Represents the state a <see cref="Connection"/> is currently in. + /// </summary> + public enum ConnectionState + { + /// <summary> + /// The Connection has either not been established yet or has been disconnected. + /// </summary> + NotConnected, + + /// <summary> + /// The Connection is currently connecting to an endpoint. + /// </summary> + Connecting, + + /// <summary> + /// The Connection is connected and data can be transfered. + /// </summary> + Connected, + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs b/Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs new file mode 100644 index 0000000..4802620 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/ConnectionStatistics.cs @@ -0,0 +1,566 @@ +using System.Runtime.CompilerServices; +using System.Threading; + +[assembly: InternalsVisibleTo("Hazel.Tests")] +namespace Impostor.Hazel +{ + /// <summary> + /// Holds statistics about the traffic through a <see cref="Connection"/>. + /// </summary> + /// <threadsafety static="true" instance="true"/> + public class ConnectionStatistics + { + private const int ExpectedMTU = 1200; + + /// <summary> + /// The total number of messages sent. + /// </summary> + public int MessagesSent + { + get + { + return UnreliableMessagesSent + ReliableMessagesSent + FragmentedMessagesSent + AcknowledgementMessagesSent + HelloMessagesSent; + } + } + + /// <summary> + /// The number of messages sent larger than 576 bytes. This is smaller than most default MTUs. + /// </summary> + /// <remarks> + /// This is the number of unreliable messages that were sent from the <see cref="Connection"/>, incremented + /// each time that LogUnreliableSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </remarks> + public int FragmentableMessagesSent + { + get + { + return fragmentableMessagesSent; + } + } + + /// <summary> + /// The number of messages sent larger than 576 bytes. + /// </summary> + int fragmentableMessagesSent; + + /// <summary> + /// The number of unreliable messages sent. + /// </summary> + /// <remarks> + /// This is the number of unreliable messages that were sent from the <see cref="Connection"/>, incremented + /// each time that LogUnreliableSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </remarks> + public int UnreliableMessagesSent + { + get + { + return unreliableMessagesSent; + } + } + + /// <summary> + /// The number of unreliable messages sent. + /// </summary> + int unreliableMessagesSent; + + /// <summary> + /// The number of reliable messages sent. + /// </summary> + /// <remarks> + /// This is the number of reliable messages that were sent from the <see cref="Connection"/>, incremented + /// each time that LogReliableSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </remarks> + public int ReliableMessagesSent + { + get + { + return reliableMessagesSent; + } + } + + /// <summary> + /// The number of unreliable messages sent. + /// </summary> + int reliableMessagesSent; + + /// <summary> + /// The number of fragmented messages sent. + /// </summary> + /// <remarks> + /// This is the number of fragmented messages that were sent from the <see cref="Connection"/>, incremented + /// each time that LogFragmentedSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </remarks> + public int FragmentedMessagesSent + { + get + { + return fragmentedMessagesSent; + } + } + + /// <summary> + /// The number of fragmented messages sent. + /// </summary> + int fragmentedMessagesSent; + + /// <summary> + /// The number of acknowledgement messages sent. + /// </summary> + /// <remarks> + /// This is the number of acknowledgements that were sent from the <see cref="Connection"/>, incremented + /// each time that LogAcknowledgementSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </remarks> + public int AcknowledgementMessagesSent + { + get + { + return acknowledgementMessagesSent; + } + } + + /// <summary> + /// The number of acknowledgement messages sent. + /// </summary> + int acknowledgementMessagesSent; + + /// <summary> + /// The number of hello messages sent. + /// </summary> + /// <remarks> + /// This is the number of hello messages that were sent from the <see cref="Connection"/>, incremented + /// each time that LogHelloSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </remarks> + public int HelloMessagesSent + { + get + { + return helloMessagesSent; + } + } + + /// <summary> + /// The number of hello messages sent. + /// </summary> + int helloMessagesSent; + + /// <summary> + /// The number of bytes of data sent. + /// </summary> + /// <remarks> + /// <para> + /// This is the number of bytes of data (i.e. user bytes) that were sent from the <see cref="Connection"/>, + /// accumulated each time that LogSend is called by the Connection. Messages that caused an error are not + /// counted and messages are only counted once all other operations in the send are complete. + /// </para> + /// <para> + /// For the number of bytes including protocol bytes see <see cref="TotalBytesSent"/>. + /// </para> + /// </remarks> + public long DataBytesSent + { + get + { + return Interlocked.Read(ref dataBytesSent); + } + } + + /// <summary> + /// The number of bytes of data sent. + /// </summary> + long dataBytesSent; + + /// <summary> + /// The number of bytes sent in total. + /// </summary> + /// <remarks> + /// <para> + /// This is the total number of bytes (the data bytes plus protocol bytes) that were sent from the + /// <see cref="Connection"/>, accumulated each time that LogSend is called by the Connection. Messages that + /// caused an error are not counted and messages are only counted once all other operations in the send are + /// complete. + /// </para> + /// <para> + /// For the number of data bytes excluding protocol bytes see <see cref="DataBytesSent"/>. + /// </para> + /// </remarks> + public long TotalBytesSent + { + get + { + return Interlocked.Read(ref totalBytesSent); + } + } + + /// <summary> + /// The number of bytes sent in total. + /// </summary> + long totalBytesSent; + + /// <summary> + /// The total number of messages received. + /// </summary> + public int MessagesReceived + { + get + { + return UnreliableMessagesReceived + ReliableMessagesReceived + FragmentedMessagesReceived + AcknowledgementMessagesReceived + helloMessagesReceived; + } + } + + /// <summary> + /// The number of unreliable messages received. + /// </summary> + /// <remarks> + /// This is the number of unreliable messages that were received by the <see cref="Connection"/>, incremented + /// each time that LogUnreliableReceive is called by the Connection. Messages are counted before the receive event is invoked. + /// </remarks> + public int UnreliableMessagesReceived + { + get + { + return unreliableMessagesReceived; + } + } + + /// <summary> + /// The number of unreliable messages received. + /// </summary> + int unreliableMessagesReceived; + + /// <summary> + /// The number of reliable messages received. + /// </summary> + /// <remarks> + /// This is the number of reliable messages that were received by the <see cref="Connection"/>, incremented + /// each time that LogReliableReceive is called by the Connection. Messages are counted before the receive event is invoked. + /// </remarks> + public int ReliableMessagesReceived + { + get + { + return reliableMessagesReceived; + } + } + + /// <summary> + /// The number of reliable messages received. + /// </summary> + int reliableMessagesReceived; + + /// <summary> + /// The number of fragmented messages received. + /// </summary> + /// <remarks> + /// This is the number of fragmented messages that were received by the <see cref="Connection"/>, incremented + /// each time that LogFragmentedReceive is called by the Connection. Messages are counted before the receive event is invoked. + /// </remarks> + public int FragmentedMessagesReceived + { + get + { + return fragmentedMessagesReceived; + } + } + + /// <summary> + /// The number of fragmented messages received. + /// </summary> + int fragmentedMessagesReceived; + + /// <summary> + /// The number of acknowledgement messages received. + /// </summary> + /// <remarks> + /// This is the number of acknowledgement messages that were received by the <see cref="Connection"/>, incremented + /// each time that LogAcknowledgemntReceive is called by the Connection. Messages are counted before the receive event is invoked. + /// </remarks> + public int AcknowledgementMessagesReceived + { + get + { + return acknowledgementMessagesReceived; + } + } + + /// <summary> + /// The number of acknowledgement messages received. + /// </summary> + int acknowledgementMessagesReceived; + + /// <summary> + /// The number of ping messages received. + /// </summary> + /// <remarks> + /// This is the number of hello messages that were received by the <see cref="Connection"/>, incremented + /// each time that LogHelloReceive is called by the Connection. Messages are counted before the receive event is invoked. + /// </remarks> + public int PingMessagesReceived + { + get + { + return pingMessagesReceived; + } + } + + /// <summary> + /// The number of hello messages received. + /// </summary> + int pingMessagesReceived; + + /// <summary> + /// The number of hello messages received. + /// </summary> + /// <remarks> + /// This is the number of hello messages that were received by the <see cref="Connection"/>, incremented + /// each time that LogHelloReceive is called by the Connection. Messages are counted before the receive event is invoked. + /// </remarks> + public int HelloMessagesReceived + { + get + { + return helloMessagesReceived; + } + } + + /// <summary> + /// The number of hello messages received. + /// </summary> + int helloMessagesReceived; + + /// <summary> + /// The number of bytes of data received. + /// </summary> + /// <remarks> + /// <para> + /// This is the number of bytes of data (i.e. user bytes) that were received by the <see cref="Connection"/>, + /// accumulated each time that LogReceive is called by the Connection. Messages are counted before the receive + /// event is invoked. + /// </para> + /// <para> + /// For the number of bytes including protocol bytes see <see cref="TotalBytesReceived"/>. + /// </para> + /// </remarks> + public long DataBytesReceived + { + get + { + return Interlocked.Read(ref dataBytesReceived); + } + } + + /// <summary> + /// The number of bytes of data received. + /// </summary> + long dataBytesReceived; + + /// <summary> + /// The number of bytes received in total. + /// </summary> + /// <remarks> + /// <para> + /// This is the total number of bytes (the data bytes plus protocol bytes) that were received by the + /// <see cref="Connection"/>, accumulated each time that LogReceive is called by the Connection. Messages are + /// counted before the receive event is invoked. + /// </para> + /// <para> + /// For the number of data bytes excluding protocol bytes see <see cref="DataBytesReceived"/>. + /// </para> + /// </remarks> + public long TotalBytesReceived + { + get + { + return Interlocked.Read(ref totalBytesReceived); + } + } + + /// <summary> + /// The number of bytes received in total. + /// </summary> + long totalBytesReceived; + + public int MessagesResent { get { return messagesResent; } } + int messagesResent; + + /// <summary> + /// Logs the sending of an unreliable data packet in the statistics. + /// </summary> + /// <param name="dataLength">The number of bytes of data sent.</param> + /// <param name="totalLength">The total number of bytes sent.</param> + /// <remarks> + /// This should be called after the data has been sent and should only be called for data that is sent sucessfully. + /// </remarks> + internal void LogUnreliableSend(int dataLength, int totalLength) + { + Interlocked.Increment(ref unreliableMessagesSent); + Interlocked.Add(ref dataBytesSent, dataLength); + Interlocked.Add(ref totalBytesSent, totalLength); + + if (totalLength > ExpectedMTU) + { + Interlocked.Increment(ref fragmentableMessagesSent); + } + } + + /// <summary> + /// Logs the sending of a reliable data packet in the statistics. + /// </summary> + /// <param name="dataLength">The number of bytes of data sent.</param> + /// <param name="totalLength">The total number of bytes sent.</param> + /// <remarks> + /// This should be called after the data has been sent and should only be called for data that is sent sucessfully. + /// </remarks> + internal void LogReliableSend(int dataLength, int totalLength) + { + Interlocked.Increment(ref reliableMessagesSent); + Interlocked.Add(ref dataBytesSent, dataLength); + Interlocked.Add(ref totalBytesSent, totalLength); + + if (totalLength > ExpectedMTU) + { + Interlocked.Increment(ref fragmentableMessagesSent); + } + } + + /// <summary> + /// Logs the sending of a fragmented data packet in the statistics. + /// </summary> + /// <param name="dataLength">The number of bytes of data sent.</param> + /// <param name="totalLength">The total number of bytes sent.</param> + /// <remarks> + /// This should be called after the data has been sent and should only be called for data that is sent sucessfully. + /// </remarks> + internal void LogFragmentedSend(int dataLength, int totalLength) + { + Interlocked.Increment(ref fragmentedMessagesSent); + Interlocked.Add(ref dataBytesSent, dataLength); + Interlocked.Add(ref totalBytesSent, totalLength); + + if (totalLength > ExpectedMTU) + { + Interlocked.Increment(ref fragmentableMessagesSent); + } + } + + /// <summary> + /// Logs the sending of a acknowledgement data packet in the statistics. + /// </summary> + /// <param name="totalLength">The total number of bytes sent.</param> + /// <remarks> + /// This should be called after the data has been sent and should only be called for data that is sent sucessfully. + /// </remarks> + internal void LogAcknowledgementSend(int totalLength) + { + Interlocked.Increment(ref acknowledgementMessagesSent); + Interlocked.Add(ref totalBytesSent, totalLength); + } + + /// <summary> + /// Logs the sending of a hellp data packet in the statistics. + /// </summary> + /// <param name="totalLength">The total number of bytes sent.</param> + /// <remarks> + /// This should be called after the data has been sent and should only be called for data that is sent sucessfully. + /// </remarks> + internal void LogHelloSend(int totalLength) + { + Interlocked.Increment(ref helloMessagesSent); + Interlocked.Add(ref totalBytesSent, totalLength); + } + + /// <summary> + /// Logs the receiving of an unreliable data packet in the statistics. + /// </summary> + /// <param name="dataLength">The number of bytes of data received.</param> + /// <param name="totalLength">The total number of bytes received.</param> + /// <remarks> + /// This should be called before the received event is invoked so it is up to date for subscribers to that event. + /// </remarks> + internal void LogUnreliableReceive(int dataLength, int totalLength) + { + Interlocked.Increment(ref unreliableMessagesReceived); + Interlocked.Add(ref dataBytesReceived, dataLength); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + + /// <summary> + /// Logs the receiving of a reliable data packet in the statistics. + /// </summary> + /// <param name="dataLength">The number of bytes of data received.</param> + /// <param name="totalLength">The total number of bytes received.</param> + /// <remarks> + /// This should be called before the received event is invoked so it is up to date for subscribers to that event. + /// </remarks> + internal void LogReliableReceive(int dataLength, int totalLength) + { + Interlocked.Increment(ref reliableMessagesReceived); + Interlocked.Add(ref dataBytesReceived, dataLength); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + + /// <summary> + /// Logs the receiving of a fragmented data packet in the statistics. + /// </summary> + /// <param name="dataLength">The number of bytes of data received.</param> + /// <param name="totalLength">The total number of bytes received.</param> + /// <remarks> + /// This should be called before the received event is invoked so it is up to date for subscribers to that event. + /// </remarks> + internal void LogFragmentedReceive(int dataLength, int totalLength) + { + Interlocked.Increment(ref fragmentedMessagesReceived); + Interlocked.Add(ref dataBytesReceived, dataLength); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + + /// <summary> + /// Logs the receiving of an acknowledgement data packet in the statistics. + /// </summary> + /// <param name="totalLength">The total number of bytes received.</param> + /// <remarks> + /// This should be called before the received event is invoked so it is up to date for subscribers to that event. + /// </remarks> + internal void LogAcknowledgementReceive(int totalLength) + { + Interlocked.Increment(ref acknowledgementMessagesReceived); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + + /// <summary> + /// Logs the receiving of a hello data packet in the statistics. + /// </summary> + /// <param name="totalLength">The total number of bytes received.</param> + /// <remarks> + /// This should be called before the received event is invoked so it is up to date for subscribers to that event. + /// </remarks> + internal void LogPingReceive(int totalLength) + { + Interlocked.Increment(ref pingMessagesReceived); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + + /// <summary> + /// Logs the receiving of a hello data packet in the statistics. + /// </summary> + /// <param name="totalLength">The total number of bytes received.</param> + /// <remarks> + /// This should be called before the received event is invoked so it is up to date for subscribers to that event. + /// </remarks> + internal void LogHelloReceive(int totalLength) + { + Interlocked.Increment(ref helloMessagesReceived); + Interlocked.Add(ref totalBytesReceived, totalLength); + } + + internal void LogMessageResent() + { + Interlocked.Increment(ref messagesResent); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs b/Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs new file mode 100644 index 0000000..9176d8d --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/DataReceivedEventArgs.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Hazel +{ + public struct DataReceivedEventArgs + { + public readonly Connection Sender; + + /// <summary> + /// The bytes received from the client. + /// </summary> + public readonly IMessageReader Message; + + /// <summary> + /// The <see cref="Type"/> the data was sent with. + /// </summary> + public readonly MessageType Type; + + public DataReceivedEventArgs(Connection sender, IMessageReader msg, MessageType type) + { + this.Sender = sender; + this.Message = msg; + this.Type = type; + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs b/Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs new file mode 100644 index 0000000..d46df4b --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/DisconnectedEventArgs.cs @@ -0,0 +1,25 @@ +using System; +using Impostor.Api.Net.Messages; + +namespace Impostor.Hazel +{ + public class DisconnectedEventArgs : EventArgs + { + /// <summary> + /// Optional disconnect reason. May be null. + /// </summary> + public readonly string Reason; + + /// <summary> + /// Optional data sent with a disconnect message. May be null. + /// You must not recycle this. If you need the message outside of a callback, you should copy it. + /// </summary> + public readonly IMessageReader Message; + + public DisconnectedEventArgs(string reason, IMessageReader message) + { + this.Reason = reason; + this.Message = message; + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs b/Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs new file mode 100644 index 0000000..56c7380 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Extensions/ServiceProviderExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Hazel.Extensions +{ + public static class ServiceProviderExtensions + { + public static void AddHazel(this IServiceCollection services) + { + services.TryAddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider()); + + services.AddSingleton(serviceProvider => + { + var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>(); + var policy = ActivatorUtilities.CreateInstance<MessageReaderPolicy>(serviceProvider); + return provider.Create(policy); + }); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/HazelException.cs b/Impostor-dev/src/Impostor.Hazel/HazelException.cs new file mode 100644 index 0000000..8c6fc3c --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/HazelException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Impostor.Hazel +{ + /// <summary> + /// Wrapper for exceptions thrown from Hazel. + /// </summary> + [Serializable] + public class HazelException : Exception + { + internal HazelException(string msg) : base (msg) + { + + } + + internal HazelException(string msg, Exception e) : base (msg, e) + { + + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/IPMode.cs b/Impostor-dev/src/Impostor.Hazel/IPMode.cs new file mode 100644 index 0000000..5eb6679 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/IPMode.cs @@ -0,0 +1,24 @@ +namespace Impostor.Hazel +{ + /// <summary> + /// Represents the IP version that a connection or listener will use. + /// </summary> + /// <remarks> + /// If you wand a client to connect or be able to connect using IPv6 then you should use <see cref="IPv4AndIPv6"/>, + /// this sets the underlying sockets to use IPv6 but still allow IPv4 sockets to connect for backwards compatability + /// and hence it is the default IPMode in most cases. + /// </remarks> + public enum IPMode + { + /// <summary> + /// Instruction to use IPv4 only, IPv6 connections will not be able to connect. + /// </summary> + IPv4, + + /// <summary> + /// Instruction to use IPv6 only, IPv4 connections will not be able to connect. IPv4 addresses can be connected + /// by converting to IPv6 addresses. + /// </summary> + IPv6 + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/IRecyclable.cs b/Impostor-dev/src/Impostor.Hazel/IRecyclable.cs new file mode 100644 index 0000000..69be122 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/IRecyclable.cs @@ -0,0 +1,24 @@ +namespace Impostor.Hazel +{ + /// <summary> + /// Interface for all items that can be returned to an object pool. + /// </summary> + /// <threadsafety static="true" instance="true"/> + public interface IRecyclable + { + /// <summary> + /// Returns this object back to the object pool. + /// </summary> + /// <remarks> + /// <para> + /// Calling this when you are done with the object returns the object back to a pool in order to be reused. + /// This can reduce the amount of work the GC has to do dramatically but it is optional to call this. + /// </para> + /// <para> + /// Calling this indicates to Hazel that this can be reused and thus you should only call this when you are + /// completely finished with the object as the contents can be overwritten at any point after. + /// </para> + /// </remarks> + void Recycle(); + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj b/Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj new file mode 100644 index 0000000..3e035fb --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Impostor.Hazel.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <TargetFramework>net5.0</TargetFramework> + <DefineConstants>HAZEL_BAG</DefineConstants> + <Version>1.0.0</Version> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0" /> + <PackageReference Include="Serilog" Version="2.10.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Hazel/MessageReader.cs b/Impostor-dev/src/Impostor.Hazel/MessageReader.cs new file mode 100644 index 0000000..986d0b0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/MessageReader.cs @@ -0,0 +1,256 @@ +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Text; +using Impostor.Api; +using Impostor.Api.Net.Messages; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Hazel +{ + public class MessageReader : IMessageReader + { + private static readonly ArrayPool<byte> ArrayPool = ArrayPool<byte>.Shared; + + private readonly ObjectPool<MessageReader> _pool; + private bool _inUse; + + internal MessageReader(ObjectPool<MessageReader> pool) + { + _pool = pool; + } + + public byte[] Buffer { get; private set; } + + public int Offset { get; internal set; } + + public int Position { get; internal set; } + + public int Length { get; internal set; } + + public byte Tag { get; private set; } + + public MessageReader Parent { get; private set; } + + private int ReadPosition => Offset + Position; + + public void Update(byte[] buffer, int offset = 0, int position = 0, int? length = null, byte tag = byte.MaxValue, MessageReader parent = null) + { + _inUse = true; + + Buffer = buffer; + Offset = offset; + Position = position; + Length = length ?? buffer.Length; + Tag = tag; + Parent = parent; + } + + internal void Reset() + { + _inUse = false; + + Tag = byte.MaxValue; + Buffer = null; + Offset = 0; + Position = 0; + Length = 0; + Parent = null; + } + + public IMessageReader ReadMessage() + { + var length = ReadUInt16(); + var tag = FastByte(); + var pos = ReadPosition; + + Position += length; + + var reader = _pool.Get(); + reader.Update(Buffer, pos, 0, length, tag, this); + return reader; + } + + public bool ReadBoolean() + { + byte val = FastByte(); + return val != 0; + } + + public sbyte ReadSByte() + { + return (sbyte)FastByte(); + } + + public byte ReadByte() + { + return FastByte(); + } + + public ushort ReadUInt16() + { + var output = BinaryPrimitives.ReadUInt16LittleEndian(Buffer.AsSpan(ReadPosition)); + Position += sizeof(ushort); + return output; + } + + public short ReadInt16() + { + var output = BinaryPrimitives.ReadInt16LittleEndian(Buffer.AsSpan(ReadPosition)); + Position += sizeof(short); + return output; + } + + public uint ReadUInt32() + { + var output = BinaryPrimitives.ReadUInt32LittleEndian(Buffer.AsSpan(ReadPosition)); + Position += sizeof(uint); + return output; + } + + public int ReadInt32() + { + var output = BinaryPrimitives.ReadInt32LittleEndian(Buffer.AsSpan(ReadPosition)); + Position += sizeof(int); + return output; + } + + public unsafe float ReadSingle() + { + var output = BinaryPrimitives.ReadSingleLittleEndian(Buffer.AsSpan(ReadPosition)); + Position += sizeof(float); + return output; + } + + public string ReadString() + { + var len = ReadPackedInt32(); + var output = Encoding.UTF8.GetString(Buffer.AsSpan(ReadPosition, len)); + Position += len; + return output; + } + + public ReadOnlyMemory<byte> ReadBytesAndSize() + { + var len = ReadPackedInt32(); + return ReadBytes(len); + } + + public ReadOnlyMemory<byte> ReadBytes(int length) + { + var output = Buffer.AsMemory(ReadPosition, length); + Position += length; + return output; + } + + public int ReadPackedInt32() + { + return (int)ReadPackedUInt32(); + } + + public uint ReadPackedUInt32() + { + bool readMore = true; + int shift = 0; + uint output = 0; + + while (readMore) + { + byte b = FastByte(); + if (b >= 0x80) + { + readMore = true; + b ^= 0x80; + } + else + { + readMore = false; + } + + output |= (uint)(b << shift); + shift += 7; + } + + return output; + } + + public void CopyTo(IMessageWriter writer) + { + writer.Write((ushort) Length); + writer.Write((byte) Tag); + writer.Write(Buffer.AsMemory(Offset, Length)); + } + + public void Seek(int position) + { + Position = position; + } + + public void RemoveMessage(IMessageReader message) + { + if (message.Buffer != Buffer) + { + throw new ImpostorProtocolException("Tried to remove message from a message that does not have the same buffer."); + } + + // Offset of where to start removing. + var offsetStart = message.Offset - 3; + + // Offset of where to end removing. + var offsetEnd = message.Offset + message.Length; + + // The amount of bytes to copy over ourselves. + var lengthToCopy = message.Buffer.Length - offsetEnd; + + System.Buffer.BlockCopy(Buffer, offsetEnd, Buffer, offsetStart, lengthToCopy); + + ((MessageReader) message).Parent.AdjustLength(message.Offset, message.Length + 3); + } + + private void AdjustLength(int offset, int amount) + { + this.Length -= amount; + + if (this.ReadPosition > offset) + { + this.Position -= amount; + } + + if (Parent != null) + { + var lengthOffset = this.Offset - 3; + var curLen = this.Buffer[lengthOffset] | + (this.Buffer[lengthOffset + 1] << 8); + + curLen -= amount; + + this.Buffer[lengthOffset] = (byte)curLen; + this.Buffer[lengthOffset + 1] = (byte)(this.Buffer[lengthOffset + 1] >> 8); + + Parent.AdjustLength(offset, amount); + } + } + + public IMessageReader Copy(int offset = 0) + { + var reader = _pool.Get(); + reader.Update(Buffer, Offset + offset, Position, Length - offset, Tag, Parent); + return reader; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte FastByte() + { + return Buffer[Offset + Position++]; + } + + public void Dispose() + { + if (_inUse) + { + _pool.Return(this); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs b/Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs new file mode 100644 index 0000000..ef3939a --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/MessageReaderPolicy.cs @@ -0,0 +1,27 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Hazel +{ + public class MessageReaderPolicy : IPooledObjectPolicy<MessageReader> + { + private readonly IServiceProvider _serviceProvider; + + public MessageReaderPolicy(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public MessageReader Create() + { + return new MessageReader(_serviceProvider.GetRequiredService<ObjectPool<MessageReader>>()); + } + + public bool Return(MessageReader obj) + { + obj.Reset(); + return true; + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/MessageWriter.cs b/Impostor-dev/src/Impostor.Hazel/MessageWriter.cs new file mode 100644 index 0000000..5b7342a --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/MessageWriter.cs @@ -0,0 +1,335 @@ +using Impostor.Api.Games; +using Impostor.Api.Net.Messages; + +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace Impostor.Hazel +{ + public class MessageWriter : IMessageWriter, IRecyclable, IDisposable + { + private static int BufferSize = 64000; + private static readonly ObjectPoolCustom<MessageWriter> WriterPool = new ObjectPoolCustom<MessageWriter>(() => new MessageWriter(BufferSize)); + + public MessageType SendOption { get; private set; } + + private Stack<int> messageStarts = new Stack<int>(); + + public MessageWriter(byte[] buffer) + { + this.Buffer = buffer; + this.Length = this.Buffer.Length; + } + + public MessageWriter(int bufferSize) + { + this.Buffer = new byte[bufferSize]; + } + + public byte[] Buffer { get; } + public int Length { get; set; } + public int Position { get; set; } + + public byte[] ToByteArray(bool includeHeader) + { + if (includeHeader) + { + byte[] output = new byte[this.Length]; + System.Buffer.BlockCopy(this.Buffer, 0, output, 0, this.Length); + return output; + } + else + { + switch (this.SendOption) + { + case MessageType.Reliable: + { + byte[] output = new byte[this.Length - 3]; + System.Buffer.BlockCopy(this.Buffer, 3, output, 0, this.Length - 3); + return output; + } + case MessageType.Unreliable: + { + byte[] output = new byte[this.Length - 1]; + System.Buffer.BlockCopy(this.Buffer, 1, output, 0, this.Length - 1); + return output; + } + default: + throw new ArgumentOutOfRangeException(); + } + } + + throw new NotImplementedException(); + } + + /// + /// <param name="sendOption">The option specifying how the message should be sent.</param> + public static MessageWriter Get(MessageType sendOption = MessageType.Unreliable) + { + var output = WriterPool.GetObject(); + output.Clear(sendOption); + + return output; + } + + public bool HasBytes(int expected) + { + if (this.SendOption == MessageType.Unreliable) + { + return this.Length > 1 + expected; + } + + return this.Length > 3 + expected; + } + + public void Write(GameCode value) + { + this.Write(value.Value); + } + + /// + public void StartMessage(byte typeFlag) + { + messageStarts.Push(this.Position); + this.Position += 2; // Skip for size + this.Write(typeFlag); + } + + /// + public void EndMessage() + { + var lastMessageStart = messageStarts.Pop(); + ushort length = (ushort)(this.Position - lastMessageStart - 3); // Minus length and type byte + this.Buffer[lastMessageStart] = (byte)length; + this.Buffer[lastMessageStart + 1] = (byte)(length >> 8); + } + + /// + public void CancelMessage() + { + this.Position = this.messageStarts.Pop(); + this.Length = this.Position; + } + + public void Clear(MessageType sendOption) + { + this.messageStarts.Clear(); + this.SendOption = sendOption; + this.Buffer[0] = (byte)sendOption; + switch (sendOption) + { + default: + case MessageType.Unreliable: + this.Length = this.Position = 1; + break; + + case MessageType.Reliable: + this.Length = this.Position = 3; + break; + } + } + + /// + public void Recycle() + { + this.Position = this.Length = 0; + WriterPool.PutObject(this); + } + + #region WriteMethods + + public void Write(bool value) + { + this.Buffer[this.Position++] = (byte)(value ? 1 : 0); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(sbyte value) + { + this.Buffer[this.Position++] = (byte)value; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte value) + { + this.Buffer[this.Position++] = value; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(short value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(ushort value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(uint value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + this.Buffer[this.Position++] = (byte)(value >> 16); + this.Buffer[this.Position++] = (byte)(value >> 24); + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(int value) + { + this.Buffer[this.Position++] = (byte)value; + this.Buffer[this.Position++] = (byte)(value >> 8); + this.Buffer[this.Position++] = (byte)(value >> 16); + this.Buffer[this.Position++] = (byte)(value >> 24); + if (this.Position > this.Length) this.Length = this.Position; + } + + public unsafe void Write(float value) + { + fixed (byte* ptr = &this.Buffer[this.Position]) + { + byte* valuePtr = (byte*)&value; + + *ptr = *valuePtr; + *(ptr + 1) = *(valuePtr + 1); + *(ptr + 2) = *(valuePtr + 2); + *(ptr + 3) = *(valuePtr + 3); + } + + this.Position += 4; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(string value) + { + var bytes = UTF8Encoding.UTF8.GetBytes(value); + this.WritePacked(bytes.Length); + this.Write(bytes); + } + + public void Write(IPAddress value) + { + this.Write(value.GetAddressBytes()); + } + + public void WriteBytesAndSize(byte[] bytes) + { + this.WritePacked((uint)bytes.Length); + this.Write(bytes); + } + + public void WriteBytesAndSize(byte[] bytes, int length) + { + this.WritePacked((uint)length); + this.Write(bytes, length); + } + + public void WriteBytesAndSize(byte[] bytes, int offset, int length) + { + this.WritePacked((uint)length); + this.Write(bytes, offset, length); + } + + public void Write(ReadOnlyMemory<byte> data) + { + Write(data.Span); + } + + public void Write(ReadOnlySpan<byte> bytes) + { + bytes.CopyTo(this.Buffer.AsSpan(this.Position, bytes.Length)); + + this.Position += bytes.Length; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte[] bytes) + { + Array.Copy(bytes, 0, this.Buffer, this.Position, bytes.Length); + this.Position += bytes.Length; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte[] bytes, int offset, int length) + { + Array.Copy(bytes, offset, this.Buffer, this.Position, length); + this.Position += length; + if (this.Position > this.Length) this.Length = this.Position; + } + + public void Write(byte[] bytes, int length) + { + Array.Copy(bytes, 0, this.Buffer, this.Position, length); + this.Position += length; + if (this.Position > this.Length) this.Length = this.Position; + } + + /// + public void WritePacked(int value) + { + this.WritePacked((uint)value); + } + + /// + public void WritePacked(uint value) + { + do + { + byte b = (byte)(value & 0xFF); + if (value >= 0x80) + { + b |= 0x80; + } + + this.Write(b); + value >>= 7; + } while (value > 0); + } + + #endregion WriteMethods + + public void Write(MessageWriter msg, bool includeHeader) + { + int offset = 0; + if (!includeHeader) + { + switch (msg.SendOption) + { + case MessageType.Unreliable: + offset = 1; + break; + + case MessageType.Reliable: + offset = 3; + break; + } + } + + this.Write(msg.Buffer, offset, msg.Length - offset); + } + + public unsafe static bool IsLittleEndian() + { + byte b; + unsafe + { + int i = 1; + byte* bp = (byte*)&i; + b = *bp; + } + + return b == 1; + } + + public void Dispose() + { + Recycle(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs b/Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs new file mode 100644 index 0000000..282fe10 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/NetworkConnection.cs @@ -0,0 +1,121 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; + +namespace Impostor.Hazel +{ + public enum HazelInternalErrors + { + SocketExceptionSend, + SocketExceptionReceive, + ReceivedZeroBytes, + PingsWithoutResponse, + ReliablePacketWithoutResponse, + ConnectionDisconnected + } + + /// <summary> + /// Abstract base class for a <see cref="Connection"/> to a remote end point via a network protocol like TCP or UDP. + /// </summary> + /// <threadsafety static="true" instance="true"/> + public abstract class NetworkConnection : Connection + { + /// <summary> + /// An event that gives us a chance to send well-formed disconnect messages to clients when an internal disconnect happens. + /// </summary> + public Func<HazelInternalErrors, MessageWriter> OnInternalDisconnect; + + /// <summary> + /// The remote end point of this connection. + /// </summary> + /// <remarks> + /// This is the end point of the other device given as an <see cref="System.Net.EndPoint"/> rather than a generic + /// <see cref="ConnectionEndPoint"/> as the base <see cref="Connection"/> does. + /// </remarks> + public IPEndPoint RemoteEndPoint { get; protected set; } + + public long GetIP4Address() + { + if (IPMode == IPMode.IPv4) + { + return ((IPEndPoint)this.RemoteEndPoint).Address.Address; + } + else + { + var bytes = ((IPEndPoint)this.RemoteEndPoint).Address.GetAddressBytes(); + return BitConverter.ToInt64(bytes, bytes.Length - 8); + } + } + + /// <summary> + /// Sends a disconnect message to the end point. + /// </summary> + protected abstract ValueTask<bool> SendDisconnect(MessageWriter writer); + + /// <summary> + /// Called when the socket has been disconnected at the remote host. + /// </summary> + protected async ValueTask DisconnectRemote(string reason, IMessageReader reader) + { + if (await SendDisconnect(null)) + { + try + { + await InvokeDisconnected(reason, reader); + } + catch { } + } + + this.Dispose(); + } + + /// <summary> + /// Called when socket is disconnected internally + /// </summary> + internal async ValueTask DisconnectInternal(HazelInternalErrors error, string reason) + { + var handler = this.OnInternalDisconnect; + if (handler != null) + { + MessageWriter messageToRemote = handler(error); + if (messageToRemote != null) + { + try + { + await Disconnect(reason, messageToRemote); + } + finally + { + messageToRemote.Recycle(); + } + } + else + { + await Disconnect(reason); + } + } + else + { + await Disconnect(reason); + } + } + + /// <summary> + /// Called when the socket has been disconnected locally. + /// </summary> + public override async ValueTask Disconnect(string reason, MessageWriter writer = null) + { + if (await SendDisconnect(writer)) + { + try + { + await InvokeDisconnected(reason, null); + } + catch { } + } + + this.Dispose(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs b/Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs new file mode 100644 index 0000000..e1d7ffa --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/NetworkConnectionListener.cs @@ -0,0 +1,21 @@ +using System.Net; + +namespace Impostor.Hazel +{ + /// <summary> + /// Abstract base class for a <see cref="ConnectionListener"/> for network based connections. + /// </summary> + /// <threadsafety static="true" instance="true"/> + public abstract class NetworkConnectionListener : ConnectionListener + { + /// <summary> + /// The local end point the listener is listening for new clients on. + /// </summary> + public IPEndPoint EndPoint { get; protected set; } + + /// <summary> + /// The <see cref="IPMode">IPMode</see> the listener is listening for new clients on. + /// </summary> + public IPMode IPMode { get; protected set; } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs b/Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs new file mode 100644 index 0000000..be9e7a2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/NewConnectionEventArgs.cs @@ -0,0 +1,24 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Hazel +{ + public struct NewConnectionEventArgs + { + /// <summary> + /// The data received from the client in the handshake. + /// This data is yours. Remember to recycle it. + /// </summary> + public readonly IMessageReader HandshakeData; + + /// <summary> + /// The <see cref="Connection"/> to the new client. + /// </summary> + public readonly Connection Connection; + + public NewConnectionEventArgs(IMessageReader handshakeData, Connection connection) + { + this.HandshakeData = handshakeData; + this.Connection = connection; + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs b/Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs new file mode 100644 index 0000000..5c9ef9b --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/ObjectPoolCustom.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace Impostor.Hazel +{ + /// <summary> + /// A fairly simple object pool for items that will be created a lot. + /// </summary> + /// <typeparam name="T">The type that is pooled.</typeparam> + /// <threadsafety static="true" instance="true"/> + public sealed class ObjectPoolCustom<T> where T : IRecyclable + { + private int numberCreated; + public int NumberCreated { get { return numberCreated; } } + + public int NumberInUse { get { return this.inuse.Count; } } + public int NumberNotInUse { get { return this.pool.Count; } } + public int Size { get { return this.NumberInUse + this.NumberNotInUse; } } + +#if HAZEL_BAG + private readonly ConcurrentBag<T> pool = new ConcurrentBag<T>(); +#else + private readonly List<T> pool = new List<T>(); +#endif + + // Unavailable objects + private readonly ConcurrentDictionary<T, bool> inuse = new ConcurrentDictionary<T, bool>(); + + /// <summary> + /// The generator for creating new objects. + /// </summary> + /// <returns></returns> + private readonly Func<T> objectFactory; + + /// <summary> + /// Internal constructor for our ObjectPool. + /// </summary> + internal ObjectPoolCustom(Func<T> objectFactory) + { + this.objectFactory = objectFactory; + } + + /// <summary> + /// Returns a pooled object of type T, if none are available another is created. + /// </summary> + /// <returns>An instance of T.</returns> + internal T GetObject() + { +#if HAZEL_BAG + if (!pool.TryTake(out T item)) + { + Interlocked.Increment(ref numberCreated); + item = objectFactory.Invoke(); + } +#else + T item; + lock (this.pool) + { + if (this.pool.Count > 0) + { + var idx = this.pool.Count - 1; + item = this.pool[idx]; + this.pool.RemoveAt(idx); + } + else + { + Interlocked.Increment(ref numberCreated); + item = objectFactory.Invoke(); + } + } +#endif + + if (!inuse.TryAdd(item, true)) + { + throw new Exception("Duplicate pull " + typeof(T).Name); + } + + return item; + } + + /// <summary> + /// Returns an object to the pool. + /// </summary> + /// <param name="item">The item to return.</param> + internal void PutObject(T item) + { + if (inuse.TryRemove(item, out bool b)) + { +#if HAZEL_BAG + pool.Add(item); +#else + lock (this.pool) + { + pool.Add(item); + } +#endif + } + else + { +#if DEBUG + throw new Exception("Duplicate add " + typeof(T).Name); +#endif + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs b/Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs new file mode 100644 index 0000000..c0c4e21 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/SendOptionInternal.cs @@ -0,0 +1,33 @@ +namespace Impostor.Hazel.Udp +{ + /// <summary> + /// Extra internal states for SendOption enumeration when using UDP. + /// </summary> + public enum UdpSendOption : byte + { + /// <summary> + /// Hello message for initiating communication. + /// </summary> + Hello = 8, + + /// <summary> + /// A single byte of continued existence + /// </summary> + Ping = 12, + + /// <summary> + /// Message for discontinuing communication. + /// </summary> + Disconnect = 9, + + /// <summary> + /// Message acknowledging the receipt of a message. + /// </summary> + Acknowledgement = 10, + + /// <summary> + /// Message that is part of a larger, fragmented message. + /// </summary> + Fragment = 11, + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs new file mode 100644 index 0000000..ed7b68d --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcastListener.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Impostor.Hazel.Udp +{ + public class BroadcastPacket + { + public string Data; + public DateTime ReceiveTime; + public IPEndPoint Sender; + + public BroadcastPacket(string data, IPEndPoint sender) + { + this.Data = data; + this.Sender = sender; + this.ReceiveTime = DateTime.Now; + } + + public string GetAddress() + { + return this.Sender.Address.ToString(); + } + } + + public class UdpBroadcastListener : IDisposable + { + private Socket socket; + private EndPoint endpoint; + private Action<string> logger; + + private byte[] buffer = new byte[1024]; + + private List<BroadcastPacket> packets = new List<BroadcastPacket>(); + + public bool Running { get; private set; } + + /// + public UdpBroadcastListener(int port, Action<string> logger = null) + { + this.logger = logger; + this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + this.socket.EnableBroadcast = true; + this.socket.MulticastLoopback = false; + this.endpoint = new IPEndPoint(IPAddress.Any, port); + this.socket.Bind(this.endpoint); + } + + /// + public void StartListen() + { + if (this.Running) return; + this.Running = true; + + try + { + EndPoint endpt = new IPEndPoint(IPAddress.Any, 0); + this.socket.BeginReceiveFrom(buffer, 0, buffer.Length, SocketFlags.None, ref endpt, this.HandleData, null); + } + catch (NullReferenceException) { } + catch (Exception e) + { + this.logger?.Invoke("BroadcastListener: " + e); + this.Dispose(); + } + } + + private void HandleData(IAsyncResult result) + { + this.Running = false; + + int numBytes; + EndPoint endpt = new IPEndPoint(IPAddress.Any, 0); + try + { + numBytes = this.socket.EndReceiveFrom(result, ref endpt); + } + catch (NullReferenceException) + { + // Already disposed + return; + } + catch (Exception e) + { + this.logger?.Invoke("BroadcastListener: " + e); + this.Dispose(); + return; + } + + if (numBytes < 3 + || buffer[0] != 4 || buffer[1] != 2) + { + this.StartListen(); + return; + } + + IPEndPoint ipEnd = (IPEndPoint)endpt; + string data = UTF8Encoding.UTF8.GetString(buffer, 2, numBytes - 2); + int dataHash = data.GetHashCode(); + + lock (packets) + { + bool found = false; + for (int i = 0; i < this.packets.Count; ++i) + { + var pkt = this.packets[i]; + if (pkt == null || pkt.Data == null) + { + this.packets.RemoveAt(i); + i--; + continue; + } + + if (pkt.Data.GetHashCode() == dataHash + && pkt.Sender.Equals(ipEnd)) + { + this.packets[i].ReceiveTime = DateTime.Now; + break; + } + } + + if (!found) + { + this.packets.Add(new BroadcastPacket(data, ipEnd)); + } + } + + this.StartListen(); + } + + /// + public BroadcastPacket[] GetPackets() + { + lock (this.packets) + { + var output = this.packets.ToArray(); + this.packets.Clear(); + return output; + } + } + + /// + public void Dispose() + { + if (this.socket != null) + { + try { this.socket.Shutdown(SocketShutdown.Both); } catch { } + try { this.socket.Close(); } catch { } + try { this.socket.Dispose(); } catch { } + this.socket = null; + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs new file mode 100644 index 0000000..5fa1cca --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpBroadcaster.cs @@ -0,0 +1,79 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Impostor.Hazel.Udp +{ + /// + public class UdpBroadcaster : IDisposable + { + private Socket socket; + private byte[] data; + private EndPoint endpoint; + private Action<string> logger; + + /// + public UdpBroadcaster(int port, Action<string> logger = null) + { + this.logger = logger; + this.socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + this.socket.EnableBroadcast = true; + this.socket.MulticastLoopback = false; + this.endpoint = new IPEndPoint(IPAddress.Broadcast, port); + } + + /// + public void SetData(string data) + { + int len = UTF8Encoding.UTF8.GetByteCount(data); + this.data = new byte[len + 2]; + this.data[0] = 4; + this.data[1] = 2; + + UTF8Encoding.UTF8.GetBytes(data, 0, data.Length, this.data, 2); + } + + /// + public void Broadcast() + { + if (this.data == null) + { + return; + } + + try + { + this.socket.BeginSendTo(data, 0, data.Length, SocketFlags.None, this.endpoint, this.FinishSendTo, null); + } + catch (Exception e) + { + this.logger?.Invoke("BroadcastListener: " + e); + } + } + + private void FinishSendTo(IAsyncResult evt) + { + try + { + this.socket.EndSendTo(evt); + } + catch (Exception e) + { + this.logger?.Invoke("BroadcastListener: " + e); + } + } + + /// + public void Dispose() + { + if (this.socket != null) + { + try { this.socket.Shutdown(SocketShutdown.Both); } catch { } + try { this.socket.Close(); } catch { } + try { this.socket.Dispose(); } catch { } + this.socket = null; + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs new file mode 100644 index 0000000..5125ebe --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpClientConnection.cs @@ -0,0 +1,225 @@ +using System; +using System.Buffers; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Microsoft.Extensions.ObjectPool; +using Serilog; + +namespace Impostor.Hazel.Udp +{ + /// <summary> + /// Represents a client's connection to a server that uses the UDP protocol. + /// </summary> + /// <inheritdoc/> + public sealed class UdpClientConnection : UdpConnection + { + private static readonly ILogger Logger = Log.ForContext<UdpClientConnection>(); + + /// <summary> + /// The socket we're connected via. + /// </summary> + private readonly UdpClient _socket; + + private readonly Timer _reliablePacketTimer; + private readonly SemaphoreSlim _connectWaitLock; + private Task _listenTask; + + /// <summary> + /// Creates a new UdpClientConnection. + /// </summary> + /// <param name="remoteEndPoint">A <see cref="NetworkEndPoint"/> to connect to.</param> + public UdpClientConnection(IPEndPoint remoteEndPoint, ObjectPool<MessageReader> readerPool, IPMode ipMode = IPMode.IPv4) : base(null, readerPool) + { + EndPoint = remoteEndPoint; + RemoteEndPoint = remoteEndPoint; + IPMode = ipMode; + + _socket = new UdpClient + { + DontFragment = false + }; + + _reliablePacketTimer = new Timer(ManageReliablePacketsInternal, null, 100, Timeout.Infinite); + _connectWaitLock = new SemaphoreSlim(1, 1); + } + + ~UdpClientConnection() + { + Dispose(false); + } + + private async void ManageReliablePacketsInternal(object state) + { + await ManageReliablePackets(); + + try + { + _reliablePacketTimer.Change(100, Timeout.Infinite); + } + catch + { + // ignored + } + } + + /// <inheritdoc /> + protected override ValueTask WriteBytesToConnection(byte[] bytes, int length) + { + return WriteBytesToConnectionReal(bytes, length); + } + + private async ValueTask WriteBytesToConnectionReal(byte[] bytes, int length) + { + try + { + await _socket.SendAsync(bytes, length); + } + catch (NullReferenceException) { } + catch (ObjectDisposedException) + { + // Already disposed and disconnected... + } + catch (SocketException ex) + { + await DisconnectInternal(HazelInternalErrors.SocketExceptionSend, "Could not send data as a SocketException occurred: " + ex.Message); + } + } + + /// <inheritdoc /> + public override async ValueTask ConnectAsync(byte[] bytes = null) + { + State = ConnectionState.Connecting; + + try + { + _socket.Connect(RemoteEndPoint); + } + catch (SocketException e) + { + State = ConnectionState.NotConnected; + throw new HazelException("A SocketException occurred while binding to the port.", e); + } + + try + { + _listenTask = Task.Factory.StartNew(ListenAsync, TaskCreationOptions.LongRunning); + } + catch (ObjectDisposedException) + { + // If the socket's been disposed then we can just end there but make sure we're in NotConnected state. + // If we end up here I'm really lost... + State = ConnectionState.NotConnected; + return; + } + catch (SocketException e) + { + Dispose(); + throw new HazelException("A SocketException occurred while initiating a receive operation.", e); + } + + // Write bytes to the server to tell it hi (and to punch a hole in our NAT, if present) + // When acknowledged set the state to connected + await SendHello(bytes, () => + { + State = ConnectionState.Connected; + InitializeKeepAliveTimer(); + }); + + await _connectWaitLock.WaitAsync(TimeSpan.FromSeconds(10)); + } + + private async Task ListenAsync() + { + // Start packet handler. + await StartAsync(); + + // Listen. + while (State != ConnectionState.NotConnected) + { + UdpReceiveResult data; + + try + { + data = await _socket.ReceiveAsync(); + } + catch (SocketException e) + { + await DisconnectInternal(HazelInternalErrors.SocketExceptionReceive, "Socket exception while reading data: " + e.Message); + return; + } + catch (Exception) + { + return; + } + + if (data.Buffer.Length == 0) + { + await DisconnectInternal(HazelInternalErrors.ReceivedZeroBytes, "Received 0 bytes"); + return; + } + + // Write to client. + await Pipeline.Writer.WriteAsync(data.Buffer); + } + } + + protected override void SetState(ConnectionState state) + { + if (state == ConnectionState.Connected) + { + _connectWaitLock.Release(); + } + } + + /// <summary> + /// Sends a disconnect message to the end point. + /// You may include optional disconnect data. The SendOption must be unreliable. + /// </summary> + protected override async ValueTask<bool> SendDisconnect(MessageWriter data = null) + { + lock (this) + { + if (_state == ConnectionState.NotConnected) return false; + _state = ConnectionState.NotConnected; + } + + var bytes = EmptyDisconnectBytes; + if (data != null && data.Length > 0) + { + if (data.SendOption != MessageType.Unreliable) + { + throw new ArgumentException("Disconnect messages can only be unreliable."); + } + + bytes = data.ToByteArray(true); + bytes[0] = (byte)UdpSendOption.Disconnect; + } + + try + { + await _socket.SendAsync(bytes, bytes.Length, RemoteEndPoint); + } + catch { } + + return true; + } + + /// <inheritdoc /> + protected override void Dispose(bool disposing) + { + State = ConnectionState.NotConnected; + + try { _socket.Close(); } catch { } + try { _socket.Dispose(); } catch { } + + _reliablePacketTimer.Dispose(); + _connectWaitLock.Dispose(); + + base.Dispose(disposing); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs new file mode 100644 index 0000000..a73291b --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.KeepAlive.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Impostor.Hazel.Udp +{ + partial class UdpConnection + { + + /// <summary> + /// Class to hold packet data + /// </summary> + public class PingPacket : IRecyclable + { + private static readonly ObjectPoolCustom<PingPacket> PacketPool = new ObjectPoolCustom<PingPacket>(() => new PingPacket()); + + public readonly Stopwatch Stopwatch = new Stopwatch(); + + internal static PingPacket GetObject() + { + return PacketPool.GetObject(); + } + + public void Recycle() + { + Stopwatch.Stop(); + PacketPool.PutObject(this); + } + } + + internal ConcurrentDictionary<ushort, PingPacket> activePingPackets = new ConcurrentDictionary<ushort, PingPacket>(); + + /// <summary> + /// The interval from data being received or transmitted to a keepalive packet being sent in milliseconds. + /// </summary> + /// <remarks> + /// <para> + /// Keepalive packets serve to close connections when an endpoint abruptly disconnects and to ensure than any + /// NAT devices do not close their translation for our argument. By ensuring there is regular contact the + /// connection can detect and prevent these issues. + /// </para> + /// <para> + /// The default value is 10 seconds, set to System.Threading.Timeout.Infinite to disable keepalive packets. + /// </para> + /// </remarks> + public int KeepAliveInterval + { + get + { + return keepAliveInterval; + } + + set + { + keepAliveInterval = value; + ResetKeepAliveTimer(); + } + } + private int keepAliveInterval = 1500; + + public int MissingPingsUntilDisconnect { get; set; } = 6; + private volatile int pingsSinceAck = 0; + + /// <summary> + /// The timer creating keepalive pulses. + /// </summary> + private Timer keepAliveTimer; + + /// <summary> + /// Starts the keepalive timer. + /// </summary> + protected void InitializeKeepAliveTimer() + { + keepAliveTimer = new Timer( + HandleKeepAlive, + null, + keepAliveInterval, + keepAliveInterval + ); + } + + private async void HandleKeepAlive(object state) + { + if (this.State != ConnectionState.Connected) return; + + if (this.pingsSinceAck >= this.MissingPingsUntilDisconnect) + { + this.DisposeKeepAliveTimer(); + await this.DisconnectInternal(HazelInternalErrors.PingsWithoutResponse, $"Sent {this.pingsSinceAck} pings that remote has not responded to."); + return; + } + + try + { + Interlocked.Increment(ref pingsSinceAck); + await SendPing(); + } + catch + { + } + } + + // Pings are special, quasi-reliable packets. + // We send them to trigger responses that validate our connection is alive + // An unacked ping should never be the sole cause of a disconnect. + // Rather, the responses will reset our pingsSinceAck, enough unacked + // pings should cause a disconnect. + private async ValueTask SendPing() + { + ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated); + + byte[] bytes = new byte[3]; + bytes[0] = (byte)UdpSendOption.Ping; + bytes[1] = (byte)(id >> 8); + bytes[2] = (byte)id; + + PingPacket pkt; + if (!this.activePingPackets.TryGetValue(id, out pkt)) + { + pkt = PingPacket.GetObject(); + if (!this.activePingPackets.TryAdd(id, pkt)) + { + throw new Exception("This shouldn't be possible"); + } + } + + pkt.Stopwatch.Restart(); + + await WriteBytesToConnection(bytes, bytes.Length); + + Statistics.LogReliableSend(0, bytes.Length); + } + + /// <summary> + /// Resets the keepalive timer to zero. + /// </summary> + private void ResetKeepAliveTimer() + { + try + { + keepAliveTimer.Change(keepAliveInterval, keepAliveInterval); + } + catch { } + } + + /// <summary> + /// Disposes of the keep alive timer. + /// </summary> + private void DisposeKeepAliveTimer() + { + if (this.keepAliveTimer != null) + { + this.keepAliveTimer.Dispose(); + } + + foreach (var kvp in activePingPackets) + { + if (this.activePingPackets.TryRemove(kvp.Key, out var pkt)) + { + pkt.Recycle(); + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs new file mode 100644 index 0000000..a7a4309 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.Reliable.cs @@ -0,0 +1,491 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; + +namespace Impostor.Hazel.Udp +{ + partial class UdpConnection + { + /// <summary> + /// The starting timeout, in miliseconds, at which data will be resent. + /// </summary> + /// <remarks> + /// <para> + /// For reliable delivery data is resent at specified intervals unless an acknowledgement is received from the + /// receiving device. The ResendTimeout specifies the interval between the packets being resent, each time a packet + /// is resent the interval is increased for that packet until the duration exceeds the <see cref="DisconnectTimeout"/> value. + /// </para> + /// <para> + /// Setting this to its default of 0 will mean the timeout is 2 times the value of the average ping, usually + /// resulting in a more dynamic resend that responds to endpoints on slower or faster connections. + /// </para> + /// </remarks> + public volatile int ResendTimeout = 0; + + /// <summary> + /// Max number of times to resend. 0 == no limit + /// </summary> + public volatile int ResendLimit = 0; + + /// <summary> + /// A compounding multiplier to back off resend timeout. + /// Applied to ping before first timeout when ResendTimeout == 0. + /// </summary> + public volatile float ResendPingMultiplier = 2; + + /// <summary> + /// Holds the last ID allocated. + /// </summary> + private int lastIDAllocated = 0; + + /// <summary> + /// The packets of data that have been transmitted reliably and not acknowledged. + /// </summary> + internal ConcurrentDictionary<ushort, Packet> reliableDataPacketsSent = new ConcurrentDictionary<ushort, Packet>(); + + /// <summary> + /// Packet ids that have not been received, but are expected. + /// </summary> + private HashSet<ushort> reliableDataPacketsMissing = new HashSet<ushort>(); + + /// <summary> + /// The packet id that was received last. + /// </summary> + private volatile ushort reliableReceiveLast = ushort.MaxValue; + + private object PingLock = new object(); + + /// <summary> + /// Returns the average ping to this endpoint. + /// </summary> + /// <remarks> + /// This returns the average ping for a one-way trip as calculated from the reliable packets that have been sent + /// and acknowledged by the endpoint. + /// </remarks> + public float AveragePingMs = 500; + + /// <summary> + /// The maximum times a message should be resent before marking the endpoint as disconnected. + /// </summary> + /// <remarks> + /// Reliable packets will be resent at an interval defined in <see cref="ResendTimeout"/> for the number of times + /// specified here. Once a packet has been retransmitted this number of times and has not been acknowledged the + /// connection will be marked as disconnected and the <see cref="Connection.Disconnected">Disconnected</see> event + /// will be invoked. + /// </remarks> + public volatile int DisconnectTimeout = 5000; + + /// <summary> + /// Class to hold packet data + /// </summary> + public class Packet : IRecyclable + { + /// <summary> + /// Object pool for this event. + /// </summary> + public static readonly ObjectPoolCustom<Packet> PacketPool = new ObjectPoolCustom<Packet>(() => new Packet()); + + /// <summary> + /// Returns an instance of this object from the pool. + /// </summary> + /// <returns></returns> + internal static Packet GetObject() + { + return PacketPool.GetObject(); + } + + public ushort Id; + private byte[] Data; + private UdpConnection Connection; + private int Length; + + public int NextTimeout; + public volatile bool Acknowledged; + + public Action AckCallback; + + public int Retransmissions; + public Stopwatch Stopwatch = new Stopwatch(); + + Packet() + { + } + + internal void Set(ushort id, UdpConnection connection, byte[] data, int length, int timeout, Action ackCallback) + { + this.Id = id; + this.Data = data; + this.Connection = connection; + this.Length = length; + + this.Acknowledged = false; + this.NextTimeout = timeout; + this.AckCallback = ackCallback; + this.Retransmissions = 0; + + this.Stopwatch.Restart(); + } + + // Packets resent + public async ValueTask<int> Resend() + { + var connection = this.Connection; + if (!this.Acknowledged && connection != null) + { + long lifetime = this.Stopwatch.ElapsedMilliseconds; + if (lifetime >= connection.DisconnectTimeout) + { + if (connection.reliableDataPacketsSent.TryRemove(this.Id, out Packet self)) + { + await connection.DisconnectInternal(HazelInternalErrors.ReliablePacketWithoutResponse, $"Reliable packet {self.Id} (size={this.Length}) was not ack'd after {lifetime}ms ({self.Retransmissions} resends)"); + + self.Recycle(); + } + + return 0; + } + + if (lifetime >= this.NextTimeout) + { + ++this.Retransmissions; + if (connection.ResendLimit != 0 + && this.Retransmissions > connection.ResendLimit) + { + if (connection.reliableDataPacketsSent.TryRemove(this.Id, out Packet self)) + { + await connection.DisconnectInternal(HazelInternalErrors.ReliablePacketWithoutResponse, $"Reliable packet {self.Id} (size={this.Length}) was not ack'd after {self.Retransmissions} resends ({lifetime}ms)"); + + self.Recycle(); + } + + return 0; + } + + this.NextTimeout += (int)Math.Min(this.NextTimeout * connection.ResendPingMultiplier, 1000); + try + { + await connection.WriteBytesToConnection(this.Data, this.Length); + connection.Statistics.LogMessageResent(); + return 1; + } + catch (InvalidOperationException) + { + await connection.DisconnectInternal(HazelInternalErrors.ConnectionDisconnected, "Could not resend data as connection is no longer connected"); + } + } + } + + return 0; + } + + /// <summary> + /// Returns this object back to the object pool from whence it came. + /// </summary> + public void Recycle() + { + this.Acknowledged = true; + this.Connection = null; + + PacketPool.PutObject(this); + } + } + + internal async ValueTask<int> ManageReliablePackets() + { + int output = 0; + if (this.reliableDataPacketsSent.Count > 0) + { + foreach (var kvp in this.reliableDataPacketsSent) + { + Packet pkt = kvp.Value; + + try + { + output += await pkt.Resend(); + } + catch { } + } + } + + return output; + } + + /// <summary> + /// Adds a 2 byte ID to the packet at offset and stores the packet reference for retransmission. + /// </summary> + /// <param name="buffer">The buffer to attach to.</param> + /// <param name="offset">The offset to attach at.</param> + /// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param> + protected void AttachReliableID(byte[] buffer, int offset, int sendLength, Action ackCallback = null) + { + ushort id = (ushort)Interlocked.Increment(ref lastIDAllocated); + + buffer[offset] = (byte)(id >> 8); + buffer[offset + 1] = (byte)id; + + Packet packet = Packet.GetObject(); + packet.Set( + id, + this, + buffer, + sendLength, + ResendTimeout > 0 ? ResendTimeout : (int)Math.Min(AveragePingMs * this.ResendPingMultiplier, 300), + ackCallback); + + if (!reliableDataPacketsSent.TryAdd(id, packet)) + { + throw new Exception("That shouldn't be possible"); + } + } + + public static int ClampToInt(float value, int min, int max) + { + if (value < min) return min; + if (value > max) return max; + return (int)value; + } + + /// <summary> + /// Sends the bytes reliably and stores the send. + /// </summary> + /// <param name="sendOption"></param> + /// <param name="data">The byte array to write to.</param> + /// <param name="ackCallback">The callback to make once the packet has been acknowledged.</param> + private async ValueTask ReliableSend(byte sendOption, byte[] data, Action ackCallback = null) + { + //Inform keepalive not to send for a while + ResetKeepAliveTimer(); + + byte[] bytes = new byte[data.Length + 3]; + + //Add message type + bytes[0] = sendOption; + + //Add reliable ID + AttachReliableID(bytes, 1, bytes.Length, ackCallback); + + //Copy data into new array + Buffer.BlockCopy(data, 0, bytes, bytes.Length - data.Length, data.Length); + + //Write to connection + await WriteBytesToConnection(bytes, bytes.Length); + + Statistics.LogReliableSend(data.Length, bytes.Length); + } + + /// <summary> + /// Handles a reliable message being received and invokes the data event. + /// </summary> + /// <param name="message">The buffer received.</param> + private async ValueTask ReliableMessageReceive(MessageReader message) + { + if (await ProcessReliableReceive(message.Buffer, 1)) + { + message.Offset += 3; + message.Length -= 3; + message.Position = 0; + + await InvokeDataReceived(message, MessageType.Reliable); + } + + Statistics.LogReliableReceive(message.Length - 3, message.Length); + } + + /// <summary> + /// Handles receives from reliable packets. + /// </summary> + /// <param name="bytes">The buffer containing the data.</param> + /// <param name="offset">The offset of the reliable header.</param> + /// <returns>Whether the packet was a new packet or not.</returns> + private async ValueTask<bool> ProcessReliableReceive(ReadOnlyMemory<byte> bytes, int offset) + { + var b1 = bytes.Span[offset]; + var b2 = bytes.Span[offset + 1]; + + //Get the ID form the packet + var id = (ushort)((b1 << 8) + b2); + + //Send an acknowledgement + await SendAck(id); + + /* + * It gets a little complicated here (note the fact I'm actually using a multiline comment for once...) + * + * In a simple world if our data is greater than the last reliable packet received (reliableReceiveLast) + * then it is guaranteed to be a new packet, if it's not we can see if we are missing that packet (lookup + * in reliableDataPacketsMissing). + * + * --------rrl############# (1) + * + * (where --- are packets received already and #### are packets that will be counted as new) + * + * Unfortunately if id becomes greater than 65535 it will loop back to zero so we will add a pointer that + * specifies any packets with an id behind it are also new (overwritePointer). + * + * ####op----------rrl##### (2) + * + * ------rll#########op---- (3) + * + * Anything behind than the reliableReceiveLast pointer (but greater than the overwritePointer is either a + * missing packet or something we've already received so when we change the pointers we need to make sure + * we keep note of what hasn't been received yet (reliableDataPacketsMissing). + * + * So... + */ + + lock (reliableDataPacketsMissing) + { + //Calculate overwritePointer + ushort overwritePointer = (ushort)(reliableReceiveLast - 32768); + + //Calculate if it is a new packet by examining if it is within the range + bool isNew; + if (overwritePointer < reliableReceiveLast) + isNew = id > reliableReceiveLast || id <= overwritePointer; //Figure (2) + else + isNew = id > reliableReceiveLast && id <= overwritePointer; //Figure (3) + + //If it's new or we've not received anything yet + if (isNew) + { + // Mark items between the most recent receive and the id received as missing + if (id > reliableReceiveLast) + { + for (ushort i = (ushort)(reliableReceiveLast + 1); i < id; i++) + { + reliableDataPacketsMissing.Add(i); + } + } + else + { + int cnt = (ushort.MaxValue - reliableReceiveLast) + id; + for (ushort i = 1; i < cnt; ++i) + { + reliableDataPacketsMissing.Add((ushort)(i + reliableReceiveLast)); + } + } + + //Update the most recently received + reliableReceiveLast = id; + } + + //Else it could be a missing packet + else + { + //See if we're missing it, else this packet is a duplicate as so we return false + if (!reliableDataPacketsMissing.Remove(id)) + { + return false; + } + } + } + + return true; + } + + /// <summary> + /// Handles acknowledgement packets to us. + /// </summary> + /// <param name="bytes">The buffer containing the data.</param> + private void AcknowledgementMessageReceive(ReadOnlySpan<byte> bytes) + { + this.pingsSinceAck = 0; + + ushort id = (ushort)((bytes[1] << 8) + bytes[2]); + AcknowledgeMessageId(id); + + if (bytes.Length == 4) + { + byte recentPackets = bytes[3]; + for (int i = 1; i <= 8; ++i) + { + if ((recentPackets & 1) != 0) + { + AcknowledgeMessageId((ushort)(id - i)); + } + + recentPackets >>= 1; + } + } + + Statistics.LogReliableReceive(0, bytes.Length); + } + + private void AcknowledgeMessageId(ushort id) + { + // Dispose of timer and remove from dictionary + if (reliableDataPacketsSent.TryRemove(id, out Packet packet)) + { + float rt = packet.Stopwatch.ElapsedMilliseconds; + + packet.AckCallback?.Invoke(); + packet.Recycle(); + + lock (PingLock) + { + this.AveragePingMs = Math.Max(50, this.AveragePingMs * .7f + rt * .3f); + } + } + else if (this.activePingPackets.TryRemove(id, out PingPacket pingPkt)) + { + float rt = pingPkt.Stopwatch.ElapsedMilliseconds; + + pingPkt.Recycle(); + + lock (PingLock) + { + this.AveragePingMs = Math.Max(50, this.AveragePingMs * .7f + rt * .3f); + } + } + } + + /// <summary> + /// Sends an acknowledgement for a packet given its identification bytes. + /// </summary> + /// <param name="byte1">The first identification byte.</param> + /// <param name="byte2">The second identification byte.</param> + private async ValueTask SendAck(ushort id) + { + byte recentPackets = 0; + lock (this.reliableDataPacketsMissing) + { + for (int i = 1; i <= 8; ++i) + { + if (!this.reliableDataPacketsMissing.Contains((ushort)(id - i))) + { + recentPackets |= (byte)(1 << (i - 1)); + } + } + } + + byte[] bytes = new byte[] + { + (byte)UdpSendOption.Acknowledgement, + (byte)(id >> 8), + (byte)(id >> 0), + recentPackets + }; + + try + { + await WriteBytesToConnection(bytes, bytes.Length); + } + catch (InvalidOperationException) { } + } + + private void DisposeReliablePackets() + { + foreach (var kvp in reliableDataPacketsSent) + { + if (this.reliableDataPacketsSent.TryRemove(kvp.Key, out var pkt)) + { + pkt.Recycle(); + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs new file mode 100644 index 0000000..5288d3c --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnection.cs @@ -0,0 +1,312 @@ +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Microsoft.Extensions.ObjectPool; +using Serilog; + +namespace Impostor.Hazel.Udp +{ + /// <summary> + /// Represents a connection that uses the UDP protocol. + /// </summary> + /// <inheritdoc /> + public abstract partial class UdpConnection : NetworkConnection + { + protected static readonly byte[] EmptyDisconnectBytes = { (byte)UdpSendOption.Disconnect }; + + private static readonly ILogger Logger = Log.ForContext<UdpConnection>(); + private readonly ConnectionListener _listener; + private readonly ObjectPool<MessageReader> _readerPool; + private readonly CancellationTokenSource _stoppingCts; + + private bool _isDisposing; + private bool _isFirst = true; + private Task _executingTask; + + protected UdpConnection(ConnectionListener listener, ObjectPool<MessageReader> readerPool) + { + _listener = listener; + _readerPool = readerPool; + _stoppingCts = new CancellationTokenSource(); + + Pipeline = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + } + + internal Channel<byte[]> Pipeline { get; } + + public Task StartAsync() + { + // Store the task we're executing + _executingTask = Task.Factory.StartNew(ReadAsync, TaskCreationOptions.LongRunning); + + // If the task is completed then return it, this will bubble cancellation and failure to the caller + if (_executingTask.IsCompleted) + { + return _executingTask; + } + + // Otherwise it's running + return Task.CompletedTask; + } + + public void Stop() + { + // Stop called without start + if (_executingTask == null) + { + return; + } + + // Signal cancellation to methods. + _stoppingCts.Cancel(); + + try + { + // Cancel reader. + Pipeline.Writer.Complete(); + } + catch (ChannelClosedException) + { + // Already done. + } + + // Remove references. + if (!_isDisposing) + { + Dispose(true); + } + } + + private async Task ReadAsync() + { + var reader = new MessageReader(_readerPool); + + while (!_stoppingCts.IsCancellationRequested) + { + var result = await Pipeline.Reader.ReadAsync(_stoppingCts.Token); + + try + { + reader.Update(result); + + await HandleReceive(reader); + } + catch (Exception e) + { + Logger.Error(e, "Exception during ReadAsync"); + Dispose(true); + break; + } + } + } + + /// <summary> + /// Writes the given bytes to the connection. + /// </summary> + /// <param name="bytes">The bytes to write.</param> + /// <param name="length"></param> + protected abstract ValueTask WriteBytesToConnection(byte[] bytes, int length); + + /// <inheritdoc/> + public override async ValueTask SendAsync(IMessageWriter msg) + { + if (this._state != ConnectionState.Connected) + throw new InvalidOperationException("Could not send data as this Connection is not connected. Did you disconnect?"); + + byte[] buffer = new byte[msg.Length]; + Buffer.BlockCopy(msg.Buffer, 0, buffer, 0, msg.Length); + + switch (msg.SendOption) + { + case MessageType.Reliable: + ResetKeepAliveTimer(); + + AttachReliableID(buffer, 1, buffer.Length); + await WriteBytesToConnection(buffer, buffer.Length); + Statistics.LogReliableSend(buffer.Length - 3, buffer.Length); + break; + + default: + await WriteBytesToConnection(buffer, buffer.Length); + Statistics.LogUnreliableSend(buffer.Length - 1, buffer.Length); + break; + } + } + + /// <inheritdoc/> + /// <remarks> + /// <include file="DocInclude/common.xml" path="docs/item[@name='Connection_SendBytes_General']/*" /> + /// <para> + /// Udp connections can currently send messages using <see cref="SendOption.None"/> and + /// <see cref="SendOption.Reliable"/>. Fragmented messages are not currently supported and will default to + /// <see cref="SendOption.None"/> until implemented. + /// </para> + /// </remarks> + public override async ValueTask SendBytes(byte[] bytes, MessageType sendOption = MessageType.Unreliable) + { + //Add header information and send + await HandleSend(bytes, (byte)sendOption); + } + + /// <summary> + /// Handles the reliable/fragmented sending from this connection. + /// </summary> + /// <param name="data">The data being sent.</param> + /// <param name="sendOption">The <see cref="SendOption"/> specified as its byte value.</param> + /// <param name="ackCallback">The callback to invoke when this packet is acknowledged.</param> + /// <returns>The bytes that should actually be sent.</returns> + protected async ValueTask HandleSend(byte[] data, byte sendOption, Action ackCallback = null) + { + switch (sendOption) + { + case (byte)UdpSendOption.Ping: + case (byte)MessageType.Reliable: + case (byte)UdpSendOption.Hello: + await ReliableSend(sendOption, data, ackCallback); + break; + + //Treat all else as unreliable + default: + await UnreliableSend(sendOption, data); + break; + } + } + + /// <summary> + /// Handles the receiving of data. + /// </summary> + /// <param name="message">The buffer containing the bytes received.</param> + protected async ValueTask HandleReceive(MessageReader message) + { + // Check if the first message received is the hello packet. + if (_isFirst) + { + _isFirst = false; + + // Slice 4 bytes to get handshake data. + if (_listener != null) + { + using (var handshake = message.Copy(4)) + { + await _listener.InvokeNewConnection(handshake, this); + } + } + } + + switch (message.Buffer[0]) + { + //Handle reliable receives + case (byte)MessageType.Reliable: + await ReliableMessageReceive(message); + break; + + //Handle acknowledgments + case (byte)UdpSendOption.Acknowledgement: + AcknowledgementMessageReceive(message.Buffer); + break; + + //We need to acknowledge hello and ping messages but dont want to invoke any events! + case (byte)UdpSendOption.Ping: + await ProcessReliableReceive(message.Buffer, 1); + Statistics.LogHelloReceive(message.Length); + break; + case (byte)UdpSendOption.Hello: + await ProcessReliableReceive(message.Buffer, 1); + Statistics.LogHelloReceive(message.Length); + break; + + case (byte)UdpSendOption.Disconnect: + using (var reader = message.Copy(1)) + { + await DisconnectRemote("The remote sent a disconnect request", reader); + } + break; + + //Treat everything else as unreliable + default: + using (var reader = message.Copy(1)) + { + await InvokeDataReceived(reader, MessageType.Unreliable); + } + Statistics.LogUnreliableReceive(message.Length - 1, message.Length); + break; + } + } + + /// <summary> + /// Sends bytes using the unreliable UDP protocol. + /// </summary> + /// <param name="sendOption">The SendOption to attach.</param> + /// <param name="data">The data.</param> + ValueTask UnreliableSend(byte sendOption, byte[] data) + { + return UnreliableSend(sendOption, data, 0, data.Length); + } + + /// <summary> + /// Sends bytes using the unreliable UDP protocol. + /// </summary> + /// <param name="data">The data.</param> + /// <param name="sendOption">The SendOption to attach.</param> + /// <param name="offset"></param> + /// <param name="length"></param> + async ValueTask UnreliableSend(byte sendOption, byte[] data, int offset, int length) + { + byte[] bytes = new byte[length + 1]; + + //Add message type + bytes[0] = sendOption; + + //Copy data into new array + Buffer.BlockCopy(data, offset, bytes, bytes.Length - length, length); + + //Write to connection + await WriteBytesToConnection(bytes, bytes.Length); + + Statistics.LogUnreliableSend(length, bytes.Length); + } + + /// <summary> + /// Sends a hello packet to the remote endpoint. + /// </summary> + /// <param name="bytes"></param> + /// <param name="acknowledgeCallback">The callback to invoke when the hello packet is acknowledged.</param> + protected ValueTask SendHello(byte[] bytes, Action acknowledgeCallback) + { + //First byte of handshake is version indicator so add data after + byte[] actualBytes; + if (bytes == null) + { + actualBytes = new byte[1]; + } + else + { + actualBytes = new byte[bytes.Length + 1]; + Buffer.BlockCopy(bytes, 0, actualBytes, 1, bytes.Length); + } + + return HandleSend(actualBytes, (byte)UdpSendOption.Hello, acknowledgeCallback); + } + + /// <inheritdoc/> + protected override void Dispose(bool disposing) + { + if (disposing) + { + _isDisposing = true; + + Stop(); + DisposeKeepAliveTimer(); + DisposeReliablePackets(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs new file mode 100644 index 0000000..573a00c --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionListener.cs @@ -0,0 +1,281 @@ +using System; +using System.Buffers; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.ObjectPool; +using Serilog; + +namespace Impostor.Hazel.Udp +{ + /// <summary> + /// Listens for new UDP connections and creates UdpConnections for them. + /// </summary> + /// <inheritdoc /> + public class UdpConnectionListener : NetworkConnectionListener + { + private static readonly ILogger Logger = Log.ForContext<UdpConnectionListener>(); + + /// <summary> + /// A callback for early connection rejection. + /// * Return false to reject connection. + /// * A null response is ok, we just won't send anything. + /// </summary> + public AcceptConnectionCheck AcceptConnection; + public delegate bool AcceptConnectionCheck(IPEndPoint endPoint, byte[] input, out byte[] response); + + private readonly UdpClient _socket; + private readonly ObjectPool<MessageReader> _readerPool; + private readonly MemoryPool<byte> _pool; + private readonly Timer _reliablePacketTimer; + private readonly ConcurrentDictionary<EndPoint, UdpServerConnection> _allConnections; + private readonly CancellationTokenSource _stoppingCts; + private readonly UdpConnectionRateLimit _connectionRateLimit; + private Task _executingTask; + + /// <summary> + /// Creates a new UdpConnectionListener for the given <see cref="IPAddress"/>, port and <see cref="IPMode"/>. + /// </summary> + /// <param name="endPoint">The endpoint to listen on.</param> + /// <param name="ipMode"></param> + public UdpConnectionListener(IPEndPoint endPoint, ObjectPool<MessageReader> readerPool, IPMode ipMode = IPMode.IPv4) + { + EndPoint = endPoint; + IPMode = ipMode; + + _readerPool = readerPool; + _pool = MemoryPool<byte>.Shared; + _socket = new UdpClient(endPoint); + + try + { + _socket.DontFragment = false; + } + catch (SocketException) + { + } + + _reliablePacketTimer = new Timer(ManageReliablePackets, null, 100, Timeout.Infinite); + + _allConnections = new ConcurrentDictionary<EndPoint, UdpServerConnection>(); + + _stoppingCts = new CancellationTokenSource(); + _stoppingCts.Token.Register(() => + { + _socket.Dispose(); + }); + + _connectionRateLimit = new UdpConnectionRateLimit(); + } + + public int ConnectionCount => this._allConnections.Count; + + private async void ManageReliablePackets(object state) + { + foreach (var kvp in _allConnections) + { + var sock = kvp.Value; + await sock.ManageReliablePackets(); + } + + try + { + this._reliablePacketTimer.Change(100, Timeout.Infinite); + } + catch { } + } + + /// <inheritdoc /> + public override Task StartAsync() + { + // Store the task we're executing + _executingTask = Task.Factory.StartNew(ListenAsync, TaskCreationOptions.LongRunning); + + // If the task is completed then return it, this will bubble cancellation and failure to the caller + if (_executingTask.IsCompleted) + { + return _executingTask; + } + + // Otherwise it's running + return Task.CompletedTask; + } + + private async Task StopAsync() + { + // Stop called without start + if (_executingTask == null) + { + return; + } + + try + { + // Signal cancellation to the executing method + _stoppingCts.Cancel(); + } + finally + { + // Wait until the task completes or the timeout triggers + await Task.WhenAny(_executingTask, Task.Delay(TimeSpan.FromSeconds(5))); + } + } + + /// <summary> + /// Instructs the listener to begin listening. + /// </summary> + private async Task ListenAsync() + { + try + { + while (!_stoppingCts.IsCancellationRequested) + { + UdpReceiveResult data; + + try + { + data = await _socket.ReceiveAsync(); + + if (data.Buffer.Length == 0) + { + Logger.Fatal("Hazel read 0 bytes from UDP server socket."); + continue; + } + } + catch (SocketException) + { + // Client no longer reachable, pretend it didn't happen + continue; + } + catch (ObjectDisposedException) + { + // Socket was disposed, don't care. + return; + } + + // Get client from active clients + if (!_allConnections.TryGetValue(data.RemoteEndPoint, out var client)) + { + // Check for malformed connection attempts + if (data.Buffer[0] != (byte)UdpSendOption.Hello) + { + continue; + } + + // Check rateLimit. + if (!_connectionRateLimit.IsAllowed(data.RemoteEndPoint.Address)) + { + Logger.Warning("Ratelimited connection attempt from {0}.", data.RemoteEndPoint); + continue; + } + + // Create new client + client = new UdpServerConnection(this, data.RemoteEndPoint, IPMode, _readerPool); + + // Store the client + if (!_allConnections.TryAdd(data.RemoteEndPoint, client)) + { + throw new HazelException("Failed to add a connection. This should never happen."); + } + + // Activate the reader loop of the client + await client.StartAsync(); + } + + // Write to client. + await client.Pipeline.Writer.WriteAsync(data.Buffer); + } + } + catch (Exception e) + { + Logger.Error(e, "Listen loop error"); + } + } + +#if DEBUG + public int TestDropRate = -1; + private int dropCounter = 0; +#endif + + /// <summary> + /// Sends data from the listener socket. + /// </summary> + /// <param name="bytes">The bytes to send.</param> + /// <param name="endPoint">The endpoint to send to.</param> + internal async ValueTask SendData(byte[] bytes, int length, IPEndPoint endPoint) + { + if (length > bytes.Length) return; + +#if DEBUG + if (TestDropRate > 0) + { + if (Interlocked.Increment(ref dropCounter) % TestDropRate == 0) + { + return; + } + } +#endif + + try + { + await _socket.SendAsync(bytes, length, endPoint); + } + catch (SocketException e) + { + Logger.Error(e, "Could not send data as a SocketException occurred"); + } + catch (ObjectDisposedException) + { + //Keep alive timer probably ran, ignore + return; + } + } + + /// <summary> + /// Sends data from the listener socket. + /// </summary> + /// <param name="bytes">The bytes to send.</param> + /// <param name="length"></param> + /// <param name="endPoint">The endpoint to send to.</param> + internal void SendDataSync(byte[] bytes, int length, IPEndPoint endPoint) + { + try + { + _socket.Send(bytes, length, endPoint); + } + catch (SocketException e) + { + Logger.Error(e, "Could not send data sync as a SocketException occurred"); + } + } + + /// <summary> + /// Removes a virtual connection from the list. + /// </summary> + /// <param name="endPoint">The endpoint of the virtual connection.</param> + internal void RemoveConnectionTo(EndPoint endPoint) + { + this._allConnections.TryRemove(endPoint, out var conn); + } + + /// <inheritdoc /> + public override async ValueTask DisposeAsync() + { + foreach (var kvp in _allConnections) + { + kvp.Value.Dispose(); + } + + await StopAsync(); + + await _reliablePacketTimer.DisposeAsync(); + + _connectionRateLimit.Dispose(); + + await base.DisposeAsync(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs new file mode 100644 index 0000000..64881d3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpConnectionRateLimit.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Threading; +using Serilog; + +namespace Impostor.Hazel.Udp +{ + public class UdpConnectionRateLimit : IDisposable + { + private static readonly ILogger Logger = Log.ForContext<UdpConnectionRateLimit>(); + + // Allow burst to 5 connections. + // Decrease by 1 every second. + private const int MaxConnections = 5; + private const int FalloffMs = 1000; + + private readonly ConcurrentDictionary<IPAddress, int> _connectionCount; + private readonly Timer _timer; + private bool _isDisposed; + + public UdpConnectionRateLimit() + { + _connectionCount = new ConcurrentDictionary<IPAddress, int>(); + _timer = new Timer(UpdateRateLimit, null, FalloffMs, Timeout.Infinite); + } + + private void UpdateRateLimit(object state) + { + try + { + foreach (var pair in _connectionCount) + { + var count = pair.Value - 1; + if (count > 0) + { + _connectionCount.TryUpdate(pair.Key, count, pair.Value); + } + else + { + _connectionCount.TryRemove(pair); + } + } + } + catch (Exception e) + { + Logger.Error(e, "Exception caught in UpdateRateLimit."); + } + finally + { + if (!_isDisposed) + { + _timer.Change(FalloffMs, Timeout.Infinite); + } + } + } + + public bool IsAllowed(IPAddress key) + { + if (_connectionCount.TryGetValue(key, out var value) && value >= MaxConnections) + { + return false; + } + + _connectionCount.AddOrUpdate(key, _ => 1, (_, i) => i + 1); + return true; + } + + public void Dispose() + { + _isDisposed = true; + _timer.Dispose(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs b/Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs new file mode 100644 index 0000000..22eed98 --- /dev/null +++ b/Impostor-dev/src/Impostor.Hazel/Udp/UdpServerConnection.cs @@ -0,0 +1,97 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Hazel.Udp +{ + /// <summary> + /// Represents a servers's connection to a client that uses the UDP protocol. + /// </summary> + /// <inheritdoc/> + internal sealed class UdpServerConnection : UdpConnection + { + /// <summary> + /// The connection listener that we use the socket of. + /// </summary> + /// <remarks> + /// Udp server connections utilize the same socket in the listener for sends/receives, this is the listener that + /// created this connection and is hence the listener this conenction sends and receives via. + /// </remarks> + public UdpConnectionListener Listener { get; private set; } + + /// <summary> + /// Creates a UdpConnection for the virtual connection to the endpoint. + /// </summary> + /// <param name="listener">The listener that created this connection.</param> + /// <param name="endPoint">The endpoint that we are connected to.</param> + /// <param name="IPMode">The IPMode we are connected using.</param> + internal UdpServerConnection(UdpConnectionListener listener, IPEndPoint endPoint, IPMode IPMode, ObjectPool<MessageReader> readerPool) : base(listener, readerPool) + { + this.Listener = listener; + this.RemoteEndPoint = endPoint; + this.EndPoint = endPoint; + this.IPMode = IPMode; + + State = ConnectionState.Connected; + this.InitializeKeepAliveTimer(); + } + + /// <inheritdoc /> + protected override async ValueTask WriteBytesToConnection(byte[] bytes, int length) + { + await Listener.SendData(bytes, length, RemoteEndPoint); + } + + /// <inheritdoc /> + /// <remarks> + /// This will always throw a HazelException. + /// </remarks> + public override ValueTask ConnectAsync(byte[] bytes = null) + { + throw new InvalidOperationException("Cannot manually connect a UdpServerConnection, did you mean to use UdpClientConnection?"); + } + + /// <summary> + /// Sends a disconnect message to the end point. + /// </summary> + protected override async ValueTask<bool> SendDisconnect(MessageWriter data = null) + { + lock (this) + { + if (this._state != ConnectionState.Connected) return false; + this._state = ConnectionState.NotConnected; + } + + var bytes = EmptyDisconnectBytes; + if (data != null && data.Length > 0) + { + if (data.SendOption != MessageType.Unreliable) throw new ArgumentException("Disconnect messages can only be unreliable."); + + bytes = data.ToByteArray(true); + bytes[0] = (byte)UdpSendOption.Disconnect; + } + + try + { + await Listener.SendData(bytes, bytes.Length, RemoteEndPoint); + } + catch { } + + return true; + } + + protected override void Dispose(bool disposing) + { + Listener.RemoveConnectionTo(RemoteEndPoint); + + if (disposing) + { + SendDisconnect(); + } + + base.Dispose(disposing); + } + } +} diff --git a/Impostor-dev/src/Impostor.Patcher/Directory.Build.props b/Impostor-dev/src/Impostor.Patcher/Directory.Build.props new file mode 100644 index 0000000..5302edd --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Directory.Build.props @@ -0,0 +1,8 @@ +<Project> + <PropertyGroup> + <AssemblyTitle>Impostor</AssemblyTitle> + <Product>Impostor</Product> + <Copyright>Copyright © AeonLucid 2020</Copyright> + <Version>1.0.0</Version> + </PropertyGroup> +</Project>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj new file mode 100644 index 0000000..c59fa87 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Impostor.Patcher.Cli.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <AssemblyName>Impostor.Cli</AssemblyName> + <TargetFramework>net5.0</TargetFramework> + <RuntimeIdentifiers>win-x64;linux-x64;linux-arm;linux-arm64;osx-x64</RuntimeIdentifiers> + <OutputType>Exe</OutputType> + <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20478.1" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs new file mode 100644 index 0000000..76653a1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Cli/Program.cs @@ -0,0 +1,88 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading.Tasks; +using Impostor.Patcher.Shared; +using Impostor.Patcher.Shared.Events; + +namespace Impostor.Patcher.Cli +{ + internal static class Program + { + private static readonly AmongUsModifier Modifier = new AmongUsModifier(); + + internal static Task<int> Main(string[] args) + { + var rootCommand = new RootCommand + { + new Option<string>( + "--address", + "IP Address of the server, will prompt if not specified" + ), + new Option<string>( + "--name", + () => AmongUsModifier.DefaultRegionName, + "Name for server region" + ) + }; + + rootCommand.Handler = CommandHandler.Create<string, string>((address, name) => + { + Modifier.RegionName = name; + Modifier.Error += ModifierOnError; + Modifier.Saved += ModifierOnSaved; + + Console.WriteLine("Welcome to Impostor"); + + if (Modifier.TryLoadRegionInfo(out var regionInfo)) + { + Console.WriteLine($"Currently selected region: {regionInfo.Name} ({regionInfo.Ping}, {regionInfo.Servers.Count} server(s))"); + } + + if (address != null) + { + return Modifier.SaveIpAsync(address); + } + + return PromptAsync(); + }); + + return rootCommand.InvokeAsync(args); + } + + private static void ModifierOnSaved(object sender, SavedEventArgs e) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine("The IP Address was saved, please (re)start Among Us."); + Console.ResetColor(); + } + + private static void WriteError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + } + + private static void ModifierOnError(object sender, ErrorEventArgs e) + { + WriteError(e.Message); + } + + private static async Task PromptAsync() + { + Console.WriteLine("Please enter in the IP Address of the server you would like to use for Among Us"); + Console.WriteLine("If you want to stop playing on the server, simply select another region"); + + while (true) + { + Console.Write("> "); + + if (await Modifier.SaveIpAsync(Console.ReadLine())) + { + return; + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs new file mode 100644 index 0000000..95f5524 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/AmongUsModifier.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Gameloop.Vdf; +using Gameloop.Vdf.Linq; +using Impostor.Patcher.Shared.Events; +using Impostor.Patcher.Shared.Innersloth; +using ErrorEventArgs = Impostor.Patcher.Shared.Events.ErrorEventArgs; + +namespace Impostor.Patcher.Shared +{ + public class AmongUsModifier + { + private const uint AppId = 945360; + public const string DefaultRegionName = "Impostor"; + public const ushort DefaultPort = 22023; + + private readonly string _amongUsDir; + private readonly string _regionFile; + + public string RegionName { get; set; } = DefaultRegionName; + + public AmongUsModifier() + { + var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..", "LocalLow"); + + if (!Directory.Exists(appData)) + { + appData = FindProtonAppData(); + } + + if (appData == null) + return; + + _amongUsDir = Path.Combine(appData, "Innersloth", "Among Us"); + _regionFile = Path.Combine(_amongUsDir, "regionInfo.dat"); + } + + private string FindProtonAppData() + { + string steamApps; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + steamApps = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".steam", "steam", "steamapps"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + steamApps = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Steam", "steamapps"); + } + else + { + return null; + } + + if (!Directory.Exists(steamApps)) + return null; + + var libraries = new List<string> + { + steamApps + }; + + var vdf = Path.Combine(steamApps, "libraryfolders.vdf"); + if (File.Exists(vdf)) + { + var libraryFolders = VdfConvert.Deserialize(File.ReadAllText(vdf)); + + foreach (var libraryFolder in libraryFolders.Value.Children<VProperty>()) + { + if (!int.TryParse(libraryFolder.Key, out _)) + continue; + + libraries.Add(Path.Combine(libraryFolder.Value.Value<string>(), "steamapps")); + } + } + + foreach (var library in libraries) + { + var path = Path.Combine(library, "compatdata", AppId.ToString(), "pfx", "drive_c", "users", "steamuser", "AppData", "LocalLow"); + if (Directory.Exists(path)) + { + return path; + } + } + + return null; + } + + public async Task<bool> SaveIpAsync(string input) + { + // Filter out whitespace. + input = input.Trim(); + + // Split port from ip. + // Only IPv4 is supported so just do it simple. + var ip = string.Empty; + var port = DefaultPort; + + var parts = input.Split(':'); + if (parts.Length >= 1) + { + ip = parts[0]; + } + + if (parts.Length >= 2) + { + ushort.TryParse(parts[1], out port); + } + + // Check if a valid IP address was entered. + if (!IPAddress.TryParse(ip, out var ipAddress)) + { + // Attempt to resolve DNS. + try + { + var hostAddresses = await Dns.GetHostAddressesAsync(ip); + if (hostAddresses.Length == 0) + { + OnError("Invalid IP Address entered"); + return false; + } + + // Use first IPv4 result. + ipAddress = hostAddresses.First(x => x.AddressFamily == AddressFamily.InterNetwork); + } + catch (SocketException) + { + OnError("Failed to resolve hostname."); + return false; + } + } + + // Only IPv4. + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + OnError("Invalid IP Address entered, only IPv4 is allowed."); + return false; + } + + return WriteIp(ipAddress, port); + } + + /// <summary> + /// Writes an IP Address to the Among Us region file. + /// </summary> + /// <param name="ipAddress">The IPv4 address to write.</param> + /// <param name="port"></param> + private bool WriteIp(IPAddress ipAddress, ushort port) + { + if (ipAddress == null || + ipAddress.AddressFamily != AddressFamily.InterNetwork) + { + throw new ArgumentException(nameof(ipAddress)); + } + + if (!Directory.Exists(_amongUsDir)) + { + OnError("Among Us directory was not found, is it installed? Try running it once."); + return false; + } + + using (var file = File.Open(_regionFile, FileMode.Create, FileAccess.Write)) + using (var writer = new BinaryWriter(file)) + { + var ip = ipAddress.ToString(); + var region = new RegionInfo(RegionName, ip, new[] + { + new ServerInfo($"{RegionName}-Master-1", ip, port) + }); + + region.Serialize(writer); + + OnSaved(ip, port); + return true; + } + } + + /// <summary> + /// Loads the existing region info from the Among Us. + /// </summary> + public bool TryLoadRegionInfo(out RegionInfo regionInfo) + { + regionInfo = null; + + if (!File.Exists(_regionFile)) + { + return false; + } + + using (var file = File.Open(_regionFile, FileMode.Open, FileAccess.Read)) + using (var reader = new BinaryReader(file)) + { + try + { + regionInfo = RegionInfo.Deserialize(reader); + return true; + } + catch (Exception exception) + { + OnError("Couldn't parse region info\n" + exception); + return false; + } + } + } + + /// <summary> + /// Loads the existing IP Address from the Among Us region file + /// if it was set by Impostor before. + /// </summary> + public bool TryLoadIp(out string ipAddress) + { + ipAddress = null; + + if (!TryLoadRegionInfo(out var regionInfo)) + { + return false; + } + + if ((regionInfo.Name == RegionName || regionInfo.Name == DefaultRegionName) && regionInfo.Servers.Count >= 1) + { + ipAddress = regionInfo.Servers.ElementAt(0).Ip; + return true; + } + + return false; + } + + private void OnError(string message) + { + Error?.Invoke(this, new ErrorEventArgs(message)); + } + + private void OnSaved(string ipAddress, ushort port) + { + Saved?.Invoke(this, new SavedEventArgs(ipAddress, port)); + } + + public event EventHandler<ErrorEventArgs> Error; + public event EventHandler<SavedEventArgs> Saved; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs new file mode 100644 index 0000000..b256156 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Configuration.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Impostor.Patcher.Shared +{ + public class Configuration + { + private const string FileRecentIps = @"recent_ips.txt"; + private const int MaxRecentIps = 5; + + private readonly string _baseDir; + private readonly string _recentIpsPath; + private readonly List<string> _recentIps; + + public Configuration() + { + var appData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); + + _baseDir = Path.Combine(appData, "Impostor"); + _recentIpsPath = Path.Combine(_baseDir, FileRecentIps); + _recentIps = new List<string>(); + } + + public IReadOnlyList<string> RecentIps => _recentIps; + + public void Load() + { + if (File.Exists(_recentIpsPath)) + { + _recentIps.AddRange(File.ReadAllLines(_recentIpsPath)); + } + } + + public void Save() + { + Directory.CreateDirectory(_baseDir); + + if (!Directory.Exists(_baseDir)) + { + return; + } + + if (_recentIps.Count > 0) + { + File.WriteAllLines(_recentIpsPath, _recentIps); + } + } + + public void AddIp(string ip) + { + if (_recentIps.Contains(ip)) + { + _recentIps.Remove(ip); + } + + _recentIps.Insert(0, ip); + + if (_recentIps.Count > MaxRecentIps) + { + _recentIps.RemoveAt(MaxRecentIps); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs new file mode 100644 index 0000000..7211d5d --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/ErrorEventArgs.cs @@ -0,0 +1,14 @@ +using System; + +namespace Impostor.Patcher.Shared.Events +{ + public class ErrorEventArgs : EventArgs + { + public ErrorEventArgs(string message) + { + Message = message; + } + + public string Message { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs new file mode 100644 index 0000000..c91d071 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Events/SavedEventArgs.cs @@ -0,0 +1,16 @@ +using System; + +namespace Impostor.Patcher.Shared.Events +{ + public class SavedEventArgs : EventArgs + { + public SavedEventArgs(string ipAddress, ushort port) + { + IpAddress = ipAddress; + Port = port; + } + + public string IpAddress { get; } + public ushort Port { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj new file mode 100644 index 0000000..e480870 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Impostor.Patcher.Shared.csproj @@ -0,0 +1,12 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks> + <Version>1.0.0</Version> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Gameloop.Vdf" Version="0.6.1" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs new file mode 100644 index 0000000..01b74d1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/RegionInfo.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.IO; + +namespace Impostor.Patcher.Shared.Innersloth +{ + public class RegionInfo + { + public RegionInfo(string name, string ping, IReadOnlyList<ServerInfo> servers) + { + Name = name; + Ping = ping; + Servers = servers; + } + + public string Name { get; } + public string Ping { get; } + public IReadOnlyList<ServerInfo> Servers { get; } + + public void Serialize(BinaryWriter writer) + { + writer.Write(0); + writer.Write(Name); + writer.Write(Ping); + writer.Write(Servers.Count); + + foreach (var server in Servers) + { + server.Serialize(writer); + } + } + + public static RegionInfo Deserialize(BinaryReader reader) + { + var unknown = reader.ReadInt32(); + var name = reader.ReadString(); + var ping = reader.ReadString(); + var servers = new List<ServerInfo>(); + var serverCount = reader.ReadInt32(); + + for (var i = 0; i < serverCount; i++) + { + servers.Add(ServerInfo.Deserialize(reader)); + } + + return new RegionInfo(name, ping, servers); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs new file mode 100644 index 0000000..7203c84 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.Shared/Innersloth/ServerInfo.cs @@ -0,0 +1,37 @@ +using System.IO; +using System.Net; + +namespace Impostor.Patcher.Shared.Innersloth +{ + public class ServerInfo + { + public string Name { get; } + public string Ip { get; } + public ushort Port { get; } + + public ServerInfo(string name, string ip, ushort port) + { + Name = name; + Ip = ip; + Port = port; + } + + public void Serialize(BinaryWriter writer) + { + writer.Write(Name); + writer.Write(IPAddress.Parse(Ip).GetAddressBytes()); + writer.Write(Port); + writer.Write(0); + } + + public static ServerInfo Deserialize(BinaryReader reader) + { + var name = reader.ReadString(); + var ip = new IPAddress(reader.ReadBytes(4)).ToString(); + var port = reader.ReadUInt16(); + var unknown = reader.ReadInt32(); + + return new ServerInfo(name, ip, port); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config new file mode 100644 index 0000000..2a83c36 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/App.config @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> + +<configuration> + <startup> + <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5.2" /> + </startup> +</configuration>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs new file mode 100644 index 0000000..0f6320b --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.Designer.cs @@ -0,0 +1,141 @@ +namespace Impostor.Patcher.WinForms.Forms +{ + partial class FrmMain + { + /// <summary> + /// Required designer variable. + /// </summary> + private System.ComponentModel.IContainer components = null; + + /// <summary> + /// Clean up any resources being used. + /// </summary> + /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// <summary> + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// </summary> + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FrmMain)); + this.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.buttonLaunch = new System.Windows.Forms.Button(); + this.lblUrl = new System.Windows.Forms.Label(); + this.label3 = new System.Windows.Forms.Label(); + this.comboIp = new System.Windows.Forms.ComboBox(); + this.SuspendLayout(); + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.Location = new System.Drawing.Point(28, 139); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(60, 13); + this.label1.TabIndex = 1; + this.label1.Text = "IP Address"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label2.Location = new System.Drawing.Point(28, 23); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(225, 91); + this.label2.TabIndex = 0; + this.label2.Text = "Welcome to Impostor\r\n\r\nPlease enter in the IP Address of the \r\nserver you would l" + + "ike to use for Among Us\r\n\r\nIf you want to stop playing on the server, \r\nsimply s" + + "elect another region"; + // + // buttonLaunch + // + this.buttonLaunch.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.buttonLaunch.Location = new System.Drawing.Point(179, 155); + this.buttonLaunch.Name = "buttonLaunch"; + this.buttonLaunch.Size = new System.Drawing.Size(74, 22); + this.buttonLaunch.TabIndex = 3; + this.buttonLaunch.Text = "Save"; + this.buttonLaunch.UseVisualStyleBackColor = true; + this.buttonLaunch.Click += new System.EventHandler(this.buttonLaunch_Click); + // + // lblUrl + // + this.lblUrl.AutoSize = true; + this.lblUrl.Cursor = System.Windows.Forms.Cursors.Hand; + this.lblUrl.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Underline, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.lblUrl.ForeColor = System.Drawing.SystemColors.Highlight; + this.lblUrl.Location = new System.Drawing.Point(39, 232); + this.lblUrl.Name = "lblUrl"; + this.lblUrl.Size = new System.Drawing.Size(212, 13); + this.lblUrl.TabIndex = 5; + this.lblUrl.Text = "https://github.com/AeonLucid/Impostor"; + this.lblUrl.Click += new System.EventHandler(this.lblUrl_Click); + // + // label3 + // + this.label3.AutoSize = true; + this.label3.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label3.Location = new System.Drawing.Point(54, 216); + this.label3.Name = "label3"; + this.label3.Size = new System.Drawing.Size(182, 13); + this.label3.TabIndex = 6; + this.label3.Text = "Source code and latest versions at\r\n"; + // + // comboIp + // + this.comboIp.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.comboIp.FormattingEnabled = true; + this.comboIp.Location = new System.Drawing.Point(31, 155); + this.comboIp.Name = "comboIp"; + this.comboIp.Size = new System.Drawing.Size(141, 21); + this.comboIp.TabIndex = 2; + this.comboIp.KeyDown += new System.Windows.Forms.KeyEventHandler(this.textIp_KeyDown); + // + // FrmMain + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(283, 262); + this.Controls.Add(this.comboIp); + this.Controls.Add(this.label3); + this.Controls.Add(this.lblUrl); + this.Controls.Add(this.buttonLaunch); + this.Controls.Add(this.label2); + this.Controls.Add(this.label1); + this.ForeColor = System.Drawing.SystemColors.ControlText; + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MaximizeBox = false; + this.Name = "FrmMain"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Impostor"; + this.Load += new System.EventHandler(this.FrmMain_Load); + this.Shown += new System.EventHandler(this.FrmMain_Shown); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Button buttonLaunch; + private System.Windows.Forms.Label lblUrl; + private System.Windows.Forms.Label label3; + private System.Windows.Forms.ComboBox comboIp; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs new file mode 100644 index 0000000..5c06669 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.cs @@ -0,0 +1,114 @@ +using System; +using System.Diagnostics; +using System.Windows.Forms; +using Impostor.Patcher.Shared; +using Impostor.Patcher.Shared.Events; + +namespace Impostor.Patcher.WinForms.Forms +{ + public partial class FrmMain : Form + { + private readonly Configuration _config; + private readonly AmongUsModifier _modifier; + + public FrmMain() + { + InitializeComponent(); + + AcceptButton = buttonLaunch; + + _config = new Configuration(); + _modifier = new AmongUsModifier(); + _modifier.Error += ModifierOnError; + _modifier.Saved += ModifierOnSaved; + } + + private void ModifierOnError(object sender, ErrorEventArgs e) + { + MessageBox.Show(e.Message, "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + + comboIp.Text = string.Empty; + comboIp.Focus(); + + comboIp.Enabled = true; + buttonLaunch.Enabled = true; + } + + private void ModifierOnSaved(object sender, SavedEventArgs e) + { + MessageBox.Show("The IP Address was saved, please (re)start Among Us.", "Success", + MessageBoxButtons.OK, + MessageBoxIcon.Information); + + var ipText = e.Port == AmongUsModifier.DefaultPort + ? e.IpAddress + : $"{e.IpAddress}:{e.Port}"; + + comboIp.Text = ipText; + comboIp.Enabled = true; + buttonLaunch.Enabled = true; + + _config.AddIp(ipText); + _config.Save(); + + RefreshComboIps(); + } + + private void FrmMain_Load(object sender, EventArgs e) + { + _config.Load(); + + RefreshComboIps(); + + if (_modifier.TryLoadIp(out var ipAddress)) + { + comboIp.Text = ipAddress; + } + } + + private void FrmMain_Shown(object sender, EventArgs e) + { + comboIp.Focus(); + } + + private void textIp_KeyDown(object sender, KeyEventArgs e) + { + if (e.KeyCode != Keys.Enter) + { + return; + } + + e.Handled = true; + + buttonLaunch_Click(this, EventArgs.Empty); + } + + private async void buttonLaunch_Click(object sender, EventArgs e) + { + comboIp.Enabled = false; + buttonLaunch.Enabled = false; + + await _modifier.SaveIpAsync(comboIp.Text); + } + + private void lblUrl_Click(object sender, EventArgs e) + { + Process.Start("https://github.com/AeonLucid/Impostor"); + } + + private void RefreshComboIps() + { + comboIp.Items.Clear(); + + if (_config.RecentIps.Count > 0) + { + foreach (var ip in _config.RecentIps) + { + comboIp.Items.Add(ip); + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx new file mode 100644 index 0000000..839a9c4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Forms/FrmMain.resx @@ -0,0 +1,2338 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + <xsd:attribute ref="xml:space" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" /> + <data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value> + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABIaXAQeMocbEwpxBBAA + ZwILAEcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAALXSbAShnjgUbQWcJDRc8CwoPNAsaPGIZH01yLRc1WkUUK1FNEB9FTwYAJVIGACVSBgAlUgYA + JVIGACVSBgAlUgYAJVIGACVSBgAlUgYAJVIGACVSDRRHUA8cU04WL3Q5FSxuIwkKNQsJCzgLEB9ZChcw + dQIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAACdOpAcgMZEdFhB6BxIAcAISAHAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAKGeNBiBSeCYaPGNFFS9VWREkSW8QHUKAEB9EpwwRN70LDjTEDBM52wsRNvUIBiv/BwQp/wcD + KP8HAif/BwEm/wYBJv8GASb/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/BgAl/wYA + Jf8GASb/BgEm/wYBJ/8GAij/BwIp/wcDKf8JCDP7CAgyzAkJNMEKCzi/Cw8+ug4XTJ8NFkp8DxtSaRQp + aU0XMXgSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACRC + nAEjPpkfFxR9CBIAcAMSAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAKGmQBxc1WxQXNFopDRg9WwkILV4KCzCeCQkuswsPNOAHBSruBwQp+QcDKP8GASb/BgAl/wYA + Jf8GACX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACb/BgAn/wYAJ/8HACj/BwAp/wcAKf8HACr/BwAq/wcA + Kv8HACr/BwAq/wcAKv8HACr/BwAq/wcAKv8HACr/BwAq/wcAKf8HACj/BgAn/wYAJ/8GACX/BgAl/wYA + Jf8GACX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/BwIo/wYBJvEHAijsBwIpuAYAJaUJCTOjCAcwYgsQ + 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 + xv8AAMb/AADG/wAAxv8AAMb/BABe/wYAJf8JADf/DgCB/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/w8Afv8SAHD/EABi/wYAJ/8GACb/DABL/w0A + Uf8NAFH/DQBR/w0AUf8PAF7/EgBu/wsAQ/8GACX/BgAn/wMAdP8AAMT/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8GAKb/EQB1/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8QAGf/DQBS/w0A + Uf8NAFH/DQBR/w0AUf8JADf/BgAl/w0VOtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAGACXNBgAl/wkAO/8NAFH/DQBR/w0AUf8NAFH/DQBR/w0AUf8NAFP/EQBr/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8MAIr/AwC5/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAwP8FAD7/BgAl/wsA + Sf8GAKf/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/DACL/xIAcP8QAGL/BgAn/wYAJv8MAEn/DQBR/w0AUf8NAFH/DQBR/w0AUv8QAGT/EQBr/wkA + Ov8GACX/BgAp/wMAef8AAMP/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAK//EAB5/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8OAFn/DQBR/w0AUf8NAFH/DQBR/wkAN/8GACX/DRU61AAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYAJX0GACX/CQA7/w0A + Uf8NAFH/DQBR/w0AUf8NAFH/DQBR/w8AXv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8RAHb/BwCj/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AACx/wYAKf8GACb/DABq/wEAwf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8MAI7/EgBw/w8AX/8GACb/BgAl/wYA + KP8JADf/DABJ/w0AUf8NAFH/DQBR/w0AUv8QAGf/EQBp/wgANP8GACX/BgAo/wMAef8AAMT/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8DALT/EAB6/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xAA + Zf8NAFH/DQBR/w0AUf8NAFH/CQA3/wYAJf8NFTrUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAABgAlVwYAJf8JADv/DQBR/w0AUf8NAFH/DQBR/w0AUf8NAFL/EQBq/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DgCA/wQAtP8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAJH/BgAl/wcA + K/8HAJf/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/w0Aiv8SAHD/DwBc/wYAJv8GACX/BgAl/wYAJf8GACf/CQA3/wwATv8NAFH/DQBR/w0A + U/8QAGj/EABl/wgAM/8GACX/BgAp/wMAe/8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DALX/EAB5/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBu/w4AVv8NAFH/DQBR/w0AUf8JADf/BgAl/w0V + OtQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGACVhBgAl/wkA + O/8NAFH/DQBR/w0AUf8NAFH/DQBR/w4AWf8SAG//EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/wwAi/8CAL7/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wMAbf8GACX/CAA7/wIAu/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMX/DwB9/xIAcP8NAFP/BgAl/wYA + Jv8HACv/BgAn/wYAJf8GACX/CAAw/wwATP8NAFH/DQBR/w0AVP8RAGr/EABl/wgAMf8GACX/BgAt/wEA + r/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8FAK7/EQB1/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/DwBg/w0AUf8NAFH/DQBR/wkAN/8GACX/DRU61AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYBJ1YGACX/CQA4/w0AUf8NAFH/DQBR/w0AUf8NAFL/EABn/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8KAJT/AQDD/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/BABK/wYA + Jf8EAGj/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wIAu/8SAHL/EgBw/woAQP8GACX/CAAy/w0AUP8MAEr/CAA1/wYAJv8GACX/BwAt/wwA + S/8NAFH/DQBR/w4AVf8RAGv/DwBg/wcAKf8GACX/AgCB/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8HAKP/EgBx/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8RAGr/DQBS/w0AUf8NAFH/CQA3/wYA + Jf8NFTrUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgIoXwYA + Jf8JADf/DQBR/w0AUf8NAFH/DQBR/w4AWP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHH/CQCZ/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAtv8FADL/BgAl/wIAk/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BwCl/xIAcP8RAGv/BwAu/wYA + Jf8KAED/DQBR/w0AUf8NAFD/CgBA/wYAKf8GACX/CAAv/wwATv8NAFH/DQBR/w4AV/8RAGz/CQA2/wYA + Jf8DAHH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8MAIz/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAb/8OAFb/DQBR/w0AUf8JADf/BgAl/w0WO9EAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAihYBgAl/wkAN/8NAFH/DQBR/w0AUf8NAFH/EABj/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/woAlv8AAMT/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCc/wYA + Jv8GAC7/AQCw/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxf8MAI7/EgBw/w0AU/8GACX/BwAp/wwATf8NAFH/DQBR/w0AUf8NAFH/DABK/wcA + K/8GACX/CQA6/w0AUf8NAFH/DQBR/w4AWv8JADj/BgAl/wMAcf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wIA + vP8QAHr/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w8AXv8NAFH/DQBR/wgA + NP8GACX/EBxCsgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgF + K2IGACX/CAA0/w0AUf8NAFH/DQBR/w0AVP8RAGz/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8MAIz/AQDC/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DAHf/BgAl/wUARP8AAMH/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQDC/w8Aff8RAGz/CAAy/wYA + Jf8IADb/DQBR/w0AUf8OAFX/DwBb/w4AWP8NAFL/CgA+/wYAJf8HACn/DABM/w0AUf8NAFH/DQBR/wgA + M/8GACX/AwBx/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wYAp/8SAHL/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EABm/w0AUf8NAFH/CAA0/wYAJf8QHEKuAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQgtZgYAJf8IADT/DQBR/w0AUf8NAFH/DwBc/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DwB8/wIAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wQA + U/8GACX/BABm/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8DALb/EQBz/w0AVP8GACb/BgAm/wsAR/8NAFH/DgBY/xIAbv8SAHD/EgBv/xAA + aP8NAE//BwAq/wYAJf8JADv/DQBR/w0AUf8NAFH/CQA3/wYAJf8DAGf/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADE/wwAiv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8RAGz/DQBT/w0A + Uf8IADL/BgAl/w0XPZoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAKDDFtBgAl/wgAMf8NAFH/DQBR/w0AUf8QAGf/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cv8GAKr/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AALz/BQA4/wYAJf8CAIv/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wgAn/8RAG3/CAAx/wYA + Jf8HACv/DABM/w0ATv8LAEP/EQBt/w8Af/8MAIr/EgBw/xAAZf8JADv/BgAl/wcAKv8MAE7/DQBR/w0A + Uf8KADz/BgAl/wQAWP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC5/xAAd/8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8OAFb/DQBR/wcAMP8GACX/CxE5kAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQNXwGACX/CAAw/w0AUf8NAFH/DQBU/xIA + bv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DQCJ/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA + qP8GACn/BgAp/wEAqf8AAMb/AADG/wAAxv8AAMb/AADG/wUArv8EALH/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMP/DgCB/w8AXv8GACb/BgAl/wYAJf8GACj/BwAq/wYAKf8PAGH/DACN/wEA + wf8OAIP/EgBw/xAAYv8HACr/BgAl/woAQf8NAFH/DQBR/wsAQv8GACX/BQBG/wAAw/8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/CACd/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w8A + W/8NAFH/BwAt/wYAJf8LDz6OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAADBQ5fwYAJf8HAC3/DQBR/w0AUf8OAFr/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cv8FALD/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCG/wYAJv8FADP/AAC9/wAAxv8AAMb/AADG/wAA + xv8AAMT/DgCD/w0AiP8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAwv8QAHz/EQBo/wgA + Mf8HACn/BgAn/wYAJ/8HACn/CAAy/xAAZv8HAKL/AADG/wYAqP8SAHD/EgBw/wsAQv8GACX/CAAv/w0A + UP8NAFH/CwBI/wYAJv8FADT/AAC4/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAMH/DgCB/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DwBh/w0AUP8HACv/BgAl/w4ZToQAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMEzhrBgAl/wcALP8NAFH/DQBR/w8A + Yf8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DQCF/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8DAGP/BgAl/wUARv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8OAIH/DwB7/wEAwP8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wMAtv8MAIv/EABz/xAAaP8QAGP/EABi/xAAZf8RAGv/DwB+/wEA + v/8AAMb/AgC8/xEAdP8SAHD/DwBg/wYAJ/8GACb/CwBG/w0AUf8MAEv/BgAo/wYAJ/8BAKH/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAK3/EgBy/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8QAGX/DABO/wcAKv8GACX/EB5WZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAABMpTmgGACX/BwAr/w0AT/8NAFH/EABn/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8GAKf/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wUASf8GACX/BABb/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADE/w4Agf8SAHH/BwCk/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8BAL//AwC3/wQAsv8FAK7/BgCp/wUAq/8CALz/AADG/wAAxv8AAMX/DwB8/xIAcP8MAIf/CQA6/wYA + Jf8IADT/DQBR/w0AUP8HAC3/BgAl/wMAeP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xf8MAIv/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xEAaf8MAEz/BgAo/wYAJf8QHlhGAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzRaXQYAJf8HACr/DABO/w0A + U/8RAGz/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EAB3/wEAv/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/BQBH/wYAJf8EAEb/AADD/wAAxv8AAMb/AADG/wAAxv8AAMT/DgCE/xIAcP8QAHr/AwC2/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxP8QAHv/EgBw/wUArv8MAGT/BgAm/wYAKP8MAEz/DgBV/wsAQ/8GACX/BABL/wAA + xP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAtf8RAHT/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EQBr/wwATP8GACf/BgAl/xUsbz0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAZOmBKBgAl/wYAKP8MAEz/DQBU/xIAbv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8MAI3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DAHD/BgAl/wYAJ/8DAHT/AADA/wAA + xv8AAMb/AADG/wAAxv8DALX/CwCQ/xEAdP8PAH3/BACw/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BACy/xEAc/8SAHD/AgC8/wkA + lP8IADL/BgAl/woAQP8OAFn/DwBc/wYAJv8GAC3/AQCu/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADF/wsAkP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8RAG3/DABK/wYAJv8HAyr/Gz2LKAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxGbDoGACX/BgAm/wwA + Sv8OAFX/EgBv/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/wYAqf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAuP8FAEr/BgAl/wYAJv8EAFD/AQCr/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC5/woA + lP8QAHn/CgCT/wMAuP8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wMAtf8PAH3/EgBw/xAAef8BAMH/BACy/wwAUP8GACX/CQA4/w8AX/8RAGz/BwAs/wYA + Jf8DAH//AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC2/xEAdP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAb/8LAEj/BgAl/wkJNPkdQZMJAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAI1mAKAcDKP8GACb/CwBH/w4AVv8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8RAHf/AgC9/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAr/8FADD/BgAl/wYA + Jf8FADT/AgCE/wAAwv8AAMb/AADG/wAAxv8AAMb/AADF/wIAuv8JAJn/DwB9/wwAjf8EALL/AADE/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAwf8IAJ7/EAB4/w8Af/8KAJX/BAC0/wAA + xv8BAMT/BwCF/wYAJ/8IADT/EQBo/xIAcP8JADf/BgAl/wQAUv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMX/DACK/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/wsARP8GACX/Cgs36Bk4 + gwIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoZ44OCgsw+wYA + Jf8LAEP/DgBW/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/w0AiP8AAMX/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADB/wUAQ/8GACX/BgAl/wYAJf8GACb/BABY/wEAsP8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8BAL7/CQCc/w8Aff8NAIj/BQCs/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wEA + wP8HAKX/DwB//w4AgP8GAKf/AQDD/wAAxv8AAMb/AADG/wAAw/8DAGj/BgAl/wsARv8SAG//EgBv/wgA + Mv8GACX/BABM/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAK3/EgBx/xIA + cP8SAHD/EgBw/xIAcP8SAHD/CgBA/wYAJf8HAyq7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAB9OdAILDzXqBgAl/woAP/8OAFb/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/CACe/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwBm/wYA + Jf8GACb/BwAu/wYAJf8GACX/BQA3/wIAi/8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQC//wcA + o/8OAID/DgCD/wQAr/8AAMb/AADG/wIAvf8KAJT/DwB//w0Ah/8HAKP/AQDB/wAAxv8AAMb/AADG/wAA + xv8AAMX/AgB+/wYAKP8IADP/EQBp/xIAcP8PAF3/BgAn/wYAJf8DAHH/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wEAw/8PAH7/EgBw/xIAcP8SAHD/EgBw/xIAb/8KAD//BgAl/woM + ObMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJmCHAAcE + KcIGACX/CQA6/w4AVf8SAG//EgBw/xIAcP8SAHD/EgBw/xIAcf8EALL/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAIn/BgAl/wYAJf8LAET/CgA9/wcAKf8GACX/BgAo/wQA + Yf8BALT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAwv8FAK3/BQCv/wAAxv8AAMb/BQCv/wYA + p/8CALr/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAn/8GAC7/BwAs/xAAYv8SAHD/EAB3/wgA + PP8GACX/BQAz/wEArv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wkA + mv8SAHD/EgBw/xIAcP8SAHD/EgBv/wkANf8GACX/BwQsfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxsQYAJf8IADX/DgBV/xIAbv8SAHD/EgBw/xIA + cP8SAHD/EAB5/wEAv/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA + pP8GACX/BgAl/woAPf8NAFH/DABK/wgAMP8GACX/BgAl/wUAOv8CAJX/AADE/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8BALP/BQA7/wYAKP8OAFn/EgBw/wwAif8DAIz/BgAn/wYAJv8CAH7/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC1/xEAcv8SAHD/EgBw/xIAcP8RAGz/BwAu/wYA + Jf8JCzdyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAGACV0BgAl/wgAMP8NAFT/EgBu/xIAcP8SAHD/EgBw/xIAcP8NAIb/AADF/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC2/wYALP8GACX/CQA3/w0AUf8NAFH/DQBP/woA + Pf8GACf/BgAl/wYAKv8DAGj/AAC5/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC7/wQATf8GACb/DABJ/xIAb/8OAIT/AQC+/wQA + Tf8GACX/BQBB/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8BAMP/DwB//xIAcP8SAHD/EgBw/xAAZ/8HACn/BgEm/woLODcAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcEKXQGACX/BwAs/w0AU/8RAG3/EgBw/xIA + cP8SAHD/EgBw/woAmP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAL7/BQA5/wYAJf8IADL/DQBU/w8AYf8QAGP/EABj/w4AV/8JADb/BgAl/wYAJf8FAD//AQCc/wAA + xP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + wP8EAF3/BgAl/woAPP8SAG7/EAB5/wMAt/8BAJ3/BgAp/wYAJf8CAIj/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8KAJb/EgBw/xIAcP8SAHD/DwBg/wYA + Jf8GAij/Cg47LAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAADx1CWAYBJv8HACn/DQBQ/xEAbf8SAHD/EgBw/xIAcP8SAHD/BgCo/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAw/8FAEH/BgAl/wcALv8QAGf/EgBw/xIA + cP8SAHD/EgBw/xEAbP8MAEr/BgAo/wYAJf8GACz/AwBu/wAAvf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMP/AwBr/wYAJv8HAC7/EABn/xEAc/8FAKz/AADE/wQA + Xv8GACX/BQA3/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wUArf8SAHH/EgBw/xIAcP8OAFb/BgAl/wgGMPIWL3MXAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKDDEtBgEm/wYAJ/8MAE3/EQBs/xIA + cP8SAHD/EgBw/xEAc/8DALj/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wQAS/8GACX/BwAu/xEAbf8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8PAF3/CAAy/wYA + Jf8GACX/BQBG/wEAof8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADB/wMA + dP8GACj/BgAl/w0AUP8RAHT/BwCk/wAAxv8BALH/BQAw/wYAJf8DAHL/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgC+/xEAdv8SAHD/EgBw/wwA + Sv8GACX/CQgz4gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAA4aQCwHAyj+BgAl/wsASf8RAGz/EgBw/xIAcP8SAHD/EAB4/wEAwP8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BABV/wYAJf8HACv/EQBp/xIA + cf8MAIv/CgCT/w4AhP8RAHX/EgBw/xIAcP8RAGr/CgA8/wYAJf8GACX/BgAt/wMAeP8AAL//AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAv/8DAGj/BgAn/wYAJf8HACn/EABo/wcApP8AAMX/AADG/wIA + gv8GACX/BgAt/wEAqv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/DgCC/xIAcP8SAHD/CgA//wYAJf8MEkLHAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIE51FgkKL/EGACX/CgBC/xEA + a/8SAHD/EgBw/xIAcP8OAID/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8EAGH/BgAl/wcAKf8QAGT/DACN/wAAw/8AAMb/AADE/wMAuf8LAJH/EgBx/xIA + bv8JADj/BgAl/wYAJf8GACX/BgAm/wQATf8BAKj/AADG/wAAxv8AAMb/AADG/wAAxv8BAK7/BABS/wYA + Jv8GACX/BgAl/wgAM/8IAJn/AADF/wAAxv8AAMT/BABL/wYAJf8EAE3/AADD/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8KAJP/EgBw/xIA + cP8IADT/BgAl/wwTRJgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAACg0y4QYAJf8JADz/EQBq/xIAcP8SAHD/EgBw/wwAjP8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAa/8GACX/BgAn/w8A + ZP8EALT/AADG/wAAxv8AAMb/AADG/wAAxf8JAJn/EABp/wYAJv8GACX/BgAt/wQATP8GACf/BgAl/wYA + Lv8CAIL/AAC//wAAxv8AAMP/AgCS/wUAMv8GACX/BQBG/wQASf8GACX/BQBI/wAAw/8AAMb/AADG/wEA + qv8GACz/BgAl/wIAgf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wcApP8SAHD/EQBq/wcALf8GACX/DRZKYgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOFz3LBgAl/wgA + Nf8QAGj/EgBw/xIAcP8SAHD/CgCV/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AwB2/wYAJf8GACX/DQBo/wEAw/8AAMb/AADG/wAAxv8AAMb/AADG/wEA + w/8KAHP/BgAl/wYAJf8EAFz/AAC+/wIAff8GAC7/BgAl/wYAJ/8EAFT/AgCM/wQAYv8GACr/BgAo/wMA + Z/8AALr/AwBk/wYAJf8FAD3/AAC+/wAAxv8AAMb/AwB8/wYAJf8GACv/AQCw/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BACx/xIA + cP8QAGL/BgAn/wYAJf8LDj0YAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4YPZQGACX/BwAv/xAAZv8SAHD/EgBw/xIAcP8IAJ7/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAID/BgAl/wYA + Jf8KAGr/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAg/8GACX/BgAm/wIAkf8AAMb/AADG/wEA + q/8EAFv/BgAq/wYAJf8GACX/BgAl/wUAOP8BAJD/AADE/wAAxv8CAID/BgAl/wYAKv8BAKL/AADG/wAA + v/8FAEL/BgAl/wQAVv8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CALr/EQB0/w0AUv8GACX/BwIo5AwQPggAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEB9EcgYA + Jf8HACr/DwBh/xIAcP8SAHD/EgBw/wcApf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wIAi/8GACb/BgAl/wgAa/8AAMX/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AwB7/wYAJf8GACv/AAC0/wAAxv8AAMb/AADG/wAAw/8BAJ7/BABb/wQAS/8DAGr/AQCy/wAA + xv8AAMb/AADG/wEAsf8FADL/BgAl/wUAPP8DAG3/BABX/wYAJv8GACj/AQCY/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA + wP8QAHn/CwBF/wYAJf8HAijGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJEovBgAl/wYAJ/8OAFv/EgBw/xIAcP8SAHD/BgCp/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCV/wYA + J/8GACX/BgBt/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8DAHv/BgAl/wYALf8AAML/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMX/AADD/wAAxf8AAMb/AADG/wAAxv8AAMb/AADF/wEAjP8FAC7/BgAl/wYA + Jf8GACX/BgAm/wQAYv8AAML/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADE/w8Aff8JADj/BgAl/wgGLo4AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQ + NQ4HBCn+BgAl/w0AUf8SAHD/EgBw/xIAcP8FAK3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAJ//BgAo/wYAJf8DAGf/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wMAe/8GACX/BgAt/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wEAmv8EAE//BQBB/wQASv8DAHb/AADA/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMX/DQCA/wcALf8GACX/CQo2fQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHklwBwkJLuMGACX/CwBG/xIAcP8SAHD/EgBw/wUA + rf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA + qP8GACn/BgAl/wQAXf8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwB7/wYAJf8GACz/AAC9/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + w/8AAL//AADC/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8LAHz/BgAn/wYBJ/8NFkpBAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAACAcwwwYAJf8KAD7/EgBw/xIAcP8SAHD/BQCt/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCy/wYAKv8GACX/BABS/wAAxf8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8DAHP/BgAl/wYAKv8BALH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADF/wgAcv8GACX/CAUu+hYwdycAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBjCGBgAl/wgAMv8SAG7/EgBw/xIA + cP8FAK3/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8BALb/BgAw/wYAJf8EAFH/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAbf8GACX/BgAp/wEA + p/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMX/BQBg/wYAJf8KDDjgIU+rAgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAwTRWoGASb/BwAq/xEAaP8SAHD/EgBw/wYAp/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAuP8FADT/BgAl/wQAUf8AAMX/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/BABh/wYAJf8GACf/AQCf/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAw/8FAEX/BgAl/woMOZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEB1VNAcCKf4GACX/DwBd/xIA + cP8SAHD/BwCk/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AAC8/wUAOv8GACX/BABR/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8EAFz/BgAl/wYA + J/8HAIP/CACg/wgAoP8IAKD/CQCa/wkAmP8KAJP/CwCR/wsAkf8LAJH/CgCY/wkAm/8IAKD/BwCl/wYA + qv8GAKj/BgCo/wYAqP8GAKr/BACy/wIAuv8CALv/AgC7/wIAvv8BAML/AQDD/wAAxf8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCy/wYAMP8GACX/Cw8+WAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAbPYoPCQgz8QYAJf8NAE7/EgBw/xIAcP8IAJ7/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/AQDD/wEAwf8DALP/BQA8/wYAJf8EAFH/AADF/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wQAVP8GACX/BgAn/xAAYf8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHL/EQBz/xEA + c/8RAHP/EQB0/xAAd/8PAH3/DQCI/wsAkv8JAJz/BgCq/wMAuP8BAML/AADF/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8CAJT/BgAl/wcCKP4JCTUZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALDz7OBgAl/woA + Pf8SAHD/EgBw/wkAmP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADE/wEAwf8CALv/BACx/wYApv8JAJj/DACN/w4A + hP8PAHz/EQB0/xEAcP8IADP/BgAl/wQAR/8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/BABU/wYA + Jf8GACf/EABk/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBy/xAAef8NAIf/CQCa/wUArP8CALv/AQDD/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMAbP8GACX/CAUu3Bcy + eQYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgFLWsGACX/BwAu/xEAbP8SAHD/CQCY/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/wEAv/8FAKv/CQCc/wsA + j/8OAIX/EAB7/xEAdf8SAHL/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBv/wkANf8GACX/BQBE/wAA + xP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8EAEz/BgAl/wYAKP8QAGj/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBx/xEA + df8OAIL/CgCU/wQAsP8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMT/BQBE/wYAJf8LDz6uAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBNEPgYA + Jf8GACf/DwBh/xIAcP8KAJX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8BAMP/AwC1/wkAm/8OAIP/EQBz/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/CQA2/wYAJf8FAET/AADE/wAAxv8AAMb/AADG/wAAxv8AAMb/AADD/wUA + Sf8GACX/BgAo/xIBZ/8TAWv/EwFr/xMBa/8TAWv/EwFr/xMBa/8TAWv/EwFr/xMBa/8TAWz/EgFt/xIB + bf8SAW3/EgBu/xIAb/8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBx/w4Agf8HAKX/AQDB/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAsf8GAC3/BgAl/w8c + UW0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOGU8VCAUt+AYAJf8NAFH/EgBw/wsAkf8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAMP/BwCj/w4Agv8RAHP/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAb/8JADf/BgAl/wUA + RP8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMD/BQBA/wYAJf8IAST/IBYa/yAWGv8gFhr/IBYa/yAW + Gv8gFhr/IBYa/yAWGv8gFhr/IBYa/yAWHf8fFSD/HxUh/x8UI/8eEyf/HhIp/x0RLv8cDzb/Gw44/xoN + P/8ZCkf/GAhQ/xYGWf8UBGL/EwJp/xIBbv8SAG//EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcf8OAID/BQCr/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AgCO/wYAJv8HBCn8IE93JgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAICDLOBgAl/woAP/8SAHD/DACL/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwC5/w0A + hv8SAHH/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAW7/EwJq/xUF + Xv8XCFP/GQpJ/xoNPv8cEDP/HhMp/w4HI/8GACX/BQBE/wAAxP8AAMb/AADG/wAAxv8AAMb/AADG/wAA + vv8FADr/BgAl/wkDIv8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMa + Df8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoP/yIZEf8iGBP/IBYb/x4T + J/8cEDT/GgxC/xcIUf8UBGL/EwFs/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHH/CwCP/wIAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8EAF//BgAl/wgH + LMIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoOPIwGACX/CAAv/xEAbf8OAIT/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wIAuv8PAH3/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + cP8SAHD/EwFs/xUEYP8YCUz/HA81/x8UIv8iGBT/IhkQ/yMaDv8jGg3/IxoN/yMaDf8jGg3/Dwgd/wYA + Jf8FAET/AADE/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC7/wUAMf8GACX/FQ0n/1VELP9WRCz/VkQs/1ZE + LP9WRCz/VkQs/1ZELP9WRCz/VkQs/1ZELP9TQir/Tj4n/04+J/9LOyX/Rjci/0I0IP89MB3/OCwa/zYq + Gf8xJhb/LCIT/yceEP8mHA//JRsO/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoO/yIZEf8gFhz/HBAy/xgK + Sv8UBGH/EgFu/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/DgCA/wIAu/8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AAC8/wUAOv8GACX/CxA1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAESNfUwYBJv8GACf/EABh/w8AfP8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/DACL/xIA + cP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8TAWv/FwhS/xwPNP8gFR3/IhkR/yMaDf8jGg3/IxoN/yMa + Df8kGg3/JBsO/ygeEP8xJRX/Oi0b/0M1If8bEyX/BgAl/wUARv8AAMT/AADG/wAAxv8AAMb/AADG/wAA + xv8BALL/BgAr/wYAJf8jGS3/fWVF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf98ZET/e2RD/3pjQv94YUH/dl9A/2xXOv9mUjb/XUsx/1JB + Kv9FNiL/OSwa/yshEv8kGw7/IxoN/yMaDf8jGg3/IxoN/yIZEv8fFCP/Gg0//xUFXf8SAG//EgBw/xIA + cP8SAHD/EgBw/xIAcP8SAHD/DgCD/wEAvv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAKD/BgAo/wYB + Jv8OGT5DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaPIgMCAYv7wYAJf8MAE3/EAB4/wEA + wP8AAMb/AADG/wAAxv8AAMb/AADG/wUArf8SAHH/EgBw/xIAcP8SAHD/EgBw/xMCaf8XB1X/HBA0/yIY + Ff8jGg7/IxoN/yMaDf8kGw7/KB4Q/zQpGP9FNiL/VUQs/2VSNv9yXD7/emJC/3tkQ/99ZUT/fmZF/ysg + L/8GACX/BABW/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wEAo/8GACn/BgAl/zQqNv+Re1b/kXxW/5F8 + Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+PeVT/jHZS/4x2Uv+MdlH/iHJO/4Rt + Sv+Cakj/gWlI/4BoR/9/Z0b/fmZF/35mRf9+ZkX/fmZF/35mRf99ZUT/emND/29aPP9cSjD/SDkk/zYq + GP8oHhD/IxoN/yMaDf8jGg3/IxkP/x8UJP8ZC0b/FANj/xIAb/8SAHD/EgBw/xIAcP8SAHD/DACO/wAA + xf8AAMb/AADG/wAAxv8AAMb/AADG/wMAdP8GACX/CQku7hQuVBMAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAIBzGuBgAl/wkAOf8RAHP/AwC6/wAAxv8AAMb/AADG/wAAxv8AAMb/DACO/xIA + cP8SAHD/EgBw/xMBav8aDED/IBYb/yMaD/8jGg3/IxoN/yQbDf8wJRX/Sjol/2BNM/9yXD7/e2ND/35m + Rf9+ZkX/f2dF/4FqSP+Jck//kHpV/5mFXf+ij2X/MSg3/wYAJf8DAGb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AgCT/wYAJ/8GACX/ZFxg/+DVrP/e1Kr/18uc/8/Cjv/OwYz/0sWT/9vQo//d0qf/49mx/+Ta + sv/g1az/2s+j/9bKm//OwY7/zL+L/8y/i//LvYr/ybyI/8a3hf/BsoH/vK18/7eneP+yoXP/qphs/6OP + Zf+ZhV3/kHpV/4hxTv+AaEf/fmZF/35mRf9+ZkX/e2ND/3FcPf9bSTD/QTMf/ykfEP8jGg3/IxoN/yMa + Df8iGBX/HA82/xQDZf8SAHD/EgBw/xIAcP8RAHX/AgC7/wAAxv8AAMb/AADG/wAAxv8AAMT/BQBG/wYA + Jf8NFju6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkKNmgGACX/BwAq/xAA + aP8EALL/AADG/wAAxv8AAMb/AADG/wEAwf8PAH3/EgBw/xIAcP8VBVz/IBYc/yMaDf8jGg3/JRwO/zMn + F/9OPij/bFc6/3tjQ/9+ZkX/gGhH/4hxTv+WgVr/p5Rp/7OidP/BsoH/y72K/87Bjv/QxJD/1ciW/9XI + l/81Lj//BgAl/wMAbP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAID/BgAl/wYAJf98c2n/28+h/9rP + of/Zzp//1MiV/9HEj//bz6H/3tOn/97Tp//Zzp//1sqa/9PHlP/TxpP/0sWS/9DEj//Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/PwY3/zb+L/8q8iP++r37/tKR1/6iV + av+Wglv/hW9M/39nRv99ZUX/dmBA/19MMv9AMh//KiAR/yMaDf8jGg3/IRYa/xYGWP8SAHD/EgBw/xIA + cf8FAK3/AADG/wAAxv8AAMb/AADG/wEAqv8GACz/BgAl/xMpT2oAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAADRdLKAcDKv0GACX/DgBX/wYAp/8AAMb/AADG/wAAxv8AAMb/AgC9/xEA + df8SAHD/FANk/yEWGv8jGg3/JRwO/0EzH/9pVTj/emND/35mRf+AaEb/kXxW/6yabv++rn7/y76K/8/C + jf/SxZL/4NWq/+feuP/p4Lz/6+G+/+jeuf/l27T/3dKp/zIrQP8GACX/AwB0/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wMAav8GACX/BgAl/5GGbf/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0sWR/9PGk//KvIj/tKN1/5N9V/+AaEf/fmZF/4hy + Uv+FclX/TT0n/ycdD/8jGg3/IRcY/xQDYv8SAHD/EgBx/wYAqf8AAMb/AADG/wAAxv8AAMb/AwB7/wYA + Jf8IBiv8FSxSDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdRZkECgw52QYA + Jf8KAEH/CQCa/wAAxv8AAMb/AADG/wAAxv8CALz/EQBz/xIAcP8bDjv/IxoN/yYdD/9bSS//gWpJ/4Bo + SP9+ZkX/loFa/72tff/SxpT/3dKl/9TIlv/p4Lv/18ua/9LFkf/VyJb/08aT/9HFkf/Qw47/0MOO/9DD + jv/JvIr/KCE3/wYAJf8DAHr/AADG/wAAxv8AAMb/AADG/wAAxv8AAMX/BABT/wYAJf8NBij/p5x5/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/d0qX/8+zN/+Xbs//Qw4//0MOR/7urfP+Re1b/gGhH/7Cggv+qmHv/YE0z/ycdD/8jGg3/Gw44/xIA + cP8SAHH/BQCu/wAAxv8AAMb/AADG/wAAw/8EAEz/BgAl/woNMsIiWYABAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANFkuLBgAl/wcALf8LAIn/AADF/wAAxv8AAMb/AADG/wIA + vf8RAHX/EgBu/yAVIf8jGg3/QjQg/4BpSP+ciWr/f2dG/6CMY//OwY3/4des//rz2P/y6sv/1MeV/9fL + m//RxJD/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/8K2h/8eFzL/BgAl/wIAhf8AAMb/AADG/wAA + xv8AAMb/AADG/wAAwP8FAED/BgAl/xYPLf+9sYT/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/SxZL/08eU/9HFkf/z7M7/8OjH/8/C + kP+gjGP/gGhH/4p0VP+EbEz/RTYi/yMaDf8gFSD/EgFu/xEAc/8CALr/AADG/wAAxv8AAMb/AQCt/wYA + Lf8GACX/DBQ5dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwT + RBMGASf/BgAm/wsAa/8AAMX/AADG/wAAxv8AAMb/AQDA/xAAev8TAWv/IRYa/yMaDf9UQyv/gWlI/4Zv + Tv+HcE3/ybuI/+HXrP/999//6uG9/9PGk//Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/u6+D/xIMK/8GACX/AgCR/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC6/wUAMv8GACX/LSY5/8m8 + iv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/+DVq//++OD/5Nqy/8i6h/+EbUr/hm9P/72ukf9UQyz/IxoN/yEW + Gv8TAWv/DwB9/wEAwv8AAMb/AADG/wAAxv8CAH7/BgAl/wgHLPoYOmArAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBFBAAcELL0GACX/CQBK/wEAv/8AAMb/AADG/wAA + xv8AAMX/DgCE/xIBbv8gFSD/IxoN/08/KP+yoYT/gmtK/5WAWv/SxZL/+vPY/+nhvP/RxI//0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv+uo33/CwUo/wYAJf8BAJ//AADG/wAA + xv8AAMb/AADG/wAAxv8BAKv/BgAp/wYAJf8/NT3/rJtu/62cb/+tnG//rZxv/62cb/+tnG//rZxv/62c + b/+tnG//rZxv/62cb/+tnG//rZxv/62cb/+yoXP/tKN1/7Sjdf+0o3X/t6d4/72tff/Cs4H/x7iF/8y/ + i//OwYz/z8KN/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0cSP//Pr + zP/17tD/z8KO/454U/9+Zkb/g2xM/04+J/8jGg3/IBUh/xIAbv8LAI7/AADG/wAAxv8AAMb/AADD/wQA + Sv8GACX/DRQ6yCltlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAACxA/eAYAJv8HADH/AwCy/wAAxv8AAMb/AADG/wAAxv8KAJX/EgBw/x0RL/8jGg3/PzEe/7yt + kf+Qe1v/loFa/9jMnf/89dz/1cmY/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Pwo3/zL+K/8S2 + g/+9rn3/taV2/31vWv8IAib/BgAr/wEAq/8AAMb/AADG/wAAxv8AAMb/AADG/wIAjf8GACb/BgAl/0c4 + Nv9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/f2dG/4FqSP+DbEr/h3BN/496VP+ZhV3/p5Rp/7emeP/DtIL/zcCM/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/5964//v02v/Qw5D/jHZR/5J9Xv+Bakr/PTAd/yMa + Df8dEDH/EgBw/wcApf8AAMb/AADG/wAAxv8BAKf/BgAq/wYAJf8OGj9sAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATJmUlCAUu+gYAJv8CAJT/AADG/wAA + xv8AAMb/AADG/wUArv8SAHH/GQpI/yMaDf8tIhP/jHhb/52Jav+MdVH/08aW//v02v/VyZf/0MOO/9DD + jv/Qw47/zsGN/8O0gv+xoHL/oY1k/5J9V/+Gb0z/gmpI/35nRf9+ZkX/Szs3/wYAJf8FADL/AQC2/wAA + xv8AAMb/AADG/wAAxv8AAMb/AwBk/wYAJf8IAib/Y08+/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/4FpSP+IcU7/mIRc/6iVav+6qnr/y76K/9DDjv/Qw47/0MOO/9DD + jv/m3bb/+fLX/8q9i/+EbEr/o5By/3ljRP8rIRL/IxoO/xgKSv8RAHT/AgC7/wAAxv8AAMb/AADG/wMA + dP8GACX/BwUq/BUwVhwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAACFOqAEKDDq/BgAl/wQAY/8AAMX/AADG/wAAxv8AAMb/AQDB/w8AfP8UBGD/IhkR/yQb + Dv9kUTb/jnhY/4FpR//BsoH/6+K//9bKmf/Qw47/yLmH/6WSZ/+Nd1L/gmpI/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf84KzL/BgAl/wUAN/8AAL7/AADG/wAAxv8AAMb/AADG/wAAuf8FADf/BgAl/xgP + Kv97ZET/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/39nRv+KdFD/p5Rp/8e5hv/Qw47/0MOO/+rhvP/w6Mf/tqZ3/39nRv+EbUz/YU4z/yQb + Df8iGBP/FANj/w0Ahv8AAMX/AADG/wAAxv8AAML/BQBB/wYAJf8LDzXCIE92AQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgHMUgGACX/BQAz/wEA + tv8AAMb/AADG/wAAxv8AAMb/CQCZ/xIBbv8fFCP/IxoN/0Y3I/9+ZkX/fmZF/6KPZf/RxI//0MOO/8W3 + hf+MdlL/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fWVF/yMZLf8GACX/BQBC/wAA + wv8AAMb/AADG/wAAxv8AAMb/AwB5/wYAJf8GACX/RDU1/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/iHJO/7en + eP/Qw47/3tOn/9rOof+Xglv/fmZF/35mRf9DNSH/IxoN/x8TJv8SAG//BgCo/wAAxv8AAMb/AADG/wEA + n/8GACj/BgEm/xEkSWcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAADBNEDwcELPYGACX/AgCF/wAAxv8AAMb/AADG/wAAxv8DALn/EQB0/xoM + Qv8jGg3/KyES/3dgQf9+ZkX/hm9M/8q8if/Pwo7/nYph/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf93YEP/Ewso/wYAJf8FAD//AADC/wAAxv8AAMb/AADG/wEAnP8FADH/BgAl/xcP + Kf9xW0L/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/g2tJ/7qre//Qw47/u6x8/4BpR/9+ZkX/dF4//ykf + Ef8jGg3/GQtG/w4Agf8BAMP/AADG/wAAxv8AAMX/AwBl/wYAJf8JCS7rIVR7FQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQo1vgYA + Jf8EAEv/AADE/wAAxv8AAMb/AADG/wAAxf8MAI3/FANi/yIZEP8jGg3/W0kw/35mRf9+ZkX/qZdr/8W2 + hP+Ca0n/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/3ReQv8OByf/BgAl/wYA + L/8CAIT/AgCV/wIAlP8DAHD/BgAw/wYAJf8PCCf/ZFA+/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/jXdS/8u+iv+Uf1n/fmZF/35mRf9XRS3/IxoN/yIZEv8UA2b/BwCj/wAAxv8AAMb/AADG/wAA + uP8FADb/BgAl/woLMJIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIFpeBgEn/wYAKf8BAKX/AADG/wAAxv8AAMb/AADG/wQA + tP8RAHP/HhIr/yMaDf85LBr/fGRE/35mRf+AaEf/jHZR/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fWVF/zgrMv8HASX/BgAl/wYAJf8GACX/BgAl/wYAJf8GACX/EAkn/15L + PP9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/jnhT/39nRv9+ZkX/e2ND/zUp + GP8jGg3/HRAx/w8Ae/8BAL//AADG/wAAxv8AAMb/AgCJ/wYAJv8HAif/ECBGPwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABk7 + hw8IBi/dBgAl/wMAZ/8AAMX/AADG/wAAxv8AAMb/AADF/wwAjv8XB1P/IxoP/yYcD/9oVDj/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/emJE/0s7 + N/8ZECr/DQYn/woEJv8KBCb/Egoo/zIlMP9oVD//fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9lUTb/JRwO/yIZEP8WBlf/BwCh/wAAxv8AAMb/AADG/wAA + xf8EAE3/BgAl/wsRNtgcRWsHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkKMHQGACX/BQAz/wEAtP8AAMb/AADG/wAA + xv8AAMb/AwC4/xIBcP8fFCH/IxoN/0I0IP9+ZkX/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39n + Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/3tkRf9uWEH/aVRA/2pVQP92X0P/f2dG/39n + Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39n + Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39n + Rv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/f2dG/39nRv9/Z0b/fWVF/z4w + Hf8jGg3/HxMl/w8BfP8BAMH/AADG/wAAxv8AAMb/AQCl/wYAKv8GACX/EiRJdQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAESFHHggHLPgGACX/AgCA/wAAxv8AAMb/AADG/wAAxv8AAMb/CACd/xgKSv8jGg7/Jx4Q/29b + Pf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr + Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr + Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr + Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4Jr + Sf+Ca0n/gmtJ/4JrSf+Ca0n/gmtJ/4JrSf9sWDv/Jh0P/yMaDv8XCVD/BgCp/wAAxv8AAMb/AADG/wAA + xf8DAGf/BgAl/wgHLPQWM1kFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADRU6tgYAJf8FAD//AADA/wAA + xv8AAMb/AADG/wAAxv8BAMH/DwF8/yAWHv8jGg3/Rjgj/4VuS/+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv + TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv + TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv + TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4Zv + TP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hm9M/4ZvTP+Gb0z/hW5L/0I0 + IP8jGg3/HxQi/w4Bg/8BAMP/AADG/wAAxv8AAMb/AAC2/wUANf8GACX/CQgukQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAXNltABgEm/wYAJ/8CAI3/AADG/wAAxv8AAMb/AADG/wAAxv8EALD/GApM/yMa + Dv8mHA//b1w+/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz + T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz + T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz + T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/+Kc0//inNP/4pz + T/+Kc0//inNP/4pzT/+Kc0//inNP/4pzT/9pVzr/JRsO/yMaDv8XCVD/BACz/wAAxv8AAMb/AADG/wAA + xv8CAIH/BgAl/wgFKvwUKk84AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJCC2/BgAl/wUA + S/8AAMH/AADG/wAAxv8AAMb/AADG/wAAxf8MAoz/IBYb/yMaDf8+Mh//inVR/454U/+OeFP/jnhT/454 + U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454 + U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454 + U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454 + U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/jnhT/454U/+OeFP/lH9Y/495VP+OeFP/iHNP/zkt + G/8jGg3/IBYc/wsCjf8AAMX/AADG/wAAxv8AAMb/AADA/wUAQ/8GACX/DBM4wSdjiQMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8cQVgHAif/BgAo/wIAmP8AAMb/AADG/wAAxv8AAMb/AADG/wEA + vv8WClb/IxoO/yUcDv9tXD7/kXxW/6KPZf+hjmX/lH9Z/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8 + Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8 + Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8 + Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8Vv+RfFb/kXxW/5F8 + Vv+VgFr/ppNo/7ysfP+1pXb/kn1X/5F8Vv9lVDn/JBsO/yMaDv8WClT/AgC9/wAAxv8AAMb/AADG/wAA + xv8CAJH/BgAn/wYAJf8PG0BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHktxCgsQ + NtkGACX/BABS/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wYBpv8fFCX/IxoN/zgsG/+Qe1b/moZd/8Kz + gf/GuIX/tKR1/6OQZv+ZhV3/loFa/5WBWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WA + Wv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WA + Wv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WAWv+VgFr/lYBa/5WA + Wv+VgFr/lYBa/5WAWv+VgFr/lYFa/5iDXP+ij2X/tqZ3/8i6h//Qw47/yryI/52JYP+VgFr/i3hT/zEm + Fv8jGg3/HxQk/wcBof8AAMb/AADG/wAAxv8AAMb/AADD/wQAT/8GACX/CQkw1hMmTAQAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBI3UQYAJf8GACj/AQCZ/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADF/xAHdP8jGg//IxoN/2JTOP+ZhV3/oo9l/8q8if/Qw47/0MOO/8q8iP+9rn3/rZxv/6CN + Y/+ahl7/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mF + Xf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mF + Xf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/mYVd/5mFXf+ZhV3/moVe/6GOZP+zonT/xreF/8/C + jf/Qw47/0sWS/8/Cjf+unHD/mYVd/5iEXf9YSTH/IxoN/yMaDv8RB27/AADE/wAAxv8AAMb/AADG/wAA + xv8BAKD/BgAp/wYBJv8OF0t4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAULFIFCgwx3gYAJf8EAFD/AADC/wAAxv8AAMb/AADG/wAAxv8AAMb/AgC7/xsROf8jGg3/LyUW/416 + Vf+diWD/q5pt/83Ai//Qw47/0MOO/9DDjv/Qw47/zb+L/8S1g/+3p3j/rJpu/6ORZ/+fjGL/nYlh/52J + Yf+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52J + YP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlg/52JYP+diWD/nYlh/56L + Yv+ij2X/qJZr/7Khc//Cs4L/zcCL/9DDjv/Qw47/0MOO/9/Uqf/k2rH/wLB//56LYv+diWD/hHJP/yoh + Ev8jGg3/GxE1/wMAuP8AAMb/AADG/wAAxv8AAMb/AADD/wQAWv8GACX/CAQs1RYxdw8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAARIkdqBgEm/wYAKP8CAJf/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/CQSX/yEYF/8jGg3/U0Yu/5+MY/+hjmT/tKN1/8/Cjv/bz6H/0cSQ/9DD + jv/Qw47/0MOO/9DDjv/Qw47/zsGM/8m7iP/Cs4L/u6t7/7ald/+xoHL/rZtv/6iWav+mk2j/pJFm/6KP + Zf+ij2X/oY5k/6GNZP+hjWT/oY1k/6GNZP+hjWT/oY1k/6GNZP+hjWT/oY1k/6GNZP+hjWT/oY1k/6GN + ZP+hjWT/oY5k/6OQZv+mlGj/q5lt/7Wkdf+/sH//x7mG/82/i//Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/az6H/+vPZ/9rOov+olWr/oY1k/52KYf9GOiX/IxoN/yEYGv8IBJv/AADG/wAAxv8AAMb/AADG/wAA + xv8BAKf/BgAs/wYAJf8KDjt3AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAACNYfgkKCzDNBgAl/wUASP8AAL7/AADG/wAAxv8AAMb/AADG/wAAxv8AAMP/Egxl/yMa + Df8mHQ//gXFP/6SRZ/+kkmf/uqp7/93Spv/l27T/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MKO/87AjP/Lvor/ybyI/8m8iP/Iuof/x7mG/8a3hf/EtYP/w7SD/8Cx + gP/AsYD/wLGA/8CxgP/AsYD/wLGA/8CxgP/AsYD/wbKB/8S1g//Iuof/yr2J/87BjP/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/08aT//fv0//n3rj/t6d5/6SRZ/+kkWf/cWJD/yQb + Dv8jGg3/EQ1q/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wQAYf8GACX/CAYv8g4ZUBEAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0VOk0HAif/BgAn/wIA + if8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8CAbz/HBUv/yMaDf8+MyD/oZBm/6iWav+pl2z/wbKB/+PZ + sf/k2rL/0cSQ/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9XI + l//d0qX/2Myc/8W2hP+qmGz/qJZq/5mIYP8zKBj/IxoN/xwVMP8CAbz/AADG/wAAxv8AAMb/AADG/wAA + xv8BAKv/BgAs/wYAJf8NFUiSAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAHERpBwwUOc0GACX/BQA7/wAAu/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8KB5H/IhkT/yQbDf9hVDn/q5lt/6yabv+unG//xbaE/+XbtP/v58b/2Myd/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0sWS/9bKmf/l27P/+vTa//fw1P/NwI3/sJ9y/6yabv+pmGz/VUgw/yMa + Df8iGRT/CQeU/wAAxv8AAMb/AADG/wAAxv8AAMb/AADD/wQAXv8GACX/BwMq6hImZQ8AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFi9VOQcC + J/cGACb/AwB0/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wEAw/8XEU3/IxoN/yogEv+LfFf/sJ5x/7Ce + cf+xoHP/v7B//9jMn//o37n/4das/9HEkP/h163/6uG8/9vPof/Qw47/0MOO/9PHlP/Xy5v/3NGj/9/U + qP/h163/49mx/93Spv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/RxI//3NCj/+HXrf/Wypj/0MOO/+DWq//38NX//Pbd//33 + 3v/w6Mj/0MOV/7emd/+wnnH/sJ5x/4N0Uf8nHhD/IxoO/xQPWf8AAMP/AADG/wAAxv8AAMb/AADG/wAA + xv8BAKP/BgAs/wYAJv8JCzd8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACg0ymQYAJf8GADH/AQCp/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wQDsP8eFiX/IxoN/zkuHf+hkWf/tKN1/7Sjdf+0o3X/uKh4/8O0gv/KvIn/zcCM/9DE + kf/Tx5b/0cSS/87BjP/PwY3/1MeW/9bKmv/Wypr/1sqa/9bKmv/VyZr/0cSR/87BjP/OwYz/zsGM/87B + jP/OwYz/zsGM/87BjP/OwYz/zsGM/87BjP/OwYz/zsGM/87BjP/OwYz/zsGM/87BjP/OwYz/zsGM/8/C + jv/azqH/29Ck/9PGlf/OwYz/0cSS/9rOo//ZzaP/zsCU/7ytff+0o3X/tKN1/7OidP95a0v/MCYW/yMa + Df8dFS7/AwK3/wAAxv8AAMb/AADG/wAAxv8AAMb/AADE/wQAVv8GACX/CQkz7BIkYRMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAZOmAjCgsw7gYAJf8EAFX/AADD/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wkHlf8hGRf/IxoN/zwx + H/+WiGD/t6d4/7eneP+3p3j/t6d4/7ioef+5qXr/uap6/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qr + e/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qr + e/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6q3v/uqt7/7qre/+6qnv/ual5/7io + ef+3p3j/t6d4/7eneP+2pnf/kYJc/y8lFf8jGg3/IBgc/wcFnv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8CAJT/BgAo/wYAJf8KDTtbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOGj9bBgAl/wYAJ/8CAIr/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADF/w0Kf/8iGRT/IxoN/ysiE/9mWj3/iHtW/5mLY/+ll2v/qptv/66f + cv+xonT/tKR2/7eoeP+3qHj/uqp6/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7us + e/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7use/+7rHv/u6x7/7us + e/+7q3v/t6h5/7Okdf+zo3X/sqN1/6+fcv+tnnH/qptv/6WXa/+ajGP/jH5Z/19UOf8rIhP/IxoN/yEY + Gf8LCIz/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AAC+/wUARv8GACX/CAYv1woOPAEAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAABEkSQEKDTLBBgAl/wUANv8BALH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/wwJ + h/8gFx//IxoN/yMaDf8jGg3/JBoN/yQbDv8kGw7/KyIT/zEnF/82LBv/PDIf/zwyH/9COCP/Rjwn/1FH + L/9SRy//Ukcv/1JHL/9SRy//Ukcv/1JHL/9SRy//VEkw/19UOP9gVTn/YFU5/2BVOf9fVDn/VEkx/1JH + L/9SRy//Ukcv/1JHL/9SRy//SD0o/0Q6Jf9EOiX/RDol/0Q6Jf89MyD/NSsa/zQqGv8zKhn/LCMU/ykg + Ef8kGw7/JBsO/yQbDf8jGg3/IxoN/yMaDf8eFib/CQaX/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8CAH7/BgAm/wcCKf0OG1JVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABc0Wi0ICC3wBgAl/wQA + Wv8AAMH/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wUEq/8VEFX/HhYn/yEYGv8iGRP/IxoP/yMa + Dv8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMa + Df8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMa + Df8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDf8jGg3/IxoN/yMaDv8jGg//IhkT/yEYGP8eFyX/Eg1l/wMC + tP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCt/wUAM/8GACX/Cgs4sxo7iAMAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsQNVYHAif/BgAn/wIAg/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AwK1/wYEp/8IBpv/CQeV/wwJhv8OC3n/Dwt2/xENaf8SDWj/FA9b/xUP + V/8XEUv/FxFK/xcRSv8XEUr/FxFK/xcRSv8XEUr/FxFK/xcRSv8ZEz//GhM9/xoTPf8aEz3/GhM+/xcR + SP8XEUr/FxFK/xcRSv8XEUr/FxFK/xUQVf8VD1j/FQ9Y/xUPWP8VD1n/Eg1n/w8Ld/8OC3j/Dgt4/wwJ + gv8LCIr/CQaY/wgGnP8GBKb/BAOz/wAAxP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + w/8EAFr/BgAl/wcELPIKDDocAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHkhuAw4Z + PrMGACX/BQAw/wEAoP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8AAMT/AADE/wAAxP8AAMT/AADE/wAA + xP8AAMT/AADE/wEAw/8BAMP/AQDD/wEAw/8BAMP/AADE/wAAxP8AAMT/AADE/wAAxP8AAMT/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCK/wYAKP8GACb/DRVJgwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHkpwFwkJL98GACX/BQA9/wEAsv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEA + r/8FADf/BgAl/wkIM8cYNX4IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAEihPSwcFLPoGACX/BABM/wAAvP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMH/BABT/wYAJf8IBS33EB1VNwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiVKkBDxtSfgYBJ/8GACX/BABc/wAA + v/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADF/wMA + ev8GACb/BgAm/w4YTX4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAACw89lgYBJv8GACb/AwBo/wAAwf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAJn/BgAs/wYAJf8JCjW2AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQHlYECAcxmAYA + Jf8GACf/AwBv/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCq/wUA + OP8GACX/CQk05RMrbSMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBi8BCgs4twYAJf8GACf/AwBt/wAAwf8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAvP8FAEf/BgAl/wcDKvEOFkk1AAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAATJ2cSCgw5xwYAJf8GACf/AwBo/wAAwP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAL//AwBe/wYA + Jv8HAin6DhpPYwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJWQSCw49xgYAJf8GACb/BABd/wAA + uv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADA/wMAYf8GACb/BwMp/g0URmwAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAQIFoDCAYumQYBJv8GACX/BABI/wEAq/8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAvf8DAF//BgAl/wYB + J/0LDj1wESJeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQHlYEDBJCkwYB + J/8GACX/BQA2/wIAkv8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AALj/BABV/wYAJv8HAij+Cg48dAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBFCZgcELPcGACX/BgAp/wMAav8AAL3/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCn/wUARP8GACX/BwQr/AwT + RGoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAADxtSPAgHMN4GACX/BgAm/wQAS/8BAKP/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADD/wIAi/8FADT/BgAl/wcDKu8PGlBdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADhdLCwsQQKAHAyv9BgAl/wYA + Lv8DAGz/AQC1/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEAtP8EAGH/BgAo/wYAJf8JCjXkDxtSNAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAFS1xAQ0WSjwIBS7SBgEm/wYAJf8FADb/AgB7/wAAt/8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + vv8CAIb/BQA2/wYAJf8HAij9CAcxnA4bUhcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcy + ehEOGE2DCAcw9gYAJf8GACb/BQA4/wMAcf8BALD/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAwf8CAI3/BQBG/wYAJ/8GACX/CAcx8g4ZT2YZN4ECAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABxBkgANFkoOCw48mAgGL/cGACX/BgAl/wYA + K/8EAFb/AgCN/wAAtv8AAMP/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADD/wEAsf8CAID/BQBA/wYA + Jv8GACX/CAcs+QkLMYMSJmEaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAgTacBFSxvLAwRQYwIBS7nBgAm/wYAJf8GACb/BQA0/wQAU/8CAH//AQCm/wAA + wP8AAMT/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMT/AAC2/wIAj/8EAFb/BQAw/wYAJf8GACX/BwMo+wsPNKMWM1k0J2aMAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAOGE1YCw8+uwgGL/oGACb/BgAl/wYAJf8GACn/BQA0/wQATP8DAHD/AgCH/wIAm/8BAKj/AACy/wAA + tP8AALT/AAC0/wAAtP8BALD/AQCn/wIAl/8CAIL/AwBp/wUAR/8GAC7/BgAm/wYAJf8GASb/Cgwx7A8b + QZMRIkgdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYNn8AEiRhHQoNOl8JCDKsCAYv5wYB + JvEGACX/BgAl/wYAJf8GACX/BgAn/wYAKv8GAC3/BgAt/wYALf8GAC3/BgAt/wYALP8GACr/BgAm/wYA + Jf8GACX/BgAl/wYBJv8HAifvCQkutRAfRFwXNlsOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzNZFBImTGsPHUKnDBE33wkLMPsHAyj/BwIn/wYB + Jv8GASb/BgEm/wYBJv8GASb/BwEm/wcDKP8HBCn/CxA27gsQNcINFTuHFS9UVhxDaAoAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAgUXcFEyhNCgoNMgsOGT4TFCtQTRQrUE4UK1BOFCtQThQrUE4YNlxDHENpHQsS + NwsbQmgIK22VAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//////////////////4P/////////wAAAAf/////B////////w + AAAAAAB////4P//////wAAAAAAAAAD///B//////AAAAAAAAAAAD//8P////wAAAAAAAAAAAAA//h/// + /AAAAAAAAAAAAAAA/8P//+AAAAAAAAAAAAAAAB/h//4AAAAAAAAAAAAAAAAA8P/4AAAAAAAAAAAAAAAA + ADj/gAAAAAAAAAAAAAAAAAA4/gAAAAAAAAAAAAAAAAAAPPwAAAAAAAAAAAAAAAAAAD78AAAAAAAAAAAA + AAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAA + AAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wA + AAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAA + AD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAA + AAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAA + AAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wA + AAAAAAAAAAAAAAAAAD/8AAAAAAAAAAAAAAAAAAA//AAAAAAAAAAAAAAAAAAAP/wAAAAAAAAAAAAAAAAA + AD/8AAAAAAAAAAAAAAAAAAB//AAAAAAAAAAAAAAAAAAAf/wAAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAAA + AAAAAAB//gAAAAAAAAAAAAAAAAAAf/4AAAAAAAAAAAAAAAAAAH/+AAAAAAAAAAAAAAAAAAB//gAAAAAA + AAAAAAAAAAAA//4AAAAAAAAAAAAAAAAAAP/+AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAA//8A + AAAAAAAAAAAAAAAAAP//AAAAAAAAAAAAAAAAAAD//wAAAAAAAAAAAAAAAAAB//8AAAAAAAAAAAAAAAAA + Af//AAAAAAAAAAAAAAAAAAH//wAAAAAAAAAAAAAAAAAB//+AAAAAAAAAAAAAAAAAAf//gAAAAAAAAAAA + AAAAAAH//4AAAAAAAAAAAAAAAAAD//+AAAAAAAAAAAAAAAAAA///gAAAAAAAAAAAAAAAAAP//8AAAAAA + AAAAAAAAAAAD///AAAAAAAAAAAAAAAAAB///wAAAAAAAAAAAAAAAAAf//8AAAAAAAAAAAAAAAAAH///g + AAAAAAAAAAAAAAAAD///4AAAAAAAAAAAAAAAAA///+AAAAAAAAAAAAAAAAAP///gAAAAAAAAAAAAAAAA + D///8AAAAAAAAAAAAAAAAB////AAAAAAAAAAAAAAAAAf///wAAAAAAAAAAAAAAAAH///8AAAAAAAAAAA + AAAAAB////gAAAAAAAAAAAAAAAA////4AAAAAAAAAAAAAAAAP///+AAAAAAAAAAAAAAAAD////wAAAAA + AAAAAAAAAAB////8AAAAAAAAAAAAAAAAf////AAAAAAAAAAAAAAAAH////4AAAAAAAAAAAAAAAD////+ + AAAAAAAAAAAAAAAA/////wAAAAAAAAAAAAAAAf////8AAAAAAAAAAAAAAAH/////AAAAAAAAAAAAAAAB + /////4AAAAAAAAAAAAAAA/////+AAAAAAAAAAAAAAAP/////wAAAAAAAAAAAAAAH/////8AAAAAAAAAA + AAAAB//////gAAAAAAAAAAAAAAf/////4AAAAAAAAAAAAAAP/////+AAAAAAAAAAAAAAD//////wAAAA + AAAAAAAAAB//////8AAAAAAAAAAAAAAf//////gAAAAAAAAAAAAAP//////4AAAAAAAAAAAAAD////// + /AAAAAAAAAAAAAB///////wAAAAAAAAAAAAAf//////+AAAAAAAAAAAAAP///////wAAAAAAAAAAAAD/ + //////8AAAAAAAAAAAAB////////gAAAAAAAAAAAAf///////4AAAAAAAAAAAAP////////AAAAAAAAA + AAAD////////4AAAAAAAAAAAB////////+AAAAAAAAAAAA/////////wAAAAAAAAAAAP////////+AAA + AAAAAAAAH/////////gAAAAAAAAAAD/////////+AAAAAAAAAAB//////////gAAAAAAAAAAf/////// + //8AAAAAAAAAAP//////////gAAAAAAAAAH//////////8AAAAAAAAAD///////////gAAAAAAAAA/// + ////////8AAAAAAAAA////////////wAAAAAAAAf///////////+AAAAAAAAP////////////wAAAAAA + AH////////////+AAAAAAAD/////////////4AAAAAAB//////////////AAAAAAB//////////////8 + AAAAAA///////////////8AAAAB////////////////gAAAB/////////////////wAAD/////////// + ///////gAH//////////////////////////////KAAAAEAAAACAAAAAAQAgAAAAAAAAQAAAAAAAAAAA + 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 + LV0HAC7/EABi/xIAcP8SAHD/CgCX/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8EAF7/CAAz/woAQP8GACn/BABd/wAAuf8AAMb/AADG/wAAxf8CALr/AADG/wMA + uf8BAMP/AADG/wAAxv8AAMH/BQBM/w4AVv8MAH//BwAs/wIAif8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/DACM/xIAcP8SAG//BwAr/wgHMTwAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAHAic6BwAq/w8AYf8SAHD/EgBw/wYAqv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AwB2/wcALf8OAFb/DQBV/wkANv8FADf/AgCW/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/BABi/wsARv8NAIn/AwB0/wUARf8AAMT/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wYAp/8SAHD/EQBq/wYB + J/8KDDkZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADRc9IQYAJ/8PAF3/EgBw/xIAcf8CALv/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wIAhf8HACn/EQBt/xIA + cP8SAG//DQBP/wYAKv8DAGb/AAC9/wAAxv8AAMb/AADG/wAAxv8AAMX/AwBy/wgAMv8OAHz/AgC6/wUA + Nv8CAIv/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8CAL7/EgBy/w8AYP8HBCv0Fi9zBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQsURAHAyj7DgBY/xIA + cP8RAHb/AADF/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8CAJH/BgAn/xAAc/8GAKr/CACd/xAAef8NAFP/BgAl/wUAPP8BAJ7/AADG/wAAxv8AAMD/AwBo/wYA + Jv8KAFj/AgC9/wIAlv8FADH/AAC+/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/w8Aff8OAFX/CAcx1wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAACQgt6w0AUf8SAHD/DwCA/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AQCb/wYAJf8IAJH/AADG/wAAxv8CALr/CgBK/wUANf8DAGv/BgAq/wMA + b/8BAJ7/BQBG/wQAY/8FAD7/AwCB/wAAxv8EAF7/BABg/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8MAI3/DABI/wcELJ4AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJLsELAEj/EgBw/w0Aif8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wEApv8GACX/BQCY/wAAxv8AAMb/AADG/wQA + Uv8DAGX/AADG/wEApP8EAFL/BQBA/wIAkP8AAMb/BABi/wQAS/8CAJL/BgAt/wEAnv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/CQCa/wkA + OP8HAilsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBSqOCgA+/xIAcP8MAI3/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BALD/BgAm/wIA + mP8AAMb/AADG/wAAxv8EAFD/AwB4/wAAxv8AAMb/AADG/wAAxf8AAMb/AADG/wAAt/8EAE//BQA1/wMA + b/8AAMX/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wcAov8HACz/CQgyQwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQkwawgA + M/8SAHD/DACO/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AAC6/wYAJ/8CAI7/AADG/wAAxv8AAMb/BABO/wMAcf8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADF/wAAw/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8FAJ7/BwIo/hEgWxoAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAoMOTwHACr/EgBu/wwAjf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAv/8GACv/AgCL/wAAxv8AAMb/AADG/wUARv8EAGX/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AgCL/wgFLd0hT6sBAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASJGERBwMp+xAAY/8NAIn/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxf8BAL3/BgAw/wIAi/8AAMb/AADG/wAA + xv8FAD//CQBM/w0AiP8NAIf/DgCD/w4AgP8OAIL/DQCH/wwAjP8MAIz/DACN/woAlP8KAJf/CQCb/wcA + o/8FAK//AgC8/wAAxf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wMA + Z/8HAyqcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcELM4NAFL/DgCE/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMT/AwC1/wYAqP8JAJv/DACO/w4Agf8QAHj/EgBx/wcA + LP8CAIX/AADG/wAAxv8AAMX/BQA6/wsAR/8SAHD/EgBw/xIAcP8SAHD/EgBw/xIAcP8SAHD/EgBw/xIA + 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 + Of8AAMH/AADG/wQAtP8bDj3/PS8d/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf99ZUX/V0U7/zsu + M/8/MTT/ZlI//35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf87Lhz/Gg1C/wIAu/8AAMb/AwB4/wkJ + LtMcRWsCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAABEhRwcIBivrAgCR/wAAxv8AAMb/DAOJ/yMaEv9vWz3/hG1K/4RtSv+EbUr/hG1K/4Rt + Sv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4Rt + Sv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv+EbUr/hG1K/4RtSv9tWTz/IxkT/wsC + kP8AAMb/AADC/wUAOv8ICC1jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQswfwUASf8AAMX/AADG/wEAwP8aD0D/PTEe/4t1 + Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2 + Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/4x2Uf+MdlH/jHZR/414 + U/+KdFD/Oi4c/xkPQv8BAMH/AADG/wIAkv8IBSrvFS1SDwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgRhkHBCr1AQCd/wAA + xv8AAMb/BwOg/yIZFP9yYEH/qJVq/6iWa/+Yg1z/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+ + WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+WP+Tflj/k35Y/5N+ + WP+Xglv/qZdr/7+vf/+ei2L/bFs+/yIZE/8IA57/AADG/wAAxf8FAEv/CActhgAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAACQgtjQQATv8AAMX/AADG/wAAxv8UDF7/Nisa/5mGXv/FtoT/zsGN/8O0gv+yoXT/pZNo/52K + Yf+bh1//m4df/5uHX/+bh1//m4df/5uHX/+bh1//m4df/5uHX/+bh1//m4df/5uHX/+bh1//m4df/5uH + X/+bh1//n4xj/6qXbP+8rXz/zcCM/9nOn/+3pnf/lYFa/zIoF/8UDVr/AADG/wAAxv8BAKH/BgEo9A8a + UCIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAABImTB0HAyjzAgCZ/wAAxv8AAMb/AgG6/x4WJv9mWDz/p5Vq/9DD + lP/WyZj/0MOO/9DDjv/OwIz/x7mG/8Kzgf+9rn3/uKl5/7ald/+0o3X/s6J0/7Ggcv+wn3L/sJ9y/7Cf + cv+xn3L/s6N0/7ioef/AsX//yryI/8/Cjf/Qw47/0MOO/93Spf/d0qv/pJFn/15QNv8eFif/AgG7/wAA + xv8AAMb/BABW/wgGLp4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxiAUARP8AAMP/AADG/wAA + xv8KB5H/KiAT/5WFXf+xoHL/3NGn/9rPoP/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DD + jv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0sWS/+HWrP/e06j/s6J0/5CA + Wv8nHRH/CgeS/wAAxv8AAMb/AQCl/wYBKPoNFkooAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABYv + VQ4HAyjjAgCF/wAAxv8AAMb/AADF/xcRTP9EOSX/rZxw/7ald//PwpT/0saU/9zRpP/SxpP/0cWR/9jM + nf/b0KL/2s6g/8/Cjf/Pwo3/z8KN/8/Cjf/Pwo3/z8KN/8/Cjf/Pwo3/z8KN/9XJmP/ZzqD/1MiW/+rg + vv/e067/vKx9/6OSaP8/NSH/FhBT/wAAxf8AAMb/AADF/wQAUv8IBzCfAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAADBM5WwYAMv8BALb/AADG/wAAxv8CArn/HRUu/0g9KP+XiWH/q5xw/7Kj + df+2p3j/ual6/7ure/+7q3v/u6t7/7ure/+7q3v/u6t7/7ure/+7q3v/u6t7/7ure/+7q3v/u6t7/7ur + e/+6qnr/t6d4/7Wmd/+yonT/q5xw/5aIYP9DOST/HBQz/wIBvP8AAMb/AADG/wIAl/8GASj1Cg07FwAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEkSQAJCS63BABa/wAAxf8AAMb/AADG/wMC + tv8XEUv/IRkX/yMaD/8lHA//KyIT/zAmFv8zKhn/OjAe/zsxHv87MR7/OzEe/z40If9BNyP/QTcj/zsx + Hv87MR7/OzEe/zUrGv80Khn/MigY/ywiE/8pIBL/JRwO/yMaD/8hGRb/FhBT/wICuv8AAMb/AADG/wAA + wP8FAD//CQo1ggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBI3FggF + K+wCAIb/AADG/wAAxv8AAMb/AADG/wEBwv8DA7P/BQSq/wcFn/8JB5f/CgiQ/wwJiP8MCYf/DAmH/wwJ + h/8MCYT/DQqA/w0KgP8MCYf/DAmH/wwJh/8LCI7/CgiP/woHk/8HBZ//BwWi/wUErP8DA7P/AQHB/wAA + xv8AAMb/AADG/wAAxv8DAHT/BwQs3QoMOgcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAANFTtQBgEt/gEAoP8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BAKL/BgEr/QsOPEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIlSpAAoLN4UFADP/AQCs/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AALr/BQA8/wkJNI0AAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQHlYBCAUulAUA + Of8BAK7/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMP/BABT/wgF + LsMTK20JAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAABMnZwUJCTSnBQA3/wEAp/8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMX/AwBp/wcELNkOGk8ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAECBaAQgHMIwGAC7/AgCO/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMP/AwBm/wcDKtsLDz0cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACQs3ZgYC + KPcDAGb/AAC9/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8BALb/BABR/wgFLdIME0QbAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAALEUErBwQswwUAOf8CAIv/AADC/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxP8CAI7/BgA1/wgGL6UPG1INAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABcyegQKDTtiBwQs4wUAPf8DAH3/AQC0/wAA + xf8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADA/wIAkv8FAEj/BwQp3gsO + PFwZN4EBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUt + cAsJCjVdCAYwxAYCLP4FAEf/AwBw/wIAkv8BAKz/AAC6/wAAvf8AAL3/AAC5/wEAqf8CAI//AwBm/wUA + Nf8JCC3fCQkvbxYzWQ0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAABg2fwAMEkQfCAcwZQkILpwJCi/hBwMp/gYBKf8GACn/BgAp/wYB + Kf8IBSr7CAcs0gkJL5MLEDZEFzZbBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJNWYDAHP/AADG/wgAn/8gFSb/gW5R/83A + mf/SxZH/vKx8/6WSaP+VgFr/iXJP/xMLLv8AAMD/AADG/wQAYP9LOzf/fmZF/35mRf9+ZkX/fmZF/35m + Rf9+ZkX/fmZF/35nRf+AaEf/h3BN/5aCW/+pl2v/xreF/9nNnv/HuZT/eWVI/yAUJ/8HAKX/AAC//wYC + NfYQIUcNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkJNAwFAT/4AADD/wIA + vv8ZC0f/U0Iq/5+MY/+tm2//fmZF/35mRf9+ZkX/cVtC/wgCL/8AAL3/AQCf/xEJLv90XkL/fmZF/35m + Rf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/gmpI/7urff+ahl//UUEp/xgL + Tf8BAMH/AgCR/wgHLaUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAIBS+tAgCa/wAAxv8MA4n/MCQa/31mRf+DbEn/fmZF/35mRf9+ZkX/emNE/y8jMP8SCjD/IBYu/2VR + Pv9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/35mRf9+ZkX/fmZF/4Rt + Sv98ZUT/LiMa/woCkv8AAMb/BABU/wsQNkUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAALDjNDBABX/wAAxv8CAL3/GxA7/2JPNP+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4Nr + Sf+AaUn/gmtJ/4NrSf+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4NrSf+Da0n/g2tJ/4Nr + Sf+Da0n/g2tJ/4NrSf9gTjP/Gg8+/wEAwP8BAK//BwMs1BUuUwQAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYOF4DBwQtzQEAq/8AAMb/CQSW/zEmGv+KdVH/jnlU/413 + Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413Uv+Nd1L/jXdS/413 + Uv+Nd1L/jXdS/413Uv+OeFP/lYBa/4lzUP8wJRn/CQSX/wAAxv8DAG7/CgswbQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxWgQAYf8AAMb/AADF/xcP + T/9lVTn/taV2/7emd/+mlGn/moZe/5eDXP+Xg1v/l4Nb/5eDW/+Xg1v/l4Nb/5eDW/+Xg1v/l4Nb/5eD + W/+Xg1v/l4Nb/5eDW/+Xg1z/noph/7Cfcv/GuIb/rZxv/2BQNv8XD0z/AADE/wAAvP8GAjXuDRY/EAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEiVKBgcF + LtYBAKz/AADG/wQCsv8qISH/l4Ve/8q8jP/TxpT/zb+L/8S2hP+8rHz/t6d4/7Khc/+vnnH/rpxw/6yb + bv+smm7/rJpu/6yabv+unXD/tKN1/72uff/HuYb/zsGN/9fLm//Sxp7/kX9Z/ycfIf8EArL/AADG/wMA + fP8IBS2EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAsPNFQEAFj/AADF/wAAxv8NCn//UUUu/66dcP/Xy6D/18ub/9PHlP/RxI//0cSP/9LG + kv/SxZL/0MOO/9DDjv/Qw47/0MOO/9DDjv/Qw47/0MOO/9LFkf/Ux5T/4das/93Sqf+vnnH/TEAr/w0K + gP8AAMb/AAC8/wYBOPENFkoXAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAABo8YgEIBi29AgCX/wAAxv8BAMP/GRNF/3FkRf+1pHb/wrOD/8i6 + i//GuIb/yLqK/8u9j//Ju4v/xbaE/8W2hP/FtoT/xbaE/8W2hP/FtoT/xbeF/8u+j//Iuov/yr2R/7am + eP9nWz7/GBJJ/wAAxP8AAMb/AwB1/wgFLoEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKDjMnBgI/9QAAvP8AAMb/AgK6/xcR + TP80KiD/Qjgk/0tAKv9SRy//WE00/1tPNv9bTzb/XFA2/2BUOf9fUzj/W082/1tPNv9WSzL/VUox/05D + LP9JPin/Qjgk/zQqIP8WEVL/AgG8/wAAxv8BALD/BgMz5AsPPw8AAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgwxawQA + X/4AAMX/AADG/wAAxv8BAcH/AwK0/wUEqv8HBaL/CAaa/wkGl/8JBpf/CQeW/woHkv8KB5P/CQaX/wkG + l/8IBpz/CAad/wYEp/8FBKz/AwK1/wEBwP8AAMb/AADG/wAAxf8EAFT+CQgzVwAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAFS9VAwgHMK8DAHr/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wIAgv8IBS6sDxtSAQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsRQQwHAyzDAgCE/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AQCh/wcD + Md0MFUgUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAME0YUBwMtxgMAe/8AAMX/AADG/wAA + xv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAAxv8AAMb/AADG/wAA + xv8BAKr/BgI66AsOPCwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + 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 + AADAAwAA4AcAAPAPAAD4HwAA +</value> + </data> +</root>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj new file mode 100644 index 0000000..7149436 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Impostor.Patcher.WinForms.csproj @@ -0,0 +1,22 @@ +<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> + + <PropertyGroup> + <AssemblyName>Impostor</AssemblyName> + <ProjectGuid>{804CF172-0C87-4423-9688-BD97D549891E}</ProjectGuid> + <OutputType>WinExe</OutputType> + <TargetFramework>net472</TargetFramework> + <UseWindowsForms>true</UseWindowsForms> + <Copyright>Copyright © AeonLucid 2020</Copyright> + <ApplicationIcon>icon.ico</ApplicationIcon> + <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> + <GenerateResourceUsePreserializedResources>true</GenerateResourceUsePreserializedResources> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj" /> + <PackageReference Include="System.Resources.Extensions" Version="5.0.0" /> + <Reference Include="System.Runtime.InteropServices.RuntimeInformation" /> + <Reference Include="System.Windows.Forms" /> + </ItemGroup> + +</Project>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs new file mode 100644 index 0000000..7ca9035 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Program.cs @@ -0,0 +1,17 @@ +using System; +using System.Windows.Forms; +using Impostor.Patcher.WinForms.Forms; + +namespace Impostor.Patcher.WinForms +{ + internal static class Program + { + [STAThread] + private static void Main() + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new FrmMain()); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs new file mode 100644 index 0000000..bb5c4cf --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.Designer.cs @@ -0,0 +1,69 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Impostor.Patcher.WinForms.Properties +{ + /// <summary> + /// A strongly-typed resource class, for looking up localized strings, etc. + /// </summary> + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", + "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources + { + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", + "CA1811:AvoidUncalledPrivateCode")] + internal Resources() + { + } + + /// <summary> + /// Returns the cached ResourceManager instance used by this class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState + .Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager + { + get + { + if ((resourceMan == null)) + { + global::System.Resources.ResourceManager temp = + new global::System.Resources.ResourceManager("Impostor.Client.WinForms.Properties.Resources", + typeof(Resources).Assembly); + resourceMan = temp; + } + + return resourceMan; + } + } + + /// <summary> + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// </summary> + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState + .Advanced)] + internal static global::System.Globalization.CultureInfo Culture + { + get { return resourceCulture; } + set { resourceCulture = value; } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Resources.resx @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="utf-8"?> +<root> + <!-- + Microsoft ResX Schema + + Version 2.0 + + The primary goals of this format is to allow a simple XML format + that is mostly human readable. The generation and parsing of the + various data types are done through the TypeConverter classes + associated with the data types. + + Example: + + ... ado.net/XML headers & schema ... + <resheader name="resmimetype">text/microsoft-resx</resheader> + <resheader name="version">2.0</resheader> + <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> + <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> + <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> + <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> + <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> + <value>[base64 mime encoded serialized .NET Framework object]</value> + </data> + <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> + <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> + <comment>This is a comment</comment> + </data> + + There are any number of "resheader" rows that contain simple + name/value pairs. + + Each data row contains a name, and value. The row also contains a + type or mimetype. Type corresponds to a .NET class that support + text/value conversion through the TypeConverter architecture. + Classes that don't support this are serialized and stored with the + mimetype set. + + The mimetype is used for serialized objects, and tells the + ResXResourceReader how to depersist the object. This is currently not + extensible. For a given mimetype the value must be set accordingly: + + Note - application/x-microsoft.net.object.binary.base64 is the format + that the ResXResourceWriter will generate, however the reader can + read any of the formats listed below. + + mimetype: application/x-microsoft.net.object.binary.base64 + value : The object must be serialized with + : System.Serialization.Formatters.Binary.BinaryFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.soap.base64 + value : The object must be serialized with + : System.Runtime.Serialization.Formatters.Soap.SoapFormatter + : and then encoded with base64 encoding. + + mimetype: application/x-microsoft.net.object.bytearray.base64 + value : The object must be serialized into a byte array + : using a System.ComponentModel.TypeConverter + : and then encoded with base64 encoding. + --> + <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> + <xsd:element name="root" msdata:IsDataSet="true"> + <xsd:complexType> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="metadata"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" /> + <xsd:attribute name="type" type="xsd:string" /> + <xsd:attribute name="mimetype" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="assembly"> + <xsd:complexType> + <xsd:attribute name="alias" type="xsd:string" /> + <xsd:attribute name="name" type="xsd:string" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="data"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" /> + <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> + <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> + </xsd:complexType> + </xsd:element> + <xsd:element name="resheader"> + <xsd:complexType> + <xsd:sequence> + <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required" /> + </xsd:complexType> + </xsd:element> + </xsd:choice> + </xsd:complexType> + </xsd:element> + </xsd:schema> + <resheader name="resmimetype"> + <value>text/microsoft-resx</value> + </resheader> + <resheader name="version"> + <value>2.0</value> + </resheader> + <resheader name="reader"> + <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> + <resheader name="writer"> + <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> + </resheader> +</root>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs new file mode 100644 index 0000000..42356db --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// <auto-generated> +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// </auto-generated> +//------------------------------------------------------------------------------ + +namespace Impostor.Patcher.Properties +{ + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute( + "Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + private static Settings defaultInstance = + ((Settings) (global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get { return defaultInstance; } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings new file mode 100644 index 0000000..3964565 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/Properties/Settings.settings @@ -0,0 +1,7 @@ +<?xml version='1.0' encoding='utf-8'?> +<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)"> + <Profiles> + <Profile Name="(Default)" /> + </Profiles> + <Settings /> +</SettingsFile> diff --git a/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.ico b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.ico Binary files differnew file mode 100644 index 0000000..8cc46f3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Patcher/Impostor.Patcher.WinForms/icon.ico diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/App.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/App.razor new file mode 100644 index 0000000..38e8633 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/App.razor @@ -0,0 +1,10 @@ +<Router AppAssembly="@typeof(DebugPlugin).Assembly"> + <Found Context="routeData"> + <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> + </Found> + <NotFound> + <LayoutView Layout="@typeof(MainLayout)"> + <p>Sorry, there's nothing at this address.</p> + </LayoutView> + </NotFound> +</Router>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs new file mode 100644 index 0000000..8f57e89 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPlugin.cs @@ -0,0 +1,13 @@ +using Impostor.Api.Plugins; + +namespace Impostor.Plugins.Debugger +{ + [ImpostorPlugin( + package: "gg.impostor.debugger", + name: "Debugger", + author: "Gerard", + version: "1.0.0")] + public class DebugPlugin : PluginBase + { + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs new file mode 100644 index 0000000..cc36ce6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/DebugPluginStartup.cs @@ -0,0 +1,35 @@ +using Impostor.Api.Plugins; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Plugins.Debugger +{ + public class DebugPluginStartup : IPluginStartup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddRazorPages(); + services.AddServerSideBlazor(); + } + + public void ConfigureHost(IHostBuilder host) + { + host.ConfigureWebHostDefaults(webBuilder => + { + webBuilder.Configure(app => + { + app.UseStaticFiles(); + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapBlazorHub(); + endpoints.MapFallbackToPage("/_Host"); + }); + }); + }); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj b/Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj new file mode 100644 index 0000000..9518e48 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Impostor.Plugins.Debugger.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <OutputType>Library</OutputType> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" /> + </ItemGroup> + + <ItemGroup> + <Folder Include="Properties\" /> + </ItemGroup> + +</Project>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor new file mode 100644 index 0000000..1bfa478 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/Index.razor @@ -0,0 +1,69 @@ +@page "/" +@implements IDisposable +@using Impostor.Api.Events.Managers +@using Impostor.Api.Games.Managers +@using Impostor.Api.Events +@using Impostor.Api.Events.Player +@implements Impostor.Api.Events.IEventListener +@inject IEventManager EventManager +@inject IGameManager GameManager + +<div class="container"> + <h2>Games</h2> + @if (GameManager.Games.Any()) + { + <table class="table table-striped"> + <thead> + <tr> + <th>Code</th> + <th>Players</th> + </tr> + </thead> + <tbody> + @foreach (var game in GameManager.Games) + { + <tr> + <td>@game.Code</td> + <td> + <ul class="mb-0"> + @foreach (var player in game.Players) + { + <li>@player.Client.Name</li> + } + </ul> + </td> + </tr> + } + </tbody> + </table> + } + else + { + <div class="text-center"> + <i class="text-muted">There are no active games.</i> + </div> + } +</div> + +@code { + private IDisposable _disposable; + + [EventListener(typeof(IGameCreatedEvent))] + [EventListener(typeof(IGameDestroyedEvent))] + [EventListener(typeof(IGamePlayerJoinedEvent))] + [EventListener(typeof(IGamePlayerLeftEvent))] + public void OnGameCreated(IGameEvent e) + { + StateHasChanged(); + } + + protected override void OnInitialized() + { + _disposable = EventManager.RegisterListener(this, InvokeAsync); + } + + public void Dispose() + { + _disposable?.Dispose(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml new file mode 100644 index 0000000..eed3aaf --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Pages/_Host.cshtml @@ -0,0 +1,19 @@ +@page "/" +@namespace Impostor.Plugins.Debugger.Pages +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <title>Impostor Debugger</title> + <base href="~/"/> + <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> +</head> +<body> +<component type="typeof(App)" render-mode="Server"/> + +<script src="_framework/blazor.server.js"></script> +</body> +</html>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor new file mode 100644 index 0000000..07da9d6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/Shared/MainLayout.razor @@ -0,0 +1,5 @@ +@inherits LayoutComponentBase + +<div class="page"> + @Body +</div>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor b/Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor new file mode 100644 index 0000000..109ec4e --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Debugger/_Imports.razor @@ -0,0 +1,8 @@ +@using System.Net.Http +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Components.Forms +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.JSInterop +@using Impostor.Plugins.Debugger.Shared
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs new file mode 100644 index 0000000..dafba7c --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePlugin.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Impostor.Api.Plugins; +using Microsoft.Extensions.Logging; + +namespace Impostor.Plugins.Example +{ + [ImpostorPlugin( + package: "gg.impostor.example", + name: "Example", + author: "AeonLucid", + version: "1.0.0")] + public class ExamplePlugin : PluginBase + { + private readonly ILogger<ExamplePlugin> _logger; + + public ExamplePlugin(ILogger<ExamplePlugin> logger) + { + _logger = logger; + } + + public override ValueTask EnableAsync() + { + _logger.LogInformation("Example is being enabled."); + return default; + } + + public override ValueTask DisableAsync() + { + _logger.LogInformation("Example is being disabled."); + return default; + } + } +} diff --git a/Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs new file mode 100644 index 0000000..936f15e --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Example/ExamplePluginStartup.cs @@ -0,0 +1,22 @@ +using Impostor.Api.Events; +using Impostor.Api.Plugins; +using Impostor.Plugins.Example.Handlers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Impostor.Plugins.Example +{ + public class ExamplePluginStartup : IPluginStartup + { + public void ConfigureHost(IHostBuilder host) + { + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton<IEventListener, GameEventListener>(); + services.AddSingleton<IEventListener, PlayerEventListener>(); + services.AddSingleton<IEventListener, MeetingEventListener>(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs new file mode 100644 index 0000000..be2d0f3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/GameEventListener.cs @@ -0,0 +1,64 @@ +using System; +using Impostor.Api.Events; + +namespace Impostor.Plugins.Example.Handlers +{ + public class GameEventListener : IEventListener + { + [EventListener(EventPriority.Monitor)] + public void OnGame(IGameEvent e) + { + Console.WriteLine(e.GetType().Name + " triggered"); + } + + [EventListener] + public void OnGameCreated(IGameCreatedEvent e) + { + Console.WriteLine("Game > created"); + } + + [EventListener] + public void OnGameStarting(IGameStartingEvent e) + { + Console.WriteLine("Game > starting"); + } + + [EventListener] + public void OnGameStarted(IGameStartedEvent e) + { + Console.WriteLine("Game > started"); + + foreach (var player in e.Game.Players) + { + var info = player.Character.PlayerInfo; + + Console.WriteLine($"- {info.PlayerName} {info.IsImpostor}"); + } + } + + [EventListener] + public void OnGameEnded(IGameEndedEvent e) + { + Console.WriteLine("Game > ended"); + Console.WriteLine("- Reason: " + e.GameOverReason); + } + + [EventListener] + public void OnGameDestroyed(IGameDestroyedEvent e) + { + Console.WriteLine("Game > destroyed"); + } + + [EventListener] + public void OnPlayerJoined(IGamePlayerJoinedEvent e) + { + Console.WriteLine("Player joined a game."); + } + + [EventListener] + public void OnPlayerLeftGame(IGamePlayerLeftEvent e) + { + Console.WriteLine("Player left a game."); + } + } +} diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs new file mode 100644 index 0000000..847532c --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/MeetingEventListener.cs @@ -0,0 +1,21 @@ +using System; +using Impostor.Api.Events; +using Impostor.Api.Events.Meeting; + +namespace Impostor.Plugins.Example.Handlers +{ + public class MeetingEventListener : IEventListener + { + [EventListener] + public void OnMeetingStarted(IMeetingStartedEvent e) + { + Console.WriteLine("Meeting > started"); + } + + [EventListener] + public void OnMeetingEnded(IMeetingEndedEvent e) + { + Console.WriteLine("Meeting > ended"); + } + } +} diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs new file mode 100644 index 0000000..0190d3b --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Example/Handlers/PlayerEventListener.cs @@ -0,0 +1,100 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using Impostor.Api.Events; +using Impostor.Api.Events.Player; +using Impostor.Api.Innersloth.Customization; +using Microsoft.Extensions.Logging; + +namespace Impostor.Plugins.Example.Handlers +{ + public class PlayerEventListener : IEventListener + { + private static readonly Random Random = new Random(); + + private readonly ILogger<PlayerEventListener> _logger; + + public PlayerEventListener(ILogger<PlayerEventListener> logger) + { + _logger = logger; + } + + [EventListener] + public void OnPlayerSpawned(IPlayerSpawnedEvent e) + { + _logger.LogDebug(e.PlayerControl.PlayerInfo.PlayerName + " spawned"); + + // Need to make a local copy because it might be possible that + // the event gets changed after being handled. + var clientPlayer = e.ClientPlayer; + var playerControl = e.PlayerControl; + + /* + Task.Run(async () => + { + Console.WriteLine("Starting player task."); + + // Give the player time to load. + await Task.Delay(TimeSpan.FromSeconds(3)); + + while (clientPlayer.Client.Connection != null && + clientPlayer.Client.Connection.IsConnected) + { + // Modify player properties. + await playerControl.SetColorAsync((byte) Random.Next(1, 9)); + await playerControl.SetHatAsync((uint) Random.Next(1, 9)); + await playerControl.SetSkinAsync((uint) Random.Next(1, 9)); + await playerControl.SetPetAsync((uint) Random.Next(1, 9)); + + await Task.Delay(TimeSpan.FromMilliseconds(5000)); + } + + _logger.LogDebug("Stopping player task."); + }); + */ + } + + [EventListener] + public void OnPlayerDestroyed(IPlayerDestroyedEvent e) + { + _logger.LogDebug(e.PlayerControl.PlayerInfo.PlayerName + " destroyed"); + } + + [EventListener] + public async ValueTask OnPlayerChat(IPlayerChatEvent e) + { + _logger.LogDebug(e.PlayerControl.PlayerInfo.PlayerName + " said " + e.Message); + + if (e.Message == "test") + { + e.Game.Options.KillCooldown = 0; + e.Game.Options.NumImpostors = 2; + e.Game.Options.PlayerSpeedMod = 5; + + await e.Game.SyncSettingsAsync(); + } + + if (e.Message == "look") + { + await e.PlayerControl.SetColorAsync(ColorType.Pink); + await e.PlayerControl.SetHatAsync(HatType.Cheese); + await e.PlayerControl.SetSkinAsync(SkinType.Police); + await e.PlayerControl.SetPetAsync(PetType.Ufo); + } + + if (e.Message == "snap") + { + await e.PlayerControl.NetworkTransform.SnapToAsync(new Vector2(1, 1)); + } + + await e.PlayerControl.SetNameAsync(e.Message); + await e.PlayerControl.SendChatAsync(e.Message); + } + + [EventListener] + public void OnPlayerStartMeetingEvent(IPlayerStartMeetingEvent e) + { + _logger.LogDebug($"Player {e.PlayerControl.PlayerInfo.PlayerName} start meeting, reason: " + (e.Body==null ? "Emergency call button" : "Found the body of the player "+e.Body.PlayerInfo.PlayerName)); + } + } +} diff --git a/Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj b/Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj new file mode 100644 index 0000000..dd4724d --- /dev/null +++ b/Impostor-dev/src/Impostor.Plugins.Example/Impostor.Plugins.Example.csproj @@ -0,0 +1,11 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>netstandard2.1</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs b/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs new file mode 100644 index 0000000..f4807e7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/AntiCheatConfig.cs @@ -0,0 +1,9 @@ +namespace Impostor.Server.Config +{ + public class AntiCheatConfig + { + public const string Section = "AntiCheat"; + + public bool BanIpFromGame { get; set; } = true; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs b/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs new file mode 100644 index 0000000..630d1b4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/DebugConfig.cs @@ -0,0 +1,11 @@ +namespace Impostor.Server.Config +{ + public class DebugConfig + { + public const string Section = "Debug"; + + public bool GameRecorderEnabled { get; set; } + + public string GameRecorderPath { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs b/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs new file mode 100644 index 0000000..a86735f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/DisconnectMessages.cs @@ -0,0 +1,19 @@ +namespace Impostor.Server.Config +{ + public static class DisconnectMessages + { + public const string Error = "There was an internal server error. " + + "Check the server console for more information. " + + "Please report the issue on the AmongUsServer GitHub if it keeps happening."; + + public const string Destroyed = "The game you tried to join is being destroyed. " + + "Please create a new game."; + + public const string NotImplemented = "Game listing has not been implemented in Impostor yet for servers " + + "running in server redirection mode."; + + public const string UsernameLength = "Your username is too long, please make it shorter."; + + public const string UsernameIllegalCharacters = "Your username contains illegal characters, please remove them."; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs b/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs new file mode 100644 index 0000000..1c58433 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/ServerConfig.cs @@ -0,0 +1,30 @@ +using Impostor.Server.Utils; + +namespace Impostor.Server.Config +{ + internal class ServerConfig + { + private string? _resolvedPublicIp; + private string? _resolvedListenIp; + + public const string Section = "Server"; + + public string PublicIp { get; set; } = "127.0.0.1"; + + public ushort PublicPort { get; set; } = 22023; + + public string ListenIp { get; set; } = "127.0.0.1"; + + public ushort ListenPort { get; set; } = 22023; + + public string ResolvePublicIp() + { + return _resolvedPublicIp ??= IpUtils.ResolveIp(PublicIp); + } + + public string ResolveListenIp() + { + return _resolvedListenIp ??= IpUtils.ResolveIp(ListenIp); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs new file mode 100644 index 0000000..0ccfa0d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorConfig.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Config +{ + public class ServerRedirectorConfig + { + public const string Section = "ServerRedirector"; + + public bool Enabled { get; set; } + + public bool Master { get; set; } + + public NodeLocator Locator { get; set; } + + public List<ServerRedirectorNode> Nodes { get; set; } + + public class NodeLocator + { + public string Redis { get; set; } + + public string UdpMasterEndpoint { get; set; } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs new file mode 100644 index 0000000..d11b60f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Config/ServerRedirectorNode.cs @@ -0,0 +1,9 @@ +namespace Impostor.Server.Config +{ + public class ServerRedirectorNode + { + public string Ip { get; set; } + + public ushort Port { get; set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Constants.cs b/Impostor-dev/src/Impostor.Server/Constants.cs new file mode 100644 index 0000000..62d90b2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Constants.cs @@ -0,0 +1,8 @@ +namespace Impostor.Server +{ + internal static class Constants + { + public const int SpawnTimeout = 2500; + public const int ConnectionTimeout = 2500; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs b/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs new file mode 100644 index 0000000..190f7f3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/EventHandler.cs @@ -0,0 +1,24 @@ +using Impostor.Api.Events; +using Impostor.Server.Events.Register; + +namespace Impostor.Server.Events +{ + internal readonly struct EventHandler + { + public EventHandler(IEventListener o, IRegisteredEventListener listener) + { + Object = o; + Listener = listener; + } + + public IEventListener Object { get; } + + public IRegisteredEventListener Listener { get; } + + public void Deconstruct(out IEventListener o, out IRegisteredEventListener listener) + { + o = Object; + listener = Listener; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/EventManager.cs b/Impostor-dev/src/Impostor.Server/Events/EventManager.cs new file mode 100644 index 0000000..5625c4d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/EventManager.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events; +using Impostor.Api.Events.Managers; +using Impostor.Server.Events.Register; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Events +{ + internal class EventManager : IEventManager + { + private readonly ConcurrentDictionary<Type, TemporaryEventRegister> _temporaryEventListeners; + private readonly ConcurrentDictionary<Type, List<EventHandler>> _cachedEventHandlers; + private readonly ILogger<EventManager> _logger; + private readonly IServiceProvider _serviceProvider; + + public EventManager(ILogger<EventManager> logger, IServiceProvider serviceProvider) + { + _logger = logger; + _serviceProvider = serviceProvider; + _temporaryEventListeners = new ConcurrentDictionary<Type, TemporaryEventRegister>(); + _cachedEventHandlers = new ConcurrentDictionary<Type, List<EventHandler>>(); + } + + /// <inheritdoc /> + public IDisposable RegisterListener<TListener>(TListener listener, Func<Func<Task>, Task> invoker = null) + where TListener : IEventListener + { + if (listener == null) + { + throw new ArgumentNullException(nameof(listener)); + } + + var eventListeners = RegisteredEventListener.FromType(listener.GetType()); + var disposes = new IDisposable[eventListeners.Count]; + + foreach (var eventListener in eventListeners) + { + IRegisteredEventListener wrappedEventListener = new WrappedRegisteredEventListener(eventListener, listener); + + if (invoker != null) + { + wrappedEventListener = new InvokedRegisteredEventListener(wrappedEventListener, invoker); + } + + var register = _temporaryEventListeners.GetOrAdd( + wrappedEventListener.EventType, + _ => new TemporaryEventRegister()); + + register.Add(wrappedEventListener); + } + + if (eventListeners.Count > 0) + { + _cachedEventHandlers.TryRemove(typeof(TListener), out _); + } + + return new MultiDisposable(disposes); + } + + /// <inheritdoc /> + public bool IsRegistered<TEvent>() + where TEvent : IEvent + { + if (_cachedEventHandlers.TryGetValue(typeof(TEvent), out var handlers)) + { + return handlers.Count > 0; + } + + return GetHandlers<TEvent>().Any(); + } + + /// <inheritdoc /> + public async ValueTask CallAsync<T>(T @event) + where T : IEvent + { + try + { + if (!_cachedEventHandlers.TryGetValue(typeof(T), out var handlers)) + { + handlers = CacheEventHandlers<T>(); + } + + foreach (var (handler, eventListener) in handlers) + { + await eventListener.InvokeAsync(handler, @event, _serviceProvider); + } + } + catch (ImpostorCheatException) + { + throw; + } + catch (Exception e) + { + _logger.LogError(e, "Invocation of event {0} threw an exception.", @event.GetType().Name); + } + } + + private List<EventHandler> CacheEventHandlers<TEvent>() + where TEvent : IEvent + { + var handlers = GetHandlers<TEvent>() + .OrderByDescending(e => e.Listener.Priority) + .ToList(); + + _cachedEventHandlers[typeof(TEvent)] = handlers; + + return handlers; + } + + /// <summary> + /// Get all the event listeners for the given event type. + /// </summary> + /// <returns>The event listeners.</returns> + private IEnumerable<EventHandler> GetHandlers<TEvent>() + where TEvent : IEvent + { + var eventType = typeof(TEvent); + var interfaces = eventType.GetInterfaces(); + + foreach (var @interface in interfaces) + { + if (_temporaryEventListeners.TryGetValue(@interface, out var cb)) + { + foreach (var eventListener in cb.GetEventListeners()) + { + yield return new EventHandler(null, eventListener); + } + } + } + + foreach (var handler in _serviceProvider.GetServices<IEventListener>()) + { + if (handler is IManualEventListener manualEventListener && manualEventListener.CanExecute<TEvent>()) + { + yield return new EventHandler(handler, new ManualRegisteredEventListener(manualEventListener)); + continue; + } + + var events = RegisteredEventListener.FromType(handler.GetType()); + + foreach (var eventHandler in events) + { + if (eventHandler.EventType != typeof(TEvent) && !interfaces.Contains(eventHandler.EventType)) + { + continue; + } + + yield return new EventHandler(handler, eventHandler); + } + } + + if (_temporaryEventListeners.TryGetValue(eventType, out var cb2)) + { + foreach (var eventListener in cb2.GetEventListeners()) + { + yield return new EventHandler(null, eventListener); + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs new file mode 100644 index 0000000..3fb2368 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameAlterEvent.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameAlterEvent : IGameAlterEvent + { + public GameAlterEvent(IGame game, bool isPublic) + { + Game = game; + IsPublic = isPublic; + } + + public IGame Game { get; } + + public bool IsPublic { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs new file mode 100644 index 0000000..57e7a20 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameCreatedEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameCreatedEvent : IGameCreatedEvent + { + public GameCreatedEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs new file mode 100644 index 0000000..5ee1b11 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameDestroyedEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameDestroyedEvent : IGameDestroyedEvent + { + public GameDestroyedEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs new file mode 100644 index 0000000..4ec4dcf --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameEndedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; + +namespace Impostor.Server.Events +{ + public class GameEndedEvent : IGameEndedEvent + { + public GameEndedEvent(IGame game, GameOverReason gameOverReason) + { + Game = game; + GameOverReason = gameOverReason; + } + + public IGame Game { get; } + + public GameOverReason GameOverReason { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs new file mode 100644 index 0000000..d728c59 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerJoinedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; +using Impostor.Api.Net; + +namespace Impostor.Server.Events +{ + public class GamePlayerJoinedEvent : IGamePlayerJoinedEvent + { + public GamePlayerJoinedEvent(IGame game, IClientPlayer player) + { + Game = game; + Player = player; + } + + public IGame Game { get; } + + public IClientPlayer Player { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs new file mode 100644 index 0000000..d295103 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GamePlayerLeftEvent.cs @@ -0,0 +1,22 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; +using Impostor.Api.Net; + +namespace Impostor.Server.Events +{ + public class GamePlayerLeftEvent : IGamePlayerLeftEvent + { + public GamePlayerLeftEvent(IGame game, IClientPlayer player, bool isBan) + { + Game = game; + Player = player; + IsBan = isBan; + } + + public IGame Game { get; } + + public IClientPlayer Player { get; } + + public bool IsBan { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs new file mode 100644 index 0000000..d21f9ec --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartedEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameStartedEvent : IGameStartedEvent + { + public GameStartedEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs new file mode 100644 index 0000000..a0763e8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/GameStartingEvent.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Events; +using Impostor.Api.Games; + +namespace Impostor.Server.Events +{ + public class GameStartingEvent : IGameStartingEvent + { + public GameStartingEvent(IGame game) + { + Game = game; + } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs new file mode 100644 index 0000000..cf55f7d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingEndedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events.Meeting; +using Impostor.Api.Games; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Meeting +{ + public class MeetingEndedEvent : IMeetingEndedEvent + { + public MeetingEndedEvent(IGame game, IInnerMeetingHud meetingHud) + { + Game = game; + MeetingHud = meetingHud; + } + + public IGame Game { get; } + + public IInnerMeetingHud MeetingHud { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs new file mode 100644 index 0000000..aa689a3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Meeting/MeetingStartedEvent.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Events.Meeting; +using Impostor.Api.Games; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Meeting +{ + public class MeetingStartedEvent : IMeetingStartedEvent + { + public MeetingStartedEvent(IGame game, IInnerMeetingHud meetingHud) + { + Game = game; + MeetingHud = meetingHud; + } + + public IGame Game { get; } + + public IInnerMeetingHud MeetingHud { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs new file mode 100644 index 0000000..7b7eb22 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerChatEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerChatEvent : IPlayerChatEvent + { + public PlayerChatEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, string message) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Message = message; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public string Message { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs new file mode 100644 index 0000000..330135d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerCompletedTaskEvent.cs @@ -0,0 +1,27 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerCompletedTaskEvent : IPlayerCompletedTaskEvent + { + public PlayerCompletedTaskEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, ITaskInfo task) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Task = task; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public ITaskInfo Task { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs new file mode 100644 index 0000000..69a20c9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerDestroyedEvent.cs @@ -0,0 +1,23 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerDestroyedEvent : IPlayerDestroyedEvent + { + public PlayerDestroyedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs new file mode 100644 index 0000000..a8660da --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerExileEvent.cs @@ -0,0 +1,23 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerExileEvent : IPlayerExileEvent + { + public PlayerExileEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs new file mode 100644 index 0000000..31ac388 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMovementEvent.cs @@ -0,0 +1,24 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + // TODO: Finish and use event, needs to be pooled + public class PlayerMovementEvent : IPlayerEvent + { + public PlayerMovementEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs new file mode 100644 index 0000000..ca64c35 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerMurderEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerMurderEvent : IPlayerMurderEvent + { + public PlayerMurderEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl victim) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Victim = victim; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IInnerPlayerControl Victim { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs new file mode 100644 index 0000000..71c25d7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSetStartCounterEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerSetStartCounterEvent : IPlayerSetStartCounterEvent + { + public PlayerSetStartCounterEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, byte secondsLeft) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + SecondsLeft = secondsLeft; + } + + public byte SecondsLeft { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IGame Game { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs new file mode 100644 index 0000000..4b6fda1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerSpawnedEvent.cs @@ -0,0 +1,23 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerSpawnedEvent : IPlayerSpawnedEvent + { + public PlayerSpawnedEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs new file mode 100644 index 0000000..70cb0d8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerStartMeetingEvent.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerStartMeetingEvent : IPlayerStartMeetingEvent + { + public PlayerStartMeetingEvent(IGame game, IClientPlayer clientPlayer, IInnerPlayerControl playerControl, IInnerPlayerControl? body) + { + Game = game; + ClientPlayer = clientPlayer; + PlayerControl = playerControl; + Body = body; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public IInnerPlayerControl? Body { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs new file mode 100644 index 0000000..0798d40 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Game/Player/PlayerVentEvent.cs @@ -0,0 +1,30 @@ +using Impostor.Api.Events.Player; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Events.Player +{ + public class PlayerVentEvent : IPlayerVentEvent + { + public PlayerVentEvent(IGame game, IClientPlayer sender, IInnerPlayerControl innerPlayerPhysics, VentLocation ventId, bool ventEnter) + { + Game = game; + ClientPlayer = sender; + PlayerControl = innerPlayerPhysics; + VentId = ventId; + VentEnter = ventEnter; + } + + public IGame Game { get; } + + public IClientPlayer ClientPlayer { get; } + + public IInnerPlayerControl PlayerControl { get; } + + public VentLocation VentId { get; } + + public bool VentEnter { get; } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs b/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs new file mode 100644 index 0000000..b68f064 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/MultiDisposable.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Impostor.Server.Events +{ + /// <summary> + /// Disposes multiple <see cref="IDisposable"/>. + /// </summary> + internal class MultiDisposable : IDisposable + { + private readonly IEnumerable<IDisposable> _disposables; + + public MultiDisposable(IEnumerable<IDisposable> disposables) + { + _disposables = disposables; + } + + public void Dispose() + { + foreach (var disposable in _disposables) + { + disposable?.Dispose(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs new file mode 100644 index 0000000..479a3f6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/IRegisteredEventListener.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal interface IRegisteredEventListener + { + Type EventType { get; } + + EventPriority Priority { get; } + + ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs new file mode 100644 index 0000000..a21c3b1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/InvokedRegisteredEventListener.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal class InvokedRegisteredEventListener : IRegisteredEventListener + { + private readonly IRegisteredEventListener _innerObject; + private readonly Func<Func<Task>, Task> _invoker; + + public InvokedRegisteredEventListener(IRegisteredEventListener innerObject, Func<Func<Task>, Task> invoker) + { + _innerObject = innerObject; + _invoker = invoker; + } + + public Type EventType => _innerObject.EventType; + + public EventPriority Priority => _innerObject.Priority; + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + return new ValueTask(_invoker(() => _innerObject.InvokeAsync(eventHandler, @event, provider).AsTask())); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs new file mode 100644 index 0000000..e81e8f8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/ManualRegisteredEventListener.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal class ManualRegisteredEventListener : IRegisteredEventListener + { + public Type EventType { get; } = typeof(object); + + private readonly IManualEventListener _manualEventListener; + + public ManualRegisteredEventListener(IManualEventListener manualEventListener) + { + _manualEventListener = manualEventListener; + } + + public EventPriority Priority => _manualEventListener.Priority; + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + if (@event is IEvent typedEvent) + { + return _manualEventListener.Execute(typedEvent); + } + + return ValueTask.CompletedTask; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs new file mode 100644 index 0000000..120a45e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/RegisteredEventListener.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Impostor.Api.Events; +using Microsoft.Extensions.DependencyInjection; + +namespace Impostor.Server.Events.Register +{ + internal class RegisteredEventListener : IRegisteredEventListener + { + private static readonly PropertyInfo IsCancelledProperty = typeof(IEventCancelable).GetProperty(nameof(IEventCancelable.IsCancelled))!; + + private static readonly ConcurrentDictionary<Type, RegisteredEventListener[]> Instances = new ConcurrentDictionary<Type, RegisteredEventListener[]>(); + private readonly Func<object, object, IServiceProvider, ValueTask> _invoker; + private readonly Type _eventListenerType; + + public RegisteredEventListener(Type eventType, MethodInfo method, EventListenerAttribute attribute, Type eventListenerType) + { + EventType = eventType; + _eventListenerType = eventListenerType; + Priority = attribute.Priority; + IgnoreCancelled = attribute.IgnoreCancelled; + Method = method.GetFriendlyName(showParameters: false); + _invoker = CreateInvoker(method, attribute.IgnoreCancelled); + } + + public Type EventType { get; } + + public EventPriority Priority { get; } + + public int PriorityOrder { get; set; } + + public bool IgnoreCancelled { get; } + + public string Method { get; } + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + return _invoker(eventHandler, @event, provider); + } + + private Func<object, object, IServiceProvider, ValueTask> CreateInvoker(MethodInfo method, bool ignoreCancelled) + { + var instance = Expression.Parameter(typeof(object), "instance"); + var eventParameter = Expression.Parameter(typeof(object), "event"); + var provider = Expression.Parameter(typeof(IServiceProvider), "provider"); + var @event = Expression.Convert(eventParameter, EventType); + + var getRequiredService = typeof(ServiceProviderServiceExtensions) + .GetMethod("GetRequiredService", new[] { typeof(IServiceProvider) }); + + if (getRequiredService == null) + { + throw new InvalidOperationException("The method GetRequiredService could not be found."); + } + + var methodArguments = method.GetParameters(); + var arguments = new Expression[methodArguments.Length]; + + for (var i = 0; i < methodArguments.Length; i++) + { + var methodArgument = methodArguments[i]; + + if (typeof(IEvent).IsAssignableFrom(methodArgument.ParameterType) + && methodArgument.ParameterType.IsAssignableFrom(EventType)) + { + arguments[i] = @event; + } + else + { + arguments[i] = Expression.Call( + getRequiredService.MakeGenericMethod(methodArgument.ParameterType), + provider); + } + } + + var returnTarget = Expression.Label(typeof(ValueTask)); + + Expression invoke = Expression.Call(Expression.Convert(instance, _eventListenerType), method, arguments); + + if (method.ReturnType == typeof(void)) + { + if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType)) + { + invoke = Expression.Block( + Expression.IfThenElse( + Expression.Property(@event, IsCancelledProperty), + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))), + Expression.Block( + invoke, + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))))), + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); + } + else + { + invoke = Expression.Block( + invoke, + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); + } + } + else if (method.ReturnType == typeof(ValueTask)) + { + if (!ignoreCancelled && typeof(IEventCancelable).IsAssignableFrom(EventType)) + { + invoke = Expression.Block( + Expression.IfThenElse( + Expression.Property(@event, IsCancelledProperty), + Expression.Return(returnTarget, Expression.Default(typeof(ValueTask))), + Expression.Return(returnTarget, invoke)), + Expression.Label(returnTarget, Expression.Default(typeof(ValueTask)))); + } + } + else + { + throw new InvalidOperationException($"The method {method.GetFriendlyName()} must return void or ValueTask."); + } + + return Expression.Lambda<Func<object, object, IServiceProvider, ValueTask>>(invoke, instance, eventParameter, provider) + .Compile(); + } + + public static IReadOnlyList<RegisteredEventListener> FromType(Type type) + { + return Instances.GetOrAdd(type, t => + { + return t.GetMethods() + .Where(m => !m.IsStatic && m.GetCustomAttributes(typeof(EventListenerAttribute), false).Any()) + .SelectMany(m => FromMethod(t, m)) + .ToArray(); + }); + } + + public static IEnumerable<RegisteredEventListener> FromMethod(Type listenerType, MethodInfo methodType) + { + // Get the return type. + var returnType = methodType.ReturnType; + + if (returnType != typeof(void) && returnType != typeof(ValueTask)) + { + throw new InvalidOperationException($"The method {methodType.GetFriendlyName()} does not return void or ValueTask."); + } + + // Register the event. + foreach (var attribute in methodType.GetCustomAttributes<EventListenerAttribute>(false)) + { + var eventType = attribute.Event; + + if (eventType == null) + { + if (methodType.GetParameters().Length == 0 || !typeof(IEvent).IsAssignableFrom(methodType.GetParameters()[0].ParameterType)) + { + throw new InvalidOperationException($"The first parameter of the method {methodType.GetFriendlyName()} should be the type {nameof(IEvent)}."); + } + + eventType = methodType.GetParameters()[0].ParameterType; + } + + yield return new RegisteredEventListener(eventType, methodType, attribute, listenerType); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs b/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs new file mode 100644 index 0000000..1446ad1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/TemporaryEventRegister.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Impostor.Server.Events.Register +{ + internal class TemporaryEventRegister + { + private readonly ConcurrentDictionary<int, IRegisteredEventListener> _callbacks; + private int _idLast; + + public TemporaryEventRegister() + { + _callbacks = new ConcurrentDictionary<int, IRegisteredEventListener>(); + } + + public IEnumerable<IRegisteredEventListener> GetEventListeners() + { + return _callbacks.Select(i => i.Value); + } + + public IDisposable Add(IRegisteredEventListener callback) + { + var id = Interlocked.Increment(ref _idLast); + + if (!_callbacks.TryAdd(id, callback)) + { + Debug.Fail("Failed to register the event listener"); + } + + return new UnregisterEvent(this, id); + } + + private void Remove(int id) + { + _callbacks.TryRemove(id, out _); + } + + private class UnregisterEvent : IDisposable + { + private readonly TemporaryEventRegister _register; + private readonly int _id; + + public UnregisterEvent(TemporaryEventRegister register, int id) + { + _register = register; + _id = id; + } + + public void Dispose() + { + _register.Remove(_id); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs b/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs new file mode 100644 index 0000000..dd668c5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Events/Register/WrappedRegisteredEventListener.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api.Events; + +namespace Impostor.Server.Events.Register +{ + internal class WrappedRegisteredEventListener : IRegisteredEventListener + { + private readonly IRegisteredEventListener _innerObject; + private readonly object _object; + + public WrappedRegisteredEventListener(IRegisteredEventListener innerObject, object o) + { + _innerObject = innerObject; + _object = o; + } + + public Type EventType => _innerObject.EventType; + + public EventPriority Priority => _innerObject.Priority; + + public ValueTask InvokeAsync(object eventHandler, object @event, IServiceProvider provider) + { + return _innerObject.InvokeAsync(_object, @event, provider); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs new file mode 100644 index 0000000..5f25e89 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Extensions/MessageReaderExtensions.cs @@ -0,0 +1,15 @@ +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.Inner; +using Impostor.Server.Net.State; + +namespace Impostor.Server +{ + internal static class MessageReaderExtensions + { + public static T ReadNetObject<T>(this IMessageReader reader, Game game) + where T : InnerNetObject + { + return game.FindObjectByNetId<T>(reader.ReadPackedUInt32()); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs new file mode 100644 index 0000000..370bca6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Extensions/NodeLocatorExtensions.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; +using Impostor.Server.Net.Redirector; + +namespace Impostor.Server +{ + public static class NodeLocatorExtensions + { + public static async ValueTask<bool> ExistsAsync(this INodeLocator nodeLocator, string gameCode) + { + return await nodeLocator.FindAsync(gameCode) != null; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs b/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..55d42fb --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Extensions/TypeExtensions.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; + +namespace Impostor.Server +{ + internal static class TypeExtensions + { + /// <summary> + /// Get the friendly name for the type. + /// </summary> + /// <param name="type">The type.</param> + /// <returns>The friendly name.</returns> + [SuppressMessage("ReSharper", "SA1503", Justification = "Readability")] + public static string GetFriendlyName(this Type type) + { + if (type == null) + return "null"; + if (type == typeof(int)) + return "int"; + if (type == typeof(short)) + return "short"; + if (type == typeof(byte)) + return "byte"; + if (type == typeof(bool)) + return "bool"; + if (type == typeof(long)) + return "long"; + if (type == typeof(float)) + return "float"; + if (type == typeof(double)) + return "double"; + if (type == typeof(decimal)) + return "decimal"; + if (type == typeof(string)) + return "string"; + if (type.IsGenericType) + return type.Name.Split('`')[0] + "<" + string.Join(", ", type.GetGenericArguments().Select(GetFriendlyName).ToArray()) + ">"; + return type.Name; + } + + /// <summary> + /// Get the friendly name for the method. + /// </summary> + /// <param name="method">The method.</param> + /// <param name="showParameters">True if the parameters should be included in the name.</param> + /// <returns>Friendly name of the method</returns> + public static string GetFriendlyName(this MethodBase method, bool showParameters = true) + { + var str = method.Name; + + if (method.DeclaringType != null) + { + str = method.DeclaringType.GetFriendlyName() + '.' + str; + } + + if (showParameters) + { + var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.GetFriendlyName())); + str += $"({parameters})"; + } + + return str; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj new file mode 100644 index 0000000..ad5a6db --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj @@ -0,0 +1,58 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + <RuntimeIdentifiers>win-x64;linux-x64;linux-arm;linux-arm64;osx-x64</RuntimeIdentifiers> + <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> + <ApplicationIcon>icon.ico</ApplicationIcon> + <CodeAnalysisRuleSet>ProjectRules.ruleset</CodeAnalysisRuleSet> + <Nullable>enable</Nullable> + <SelfContained>false</SelfContained> + </PropertyGroup> + + <PropertyGroup> + <AssemblyName>Impostor.Server</AssemblyName> + <AssemblyTitle>Impostor.Server</AssemblyTitle> + <Product>Impostor.Server</Product> + <Copyright>Copyright © AeonLucid 2020</Copyright> + <Version>1.0.0</Version> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Api\Impostor.Api.csproj" /> + <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" /> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> + <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="5.0.0" /> + <PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" /> + <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> + <PackageReference Include="StyleCop.Analyzers" Version="1.1.118"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <Content Include="config.json"> + <CopyToPublishDirectory>Always</CopyToPublishDirectory> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + </Content> + <Content Include="config.*.json"> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + </Content> + <Content Include="config-full.json"> + <CopyToPublishDirectory>Never</CopyToPublishDirectory> + <CopyToOutputDirectory>Never</CopyToOutputDirectory> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + </Content> + </ItemGroup> + +</Project>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings new file mode 100644 index 0000000..5c07b47 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Impostor.Server.csproj.DotSettings @@ -0,0 +1,3 @@ +<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events_005Cgame/@EntryIndexedValue">True</s:Boolean> + <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=extensions/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Client.cs b/Impostor-dev/src/Impostor.Server/Net/Client.cs new file mode 100644 index 0000000..87a1bb4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Client.cs @@ -0,0 +1,338 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Config; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net +{ + internal class Client : ClientBase + { + private readonly ILogger<Client> _logger; + private readonly AntiCheatConfig _antiCheatConfig; + private readonly ClientManager _clientManager; + private readonly GameManager _gameManager; + + public Client(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, IHazelConnection connection) + : base(name, connection) + { + _logger = logger; + _antiCheatConfig = antiCheatOptions.Value; + _clientManager = clientManager; + _gameManager = gameManager; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + var flag = reader.Tag; + + _logger.LogTrace("[{0}] Server got {1}.", Id, flag); + + switch (flag) + { + case MessageFlags.HostGame: + { + // Read game settings. + var gameInfo = Message00HostGameC2S.Deserialize(reader); + + // Create game. + var game = await _gameManager.CreateAsync(gameInfo); + + // Code in the packet below will be used in JoinGame. + using (var writer = MessageWriter.Get(MessageType.Reliable)) + { + Message00HostGameS2C.Serialize(writer, game.Code); + await Connection.SendAsync(writer); + } + + break; + } + + case MessageFlags.JoinGame: + { + Message01JoinGameC2S.Deserialize( + reader, + out var gameCode, + out _); + + var game = _gameManager.Find(gameCode); + if (game == null) + { + await DisconnectAsync(DisconnectReason.GameMissing); + return; + } + + var result = await game.AddClientAsync(this); + + switch (result.Error) + { + case GameJoinError.None: + break; + case GameJoinError.InvalidClient: + await DisconnectAsync(DisconnectReason.Custom, "Client is in an invalid state."); + break; + case GameJoinError.Banned: + await DisconnectAsync(DisconnectReason.Banned); + break; + case GameJoinError.GameFull: + await DisconnectAsync(DisconnectReason.GameFull); + break; + case GameJoinError.InvalidLimbo: + await DisconnectAsync(DisconnectReason.Custom, "Invalid limbo state while joining."); + break; + case GameJoinError.GameStarted: + await DisconnectAsync(DisconnectReason.GameStarted); + break; + case GameJoinError.GameDestroyed: + await DisconnectAsync(DisconnectReason.Custom, DisconnectMessages.Destroyed); + break; + case GameJoinError.Custom: + await DisconnectAsync(DisconnectReason.Custom, result.Message); + break; + default: + await DisconnectAsync(DisconnectReason.Custom, "Unknown error."); + break; + } + + break; + } + + case MessageFlags.StartGame: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + await Player.Game.HandleStartGame(reader); + break; + } + + // No idea how this flag is triggered. + case MessageFlags.RemoveGame: + break; + + case MessageFlags.RemovePlayer: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message04RemovePlayerC2S.Deserialize( + reader, + out var playerId, + out var reason); + + await Player.Game.HandleRemovePlayer(playerId, (DisconnectReason)reason); + break; + } + + case MessageFlags.GameData: + case MessageFlags.GameDataTo: + { + if (!IsPacketAllowed(reader, false)) + { + return; + } + + var toPlayer = flag == MessageFlags.GameDataTo; + + // Handle packet. + using var readerCopy = reader.Copy(); + + // TODO: Return value, either a bool (to cancel) or a writer (to cancel (null) or modify/overwrite). + try + { + var verified = await Player.Game.HandleGameDataAsync(readerCopy, Player, toPlayer); + if (verified) + { + // Broadcast packet to all other players. + using (var writer = MessageWriter.Get(messageType)) + { + if (toPlayer) + { + var target = reader.ReadPackedInt32(); + reader.CopyTo(writer); + await Player.Game.SendToAsync(writer, target); + } + else + { + reader.CopyTo(writer); + await Player.Game.SendToAllExceptAsync(writer, Id); + } + } + } + } + catch (ImpostorCheatException e) + { + if (_antiCheatConfig.BanIpFromGame) + { + Player.Game.BanIp(Connection.EndPoint.Address); + } + + await DisconnectAsync(DisconnectReason.Hacking, e.Message); + } + + break; + } + + case MessageFlags.EndGame: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message08EndGameC2S.Deserialize( + reader, + out var gameOverReason); + + await Player.Game.HandleEndGame(reader, gameOverReason); + break; + } + + case MessageFlags.AlterGame: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message10AlterGameC2S.Deserialize( + reader, + out var gameTag, + out var value); + + if (gameTag != AlterGameTags.ChangePrivacy) + { + return; + } + + await Player.Game.HandleAlterGame(reader, Player, value); + break; + } + + case MessageFlags.KickPlayer: + { + if (!IsPacketAllowed(reader, true)) + { + return; + } + + Message11KickPlayerC2S.Deserialize( + reader, + out var playerId, + out var isBan); + + await Player.Game.HandleKickPlayer(playerId, isBan); + break; + } + + case MessageFlags.GetGameListV2: + { + Message16GetGameListC2S.Deserialize(reader, out var options); + await OnRequestGameListAsync(options); + break; + } + + default: + _logger.LogWarning("Server received unknown flag {0}.", flag); + break; + } + +#if DEBUG + if (flag != MessageFlags.GameData && + flag != MessageFlags.GameDataTo && + flag != MessageFlags.EndGame && + reader.Position < reader.Length) + { + _logger.LogWarning( + "Server did not consume all bytes from {0} ({1} < {2}).", + flag, + reader.Position, + reader.Length); + } +#endif + } + + public override async ValueTask HandleDisconnectAsync(string reason) + { + try + { + if (Player != null) + { + await Player.Game.HandleRemovePlayer(Id, DisconnectReason.ExitGame); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Exception caught in client disconnection."); + } + + _logger.LogInformation("Client {0} disconnecting, reason: {1}", Id, reason); + _clientManager.Remove(this); + } + + private bool IsPacketAllowed(IMessageReader message, bool hostOnly) + { + if (Player == null) + { + return false; + } + + var game = Player.Game; + + // GameCode must match code of the current game assigned to the player. + if (message.ReadInt32() != game.Code) + { + return false; + } + + // Some packets should only be sent by the host of the game. + if (hostOnly) + { + if (game.HostId == Id) + { + return true; + } + + _logger.LogWarning("[{0}] Client sent packet only allowed by the host ({1}).", Id, game.HostId); + return false; + } + + return true; + } + + /// <summary> + /// Triggered when the connected client requests the game listing. + /// </summary> + /// <param name="options"> + /// All options given. + /// At this moment, the client can only specify the map, impostor count and chat language. + /// </param> + private ValueTask OnRequestGameListAsync(GameOptionsData options) + { + using var message = MessageWriter.Get(MessageType.Reliable); + + var games = _gameManager.FindListings((MapFlags)options.MapId, options.NumImpostors, options.Keywords); + + var skeldGameCount = _gameManager.GetGameCount(MapFlags.Skeld); + var miraHqGameCount = _gameManager.GetGameCount(MapFlags.MiraHQ); + var polusGameCount = _gameManager.GetGameCount(MapFlags.Polus); + + Message16GetGameListS2C.Serialize(message, skeldGameCount, miraHqGameCount, polusGameCount, games); + + return Connection.SendAsync(message); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs b/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs new file mode 100644 index 0000000..5279192 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/ClientBase.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Net +{ + internal abstract class ClientBase : IClient + { + protected ClientBase(string name, IHazelConnection connection) + { + Name = name; + Connection = connection; + Items = new ConcurrentDictionary<object, object>(); + } + + public int Id { get; set; } + + public string Name { get; } + + public IHazelConnection Connection { get; } + + public IDictionary<object, object> Items { get; } + + public ClientPlayer? Player { get; set; } + + IClientPlayer? IClient.Player => Player; + + public abstract ValueTask HandleMessageAsync(IMessageReader message, MessageType messageType); + + public abstract ValueTask HandleDisconnectAsync(string reason); + + + public async ValueTask DisconnectAsync(DisconnectReason reason, string? message = null) + { + if (!Connection.IsConnected) + { + return; + } + + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, reason, message); + + await Connection.SendAsync(packet); + + // Need this to show the correct message, otherwise it shows a generic disconnect message. + await Task.Delay(TimeSpan.FromMilliseconds(250)); + + await Connection.DisconnectAsync(message ?? reason.ToString()); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs b/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs new file mode 100644 index 0000000..ad1fdc7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Factories/ClientFactory.cs @@ -0,0 +1,24 @@ +using System; +using Impostor.Api.Net; +using Microsoft.Extensions.DependencyInjection; + +namespace Impostor.Server.Net.Factories +{ + internal class ClientFactory<TClient> : IClientFactory + where TClient : ClientBase + { + private readonly IServiceProvider _serviceProvider; + + public ClientFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public ClientBase Create(IHazelConnection connection, string name, int clientVersion) + { + var client = ActivatorUtilities.CreateInstance<TClient>(_serviceProvider, name, connection); + connection.Client = client; + return client; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs b/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs new file mode 100644 index 0000000..6859ae3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Factories/IClientFactory.cs @@ -0,0 +1,9 @@ +using Impostor.Api.Net; + +namespace Impostor.Server.Net.Factories +{ + internal interface IClientFactory + { + ClientBase Create(IHazelConnection connection, string name, int clientVersion); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs b/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs new file mode 100644 index 0000000..2a0553f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/GameCodeFactory.cs @@ -0,0 +1,12 @@ +using Impostor.Api.Games; + +namespace Impostor.Server.Net +{ + public class GameCodeFactory : IGameCodeFactory + { + public GameCode Create() + { + return GameCode.Create(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs b/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs new file mode 100644 index 0000000..45d2be8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Hazel/HazelConnection.cs @@ -0,0 +1,74 @@ +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Hazel +{ + internal class HazelConnection : IHazelConnection + { + private readonly ILogger<HazelConnection> _logger; + + public HazelConnection(Connection innerConnection, ILogger<HazelConnection> logger) + { + _logger = logger; + InnerConnection = innerConnection; + innerConnection.DataReceived = ConnectionOnDataReceived; + innerConnection.Disconnected = ConnectionOnDisconnected; + } + + public Connection InnerConnection { get; } + + public IPEndPoint EndPoint => InnerConnection.EndPoint; + + public bool IsConnected => InnerConnection.State == ConnectionState.Connected; + + public IClient Client { get; set; } + + public ValueTask SendAsync(IMessageWriter writer) + { + return InnerConnection.SendAsync(writer); + } + + public ValueTask DisconnectAsync(string reason) + { + return InnerConnection.Disconnect(reason); + } + + public void DisposeInnerConnection() + { + InnerConnection.Dispose(); + } + + private async ValueTask ConnectionOnDisconnected(DisconnectedEventArgs e) + { + if (Client != null) + { + await Client.HandleDisconnectAsync(e.Reason); + } + } + + private async ValueTask ConnectionOnDataReceived(DataReceivedEventArgs e) + { + if (Client == null) + { + return; + } + + while (true) + { + if (e.Message.Position >= e.Message.Length) + { + break; + } + + using (var message = e.Message.ReadMessage()) + { + await Client.HandleMessageAsync(message, e.Type); + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs new file mode 100644 index 0000000..fa9ddb8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/GameDataTag.cs @@ -0,0 +1,13 @@ +namespace Impostor.Server.Net.Inner +{ + public static class GameDataTag + { + public const byte DataFlag = 1; + public const byte RpcFlag = 2; + public const byte SpawnFlag = 4; + public const byte DespawnFlag = 5; + public const byte SceneChangeFlag = 6; + public const byte ReadyFlag = 7; + public const byte ChangeSettingsFlag = 8; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs new file mode 100644 index 0000000..9b53d70 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/GameObject.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Net.Inner +{ + internal class GameObject + { + public GameObject() + { + Components = new List<object>(); + } + + protected List<object> Components { get; } + + public List<T> GetComponentsInChildren<T>() + { + var result = new List<T>(); + + foreach (var component in Components) + { + if (component is T c) + { + result.Add(c); + } + } + + return result; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs new file mode 100644 index 0000000..78f1a55 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/InnerNetObject.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Net.Inner +{ + internal abstract class InnerNetObject : GameObject, IInnerNetObject + { + private const int HostInheritId = -2; + + public uint NetId { get; internal set; } + + public int OwnerId { get; internal set; } + + public SpawnFlags SpawnFlags { get; internal set; } + + public abstract ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader); + + public abstract bool Serialize(IMessageWriter writer, bool initialState); + + public abstract void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState); + + public bool IsOwnedBy(IClientPlayer player) + { + return OwnerId == player.Client.Id || + (OwnerId == HostInheritId && player.IsHost); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs new file mode 100644 index 0000000..6cdd6b9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.Api.cs @@ -0,0 +1,25 @@ +using System.Numerics; +using System.Threading.Tasks; +using Impostor.Api.Net.Inner.Objects.Components; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerCustomNetworkTransform : IInnerCustomNetworkTransform + { + public async ValueTask SnapToAsync(Vector2 position) + { + var minSid = (ushort)(_lastSequenceId + 5U); + + // Snap in the server. + SnapTo(position, minSid); + + // Broadcast to all clients. + using (var writer = _game.StartRpc(NetId, RpcCalls.SnapTo)) + { + WriteVector2(writer, position); + writer.Write(_lastSequenceId); + await _game.FinishRpcAsync(writer); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs new file mode 100644 index 0000000..d261c11 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerCustomNetworkTransform.cs @@ -0,0 +1,148 @@ +using System.Numerics; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerCustomNetworkTransform : InnerNetObject + { + private static readonly FloatRange XRange = new FloatRange(-40f, 40f); + private static readonly FloatRange YRange = new FloatRange(-40f, 40f); + + private readonly ILogger<InnerCustomNetworkTransform> _logger; + private readonly InnerPlayerControl _playerControl; + private readonly Game _game; + + private ushort _lastSequenceId; + private Vector2 _targetSyncPosition; + private Vector2 _targetSyncVelocity; + + public InnerCustomNetworkTransform(ILogger<InnerCustomNetworkTransform> logger, InnerPlayerControl playerControl, Game game) + { + _logger = logger; + _playerControl = playerControl; + _game = game; + } + + private static bool SidGreaterThan(ushort newSid, ushort prevSid) + { + var num = (ushort)(prevSid + (uint) short.MaxValue); + + return (int) prevSid < (int) num + ? newSid > prevSid && newSid <= num + : newSid > prevSid || newSid <= num; + } + + private static void WriteVector2(IMessageWriter writer, Vector2 vec) + { + writer.Write((ushort)(XRange.ReverseLerp(vec.X) * (double) ushort.MaxValue)); + writer.Write((ushort)(YRange.ReverseLerp(vec.Y) * (double) ushort.MaxValue)); + } + + private static Vector2 ReadVector2(IMessageReader reader) + { + var v1 = reader.ReadUInt16() / (float) ushort.MaxValue; + var v2 = reader.ReadUInt16() / (float) ushort.MaxValue; + + return new Vector2(XRange.Lerp(v1), YRange.Lerp(v2)); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + if (call == RpcCalls.SnapTo) + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} to a specific player instead of broadcast"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SnapTo)} as crewmate"); + } + + SnapTo(ReadVector2(reader), reader.ReadUInt16()); + } + else + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerCustomNetworkTransform), call); + } + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + if (initialState) + { + writer.Write(_lastSequenceId); + WriteVector2(writer, _targetSyncPosition); + WriteVector2(writer, _targetSyncVelocity); + return true; + } + + // TODO: DirtyBits == 0 return false. + _lastSequenceId++; + + writer.Write(_lastSequenceId); + WriteVector2(writer, _targetSyncPosition); + WriteVector2(writer, _targetSyncVelocity); + return true; + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + var sequenceId = reader.ReadUInt16(); + + if (initialState) + { + _lastSequenceId = sequenceId; + _targetSyncPosition = ReadVector2(reader); + _targetSyncVelocity = ReadVector2(reader); + } + else + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client attempted to send unowned {nameof(InnerCustomNetworkTransform)} data"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client attempted to send {nameof(InnerCustomNetworkTransform)} data to a specific player, must be broadcast"); + } + + if (!SidGreaterThan(sequenceId, _lastSequenceId)) + { + return; + } + + _lastSequenceId = sequenceId; + _targetSyncPosition = ReadVector2(reader); + _targetSyncVelocity = ReadVector2(reader); + } + } + + private void SnapTo(Vector2 position, ushort minSid) + { + if (!SidGreaterThan(minSid, _lastSequenceId)) + { + return; + } + + _lastSequenceId = minSid; + _targetSyncPosition = position; + _targetSyncVelocity = Vector2.Zero; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs new file mode 100644 index 0000000..6af54a0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.Api.cs @@ -0,0 +1,8 @@ +using Impostor.Api.Net.Inner.Objects.Components; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerPlayerPhysics : IInnerPlayerPhysics + { + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs new file mode 100644 index 0000000..29bc996 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerPlayerPhysics.cs @@ -0,0 +1,70 @@ +using System; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal partial class InnerPlayerPhysics : InnerNetObject + { + private readonly ILogger<InnerPlayerPhysics> _logger; + private readonly InnerPlayerControl _playerControl; + private readonly IEventManager _eventManager; + private readonly Game _game; + + public InnerPlayerPhysics(ILogger<InnerPlayerPhysics> logger, InnerPlayerControl playerControl, IEventManager eventManager, Game game) + { + _logger = logger; + _playerControl = playerControl; + _eventManager = eventManager; + _game = game; + } + + public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + if (call != RpcCalls.EnterVent && call != RpcCalls.ExitVent) + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerPlayerPhysics), call); + return; + } + + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {call} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {call} to a specific player instead of broadcast"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {call} as crewmate"); + } + + var ventId = reader.ReadPackedUInt32(); + var ventEnter = call == RpcCalls.EnterVent; + + await _eventManager.CallAsync(new PlayerVentEvent(_game, sender, _playerControl, (VentLocation)ventId, ventEnter)); + + return; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + throw new NotImplementedException(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs new file mode 100644 index 0000000..58b9b54 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Components/InnerVoteBanSystem.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects.Components +{ + internal class InnerVoteBanSystem : InnerNetObject, IInnerVoteBanSystem + { + private readonly ILogger<InnerVoteBanSystem> _logger; + private readonly Dictionary<int, int[]> _votes; + + public InnerVoteBanSystem(ILogger<InnerVoteBanSystem> logger) + { + _logger = logger; + _votes = new Dictionary<int, int[]>(); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + if (call != RpcCalls.AddVote) + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerVoteBanSystem), call); + return default; + } + + var clientId = reader.ReadInt32(); + if (clientId != sender.Client.Id) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.AddVote)} as other client"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to wrong destinition, must be broadcast"); + } + + var targetClientId = reader.ReadInt32(); + + // TODO: Use. + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerShipStatus)} as non-host"); + } + + var votes = _votes; + var unknown = reader.ReadByte(); + if (unknown != 0) + { + for (var i = 0; i < unknown; i++) + { + var v4 = reader.ReadInt32(); + if (v4 == 0) + { + break; + } + + if (!votes.TryGetValue(v4, out var v12)) + { + v12 = new int[3]; + votes[v4] = v12; + } + + for (var j = 0; j < 3; j++) + { + v12[j] = reader.ReadPackedInt32(); + } + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs new file mode 100644 index 0000000..29ca50a --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.TaskInfo.cs @@ -0,0 +1,30 @@ +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerGameData + { + public class TaskInfo : ITaskInfo + { + public uint Id { get; internal set; } + + public bool Complete { get; internal set; } + + public TaskTypes Type { get; internal set; } + + public void Serialize(IMessageWriter writer) + { + writer.WritePacked((uint)Id); + writer.Write(Complete); + } + + public void Deserialize(IMessageReader reader) + { + Id = reader.ReadPackedUInt32(); + Complete = reader.ReadBoolean(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs new file mode 100644 index 0000000..ed68038 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerGameData.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.Inner.Objects.Components; +using Impostor.Server.Net.State; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerGameData : InnerNetObject, IInnerGameData + { + private readonly ILogger<InnerGameData> _logger; + private readonly Game _game; + private readonly ConcurrentDictionary<byte, InnerPlayerInfo> _allPlayers; + + public InnerGameData(ILogger<InnerGameData> logger, Game game, IServiceProvider serviceProvider) + { + _logger = logger; + _game = game; + _allPlayers = new ConcurrentDictionary<byte, InnerPlayerInfo>(); + + Components.Add(this); + Components.Add(ActivatorUtilities.CreateInstance<InnerVoteBanSystem>(serviceProvider)); + } + + public int PlayerCount => _allPlayers.Count; + + public IReadOnlyDictionary<byte, InnerPlayerInfo> Players => _allPlayers; + + public InnerPlayerInfo? GetPlayerById(byte id) + { + if (id == byte.MaxValue) + { + return null; + } + + return _allPlayers.TryGetValue(id, out var player) ? player : null; + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + case RpcCalls.SetTasks: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} to a specific player instead of broadcast"); + } + + var playerId = reader.ReadByte(); + var taskTypeIds = reader.ReadBytesAndSize(); + + SetTasks(playerId, taskTypeIds); + break; + } + + case RpcCalls.UpdateGameData: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetTasks)} to a specific player instead of broadcast"); + } + + while (reader.Position < reader.Length) + { + using var message = reader.ReadMessage(); + var player = GetPlayerById(message.Tag); + if (player != null) + { + player.Deserialize(message); + } + else + { + var playerInfo = new InnerPlayerInfo(message.Tag); + + playerInfo.Deserialize(reader); + + if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo)) + { + throw new ImpostorException("Failed to add player to InnerGameData."); + } + } + } + + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerGameData), call); + break; + } + } + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerGameData)} as non-host"); + } + + if (initialState) + { + var num = reader.ReadPackedInt32(); + + for (var i = 0; i < num; i++) + { + var playerId = reader.ReadByte(); + var playerInfo = new InnerPlayerInfo(playerId); + + playerInfo.Deserialize(reader); + + if (!_allPlayers.TryAdd(playerInfo.PlayerId, playerInfo)) + { + throw new ImpostorException("Failed to add player to InnerGameData."); + } + } + } + else + { + throw new NotImplementedException("This shouldn't happen, according to Among Us disassembly."); + } + } + + internal void AddPlayer(InnerPlayerControl control) + { + var playerId = control.PlayerId; + var playerInfo = new InnerPlayerInfo(control.PlayerId); + + if (_allPlayers.TryAdd(playerId, playerInfo)) + { + control.PlayerInfo = playerInfo; + } + } + + private void SetTasks(byte playerId, ReadOnlyMemory<byte> taskTypeIds) + { + var player = GetPlayerById(playerId); + if (player == null) + { + _logger.LogTrace("Could not set tasks for playerId {0}.", playerId); + return; + } + + if (player.Disconnected) + { + return; + } + + player.Tasks = new List<TaskInfo>(taskTypeIds.Length); + + foreach (var taskId in taskTypeIds.ToArray()) + { + player.Tasks.Add(new TaskInfo + { + Id = taskId, + }); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs new file mode 100644 index 0000000..63448ed --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerLobbyBehaviour.cs @@ -0,0 +1,36 @@ +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.State; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal class InnerLobbyBehaviour : InnerNetObject, IInnerLobbyBehaviour + { + private readonly IGame _game; + + public InnerLobbyBehaviour(IGame game) + { + _game = game; + + Components.Add(this); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + throw new System.NotImplementedException(); + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + throw new System.NotImplementedException(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs new file mode 100644 index 0000000..5d120c5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.Api.cs @@ -0,0 +1,9 @@ +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud : IInnerMeetingHud + { + + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs new file mode 100644 index 0000000..fbf2510 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.PlayerVoteArea.cs @@ -0,0 +1,49 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud + { + public class PlayerVoteArea + { + private const byte VoteMask = 15; + private const byte ReportedBit = 32; + private const byte VotedBit = 64; + private const byte DeadBit = 128; + + public PlayerVoteArea(InnerMeetingHud parent, byte targetPlayerId) + { + Parent = parent; + TargetPlayerId = targetPlayerId; + } + + public InnerMeetingHud Parent { get; } + + public byte TargetPlayerId { get; } + + public bool IsDead { get; private set; } + + public bool DidVote { get; private set; } + + public bool DidReport { get; private set; } + + public sbyte VotedFor { get; private set; } + + internal void SetDead(bool didReport, bool isDead) + { + DidReport = didReport; + IsDead = isDead; + } + + public void Deserialize(IMessageReader reader) + { + var num = reader.ReadByte(); + + VotedFor = (sbyte)((num & VoteMask) - 1); + IsDead = (num & DeadBit) > 0; + DidVote = (num & VotedBit) > 0; + DidReport = (num & ReportedBit) > 0; + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs new file mode 100644 index 0000000..c2bcb9d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerMeetingHud.cs @@ -0,0 +1,176 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Events.Meeting; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerMeetingHud : InnerNetObject + { + private readonly ILogger<InnerMeetingHud> _logger; + private readonly IEventManager _eventManager; + private readonly Game _game; + private readonly GameNet _gameNet; + private PlayerVoteArea[] _playerStates; + + public InnerMeetingHud(ILogger<InnerMeetingHud> logger, IEventManager eventManager, Game game) + { + _logger = logger; + _eventManager = eventManager; + _game = game; + _gameNet = game.GameNet; + _playerStates = null; + + Components.Add(this); + } + + public byte ReporterId { get; private set; } + + private void PopulateButtons(byte reporter) + { + _playerStates = _gameNet.GameData.Players + .Select(x => + { + var area = new PlayerVoteArea(this, x.Key); + area.SetDead(x.Value.PlayerId == reporter, x.Value.Disconnected || x.Value.IsDead); + return area; + }) + .ToArray(); + } + + public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + case RpcCalls.Close: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Close)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Close)} to a specific player instead of broadcast"); + } + + break; + } + + case RpcCalls.VotingComplete: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.VotingComplete)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.VotingComplete)} to a specific player instead of broadcast"); + } + + var states = reader.ReadBytesAndSize(); + var playerId = reader.ReadByte(); + var tie = reader.ReadBoolean(); + + if (playerId != byte.MaxValue) + { + var player = _game.GameNet.GameData.GetPlayerById(playerId); + if (player != null) + { + player.Controller.Die(DeathReason.Exile); + await _eventManager.CallAsync(new PlayerExileEvent(_game, sender, player.Controller)); + } + } + + await _eventManager.CallAsync(new MeetingEndedEvent(_game, this)); + + break; + } + + case RpcCalls.CastVote: + { + var srcPlayerId = reader.ReadByte(); + if (srcPlayerId != sender.Character.PlayerId) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to an unowned {nameof(InnerPlayerControl)}"); + } + + // Host broadcasts vote to others. + if (sender.IsHost && target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to a specific player instead of broadcast"); + } + + // Player sends vote to host. + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CastVote)} to wrong destinition, must be host"); + } + + var targetPlayerId = reader.ReadByte(); + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerMeetingHud), call); + break; + } + } + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerMeetingHud)} as non-host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client attempted to send {nameof(InnerMeetingHud)} data to a specific player, must be broadcast"); + } + + if (initialState) + { + PopulateButtons(0); + + foreach (var playerState in _playerStates) + { + playerState.Deserialize(reader); + + if (playerState.DidReport) + { + ReporterId = playerState.TargetPlayerId; + } + } + } + else + { + var num = reader.ReadPackedUInt32(); + + for (var i = 0; i < _playerStates.Length; i++) + { + if ((num & 1 << i) != 0) + { + _playerStates[i].Deserialize(reader); + } + } + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs new file mode 100644 index 0000000..0a7997d --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.Api.cs @@ -0,0 +1,138 @@ +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Innersloth.Customization; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Inner.Objects.Components; +using Impostor.Server.Events.Player; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerControl : IInnerPlayerControl + { + IInnerPlayerPhysics IInnerPlayerControl.Physics => Physics; + + IInnerCustomNetworkTransform IInnerPlayerControl.NetworkTransform => NetworkTransform; + + IInnerPlayerInfo IInnerPlayerControl.PlayerInfo => PlayerInfo; + + public async ValueTask SetNameAsync(string name) + { + PlayerInfo.PlayerName = name; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetName); + writer.Write(name); + await _game.FinishRpcAsync(writer); + } + + public async ValueTask SetColorAsync(byte colorId) + { + PlayerInfo.ColorId = colorId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetColor); + writer.Write(colorId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetColorAsync(ColorType colorType) + { + return SetColorAsync((byte)colorType); + } + + public async ValueTask SetHatAsync(uint hatId) + { + PlayerInfo.HatId = hatId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetHat); + writer.WritePacked(hatId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetHatAsync(HatType hatType) + { + return SetHatAsync((uint)hatType); + } + + public async ValueTask SetPetAsync(uint petId) + { + PlayerInfo.PetId = petId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetPet); + writer.WritePacked(petId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetPetAsync(PetType petType) + { + return SetPetAsync((uint)petType); + } + + public async ValueTask SetSkinAsync(uint skinId) + { + PlayerInfo.SkinId = skinId; + + using var writer = _game.StartRpc(NetId, RpcCalls.SetSkin); + writer.WritePacked(skinId); + await _game.FinishRpcAsync(writer); + } + + public ValueTask SetSkinAsync(SkinType skinType) + { + return SetSkinAsync((uint)skinType); + } + + public async ValueTask SendChatAsync(string text) + { + using var writer = _game.StartRpc(NetId, RpcCalls.SendChat); + writer.Write(text); + await _game.FinishRpcAsync(writer); + } + + public async ValueTask SendChatToPlayerAsync(string text, IInnerPlayerControl? player = null) + { + if (player == null) + { + player = this; + } + + using var writer = _game.StartRpc(NetId, RpcCalls.SendChat); + writer.Write(text); + await _game.FinishRpcAsync(writer, player.OwnerId); + } + + public async ValueTask SetMurderedByAsync(IClientPlayer impostor) + { + if (impostor.Character == null) + { + throw new ImpostorException("Character is null."); + } + + if (!impostor.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorProtocolException("Plugin tried to murder a player while the impostor specified was not an impostor."); + } + + if (impostor.Character.PlayerInfo.IsDead) + { + throw new ImpostorProtocolException("Plugin tried to murder a player while the impostor specified was dead."); + } + + if (PlayerInfo.IsDead) + { + return; + } + + // Update player. + Die(DeathReason.Kill); + + // Send RPC. + using var writer = _game.StartRpc(impostor.Character.NetId, RpcCalls.MurderPlayer); + writer.WritePacked(NetId); + await _game.FinishRpcAsync(writer); + + // Notify plugins. + await _eventManager.CallAsync(new PlayerMurderEvent(_game, impostor, impostor.Character, this)); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs new file mode 100644 index 0000000..c71fcf7 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerControl.cs @@ -0,0 +1,455 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.Inner.Objects.Components; +using Impostor.Server.Net.State; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerControl : InnerNetObject + { + private readonly ILogger<InnerPlayerControl> _logger; + private readonly IEventManager _eventManager; + private readonly Game _game; + + public InnerPlayerControl(ILogger<InnerPlayerControl> logger, IServiceProvider serviceProvider, IEventManager eventManager, Game game) + { + _logger = logger; + _eventManager = eventManager; + _game = game; + + Physics = ActivatorUtilities.CreateInstance<InnerPlayerPhysics>(serviceProvider, this, _eventManager, _game); + NetworkTransform = ActivatorUtilities.CreateInstance<InnerCustomNetworkTransform>(serviceProvider, this, _game); + + Components.Add(this); + Components.Add(Physics); + Components.Add(NetworkTransform); + + PlayerId = byte.MaxValue; + } + + public bool IsNew { get; private set; } + + public byte PlayerId { get; private set; } + + public InnerPlayerPhysics Physics { get; } + + public InnerCustomNetworkTransform NetworkTransform { get; } + + public InnerPlayerInfo PlayerInfo { get; internal set; } + + public override async ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, IMessageReader reader) + { + switch (call) + { + // Play an animation. + case RpcCalls.PlayAnimation: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.PlayAnimation)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.PlayAnimation)} to a specific player instead of broadcast"); + } + + var animation = reader.ReadByte(); + break; + } + + // Complete a task. + case RpcCalls.CompleteTask: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CompleteTask)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CompleteTask)} to a specific player instead of broadcast"); + } + + var taskId = reader.ReadPackedUInt32(); + var task = PlayerInfo.Tasks[(int)taskId]; + if (task == null) + { + _logger.LogWarning($"Client sent {nameof(RpcCalls.CompleteTask)} with a taskIndex that is not in their {nameof(InnerPlayerInfo)}"); + } + else + { + task.Complete = true; + await _eventManager.CallAsync(new PlayerCompletedTaskEvent(_game, sender, this, task)); + } + + break; + } + + // Update GameOptions. + case RpcCalls.SyncSettings: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SyncSettings)} but was not a host"); + } + + _game.Options.Deserialize(reader.ReadBytesAndSize()); + break; + } + + // Set Impostors. + case RpcCalls.SetInfected: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetInfected)} but was not a host"); + } + + var length = reader.ReadPackedInt32(); + + for (var i = 0; i < length; i++) + { + var playerId = reader.ReadByte(); + var player = _game.GameNet.GameData.GetPlayerById(playerId); + if (player != null) + { + player.IsImpostor = true; + } + } + + if (_game.GameState == GameStates.Starting) + { + await _game.StartedAsync(); + } + + break; + } + + // Player was voted out. + case RpcCalls.Exiled: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Exiled)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.Exiled)} to a specific player instead of broadcast"); + } + + // TODO: Not hit? + Die(DeathReason.Exile); + + await _eventManager.CallAsync(new PlayerExileEvent(_game, sender, this)); + break; + } + + // Validates the player name at the host. + case RpcCalls.CheckName: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CheckName)} to the wrong player"); + } + + var name = reader.ReadString(); + break; + } + + // Update the name of a player. + case RpcCalls.SetName: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetName)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetName)} to a specific player instead of broadcast"); + } + + PlayerInfo.PlayerName = reader.ReadString(); + break; + } + + // Validates the color at the host. + case RpcCalls.CheckColor: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CheckColor)} to the wrong player"); + } + + var color = reader.ReadByte(); + break; + } + + // Update the color of a player. + case RpcCalls.SetColor: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetColor)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetColor)} to a specific player instead of broadcast"); + } + + PlayerInfo.ColorId = reader.ReadByte(); + break; + } + + // Update the hat of a player. + case RpcCalls.SetHat: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to a specific player instead of broadcast"); + } + + PlayerInfo.HatId = reader.ReadPackedUInt32(); + break; + } + + case RpcCalls.SetSkin: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetSkin)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetHat)} to a specific player instead of broadcast"); + } + + PlayerInfo.SkinId = reader.ReadPackedUInt32(); + break; + } + + // TODO: (ANTICHEAT) Location check? + // only called by a non-host player on to start meeting + case RpcCalls.ReportDeadBody: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.ReportDeadBody)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.ReportDeadBody)} to a specific player instead of broadcast"); + } + + + var deadBodyPlayerId = reader.ReadByte(); + // deadBodyPlayerId == byte.MaxValue -- means emergency call by button + + break; + } + + // TODO: (ANTICHEAT) Cooldown check? + case RpcCalls.MurderPlayer: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} to a specific player instead of broadcast"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} as crewmate"); + } + + if (!sender.Character.PlayerInfo.CanMurder(_game)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.MurderPlayer)} too fast"); + } + + sender.Character.PlayerInfo.LastMurder = DateTimeOffset.UtcNow; + + var player = reader.ReadNetObject<InnerPlayerControl>(_game); + if (!player.PlayerInfo.IsDead) + { + player.Die(DeathReason.Kill); + await _eventManager.CallAsync(new PlayerMurderEvent(_game, sender, this, player)); + } + + break; + } + + case RpcCalls.SendChat: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChat)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChat)} to a specific player instead of broadcast"); + } + + var chat = reader.ReadString(); + + await _eventManager.CallAsync(new PlayerChatEvent(_game, sender, this, chat)); + break; + } + + case RpcCalls.StartMeeting: + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.StartMeeting)} but was not a host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.StartMeeting)} to a specific player instead of broadcast"); + } + + // deadBodyPlayerId == byte.MaxValue -- means emergency call by button + var deadBodyPlayerId = reader.ReadByte(); + var deadPlayer = deadBodyPlayerId != byte.MaxValue + ? _game.GameNet.GameData.GetPlayerById(deadBodyPlayerId)?.Controller + : null; + + await _eventManager.CallAsync(new PlayerStartMeetingEvent(_game, _game.GetClientPlayer(this.OwnerId), this, deadPlayer)); + break; + } + + case RpcCalls.SetScanner: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetScanner)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetScanner)} to a specific player instead of broadcast"); + } + + var on = reader.ReadBoolean(); + var count = reader.ReadByte(); + break; + } + + case RpcCalls.SendChatNote: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChatNote)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SendChatNote)} to a specific player instead of broadcast"); + } + + var playerId = reader.ReadByte(); + var chatNote = (ChatNoteType)reader.ReadByte(); + break; + } + + case RpcCalls.SetPet: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetPet)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetPet)} to a specific player instead of broadcast"); + } + + PlayerInfo.PetId = reader.ReadPackedUInt32(); + break; + } + + // TODO: Understand this RPC + case RpcCalls.SetStartCounter: + { + if (!sender.IsOwner(this)) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetStartCounter)} to an unowned {nameof(InnerPlayerControl)}"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.SetStartCounter)} to a specific player instead of broadcast"); + } + + // Used to compare with LastStartCounter. + var startCounter = reader.ReadPackedUInt32(); + + // Is either start countdown or byte.MaxValue + var secondsLeft = reader.ReadByte(); + if (secondsLeft < byte.MaxValue) + { + await _eventManager.CallAsync(new PlayerSetStartCounterEvent(_game, sender, this, secondsLeft)); + } + + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerPlayerControl), call); + break; + } + } + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerPlayerControl)} as non-host"); + } + + if (initialState) + { + IsNew = reader.ReadBoolean(); + } + + PlayerId = reader.ReadByte(); + } + + internal void Die(DeathReason reason) + { + PlayerInfo.IsDead = true; + PlayerInfo.LastDeathReason = reason; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs new file mode 100644 index 0000000..512a4f1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.Api.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerInfo : IInnerPlayerInfo + { + IEnumerable<ITaskInfo> IInnerPlayerInfo.Tasks => Tasks; + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs new file mode 100644 index 0000000..f248994 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerPlayerInfo.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal partial class InnerPlayerInfo + { + public InnerPlayerInfo(byte playerId) + { + PlayerId = playerId; + } + + public InnerPlayerControl Controller { get; internal set; } + + public byte PlayerId { get; } + + public string PlayerName { get; internal set; } + + public byte ColorId { get; internal set; } + + public uint HatId { get; internal set; } + + public uint PetId { get; internal set; } + + public uint SkinId { get; internal set; } + + public bool Disconnected { get; internal set; } + + public bool IsImpostor { get; internal set; } + + public bool IsDead { get; internal set; } + + public DeathReason LastDeathReason { get; internal set; } + + public List<InnerGameData.TaskInfo> Tasks { get; internal set; } + + public DateTimeOffset LastMurder { get; set; } + + public bool CanMurder(IGame game) + { + if (!IsImpostor) + { + return false; + } + + return DateTimeOffset.UtcNow.Subtract(LastMurder).TotalSeconds >= game.Options.KillCooldown; + } + + public void Serialize(IMessageWriter writer) + { + throw new NotImplementedException(); + } + + public void Deserialize(IMessageReader reader) + { + PlayerName = reader.ReadString(); + ColorId = reader.ReadByte(); + HatId = reader.ReadPackedUInt32(); + PetId = reader.ReadPackedUInt32(); + SkinId = reader.ReadPackedUInt32(); + var flag = reader.ReadByte(); + Disconnected = (flag & 1) > 0; + IsImpostor = (flag & 2) > 0; + IsDead = (flag & 4) > 0; + var taskCount = reader.ReadByte(); + for (var i = 0; i < taskCount; i++) + { + Tasks[i] ??= new InnerGameData.TaskInfo(); + Tasks[i].Deserialize(reader); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs new file mode 100644 index 0000000..b1a3f18 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/InnerShipStatus.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Api.Net.Messages; +using Impostor.Server.Net.Inner.Objects.Systems; +using Impostor.Server.Net.Inner.Objects.Systems.ShipStatus; +using Impostor.Server.Net.State; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Inner.Objects +{ + internal class InnerShipStatus : InnerNetObject, IInnerShipStatus + { + private readonly ILogger<InnerShipStatus> _logger; + private readonly Game _game; + private readonly Dictionary<SystemTypes, ISystemType> _systems; + + public InnerShipStatus(ILogger<InnerShipStatus> logger, Game game) + { + _logger = logger; + _game = game; + + _systems = new Dictionary<SystemTypes, ISystemType> + { + [SystemTypes.Electrical] = new SwitchSystem(), + [SystemTypes.MedBay] = new MedScanSystem(), + [SystemTypes.Reactor] = new ReactorSystemType(), + [SystemTypes.LifeSupp] = new LifeSuppSystemType(), + [SystemTypes.Security] = new SecurityCameraSystemType(), + [SystemTypes.Comms] = new HudOverrideSystemType(), + [SystemTypes.Doors] = new DoorsSystemType(_game), + }; + + _systems.Add(SystemTypes.Sabotage, new SabotageSystemType(new[] + { + (IActivatable)_systems[SystemTypes.Comms], + (IActivatable)_systems[SystemTypes.Reactor], + (IActivatable)_systems[SystemTypes.LifeSupp], + (IActivatable)_systems[SystemTypes.Electrical], + })); + + Components.Add(this); + } + + public override ValueTask HandleRpc(ClientPlayer sender, ClientPlayer? target, RpcCalls call, + IMessageReader reader) + { + switch (call) + { + case RpcCalls.CloseDoorsOfType: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CloseDoorsOfType)} to wrong destinition, must be host"); + } + + if (!sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.CloseDoorsOfType)} as crewmate"); + } + + var systemType = (SystemTypes)reader.ReadByte(); + + break; + } + + case RpcCalls.RepairSystem: + { + if (target == null || !target.IsHost) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.RepairSystem)} to wrong destinition, must be host"); + } + + var systemType = (SystemTypes)reader.ReadByte(); + if (systemType == SystemTypes.Sabotage && !sender.Character.PlayerInfo.IsImpostor) + { + throw new ImpostorCheatException($"Client sent {nameof(RpcCalls.RepairSystem)} for {systemType} as crewmate"); + } + + var player = reader.ReadNetObject<InnerPlayerControl>(_game); + var amount = reader.ReadByte(); + + // TODO: Modify data (?) + break; + } + + default: + { + _logger.LogWarning("{0}: Unknown rpc call {1}", nameof(InnerShipStatus), call); + break; + } + } + + return default; + } + + public override bool Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public override void Deserialize(IClientPlayer sender, IClientPlayer? target, IMessageReader reader, bool initialState) + { + if (!sender.IsHost) + { + throw new ImpostorCheatException($"Client attempted to send data for {nameof(InnerShipStatus)} as non-host"); + } + + if (target != null) + { + throw new ImpostorCheatException($"Client attempted to send {nameof(InnerShipStatus)} data to a specific player, must be broadcast"); + } + + if (initialState) + { + // TODO: (_systems[SystemTypes.Doors] as DoorsSystemType).SetDoors(); + foreach (var systemType in SystemTypeHelpers.AllTypes) + { + if (_systems.TryGetValue(systemType, out var system)) + { + system.Deserialize(reader, true); + } + } + } + else + { + var count = reader.ReadPackedUInt32(); + + foreach (var systemType in SystemTypeHelpers.AllTypes) + { + // TODO: Not sure what is going on here, check. + if ((count & 1 << (int)(systemType & (SystemTypes.ShipTasks | SystemTypes.Doors))) != 0L) + { + if (_systems.TryGetValue(systemType, out var system)) + { + system.Deserialize(reader, false); + } + } + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs new file mode 100644 index 0000000..e595b25 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/IActivatable.cs @@ -0,0 +1,7 @@ +namespace Impostor.Server.Net.Inner.Objects.Systems +{ + public interface IActivatable + { + bool IsActive { get; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs new file mode 100644 index 0000000..a6ef88e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ISystemType.cs @@ -0,0 +1,11 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems +{ + public interface ISystemType + { + void Serialize(IMessageWriter writer, bool initialState); + + void Deserialize(IMessageReader reader, bool initialState); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs new file mode 100644 index 0000000..64b1f5f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/DoorsSystemType.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class DoorsSystemType : ISystemType + { + // TODO: AutoDoors + private readonly Dictionary<int, bool> _doors; + + public DoorsSystemType(IGame game) + { + var doorCount = game.Options.Map switch + { + MapTypes.Skeld => 13, + MapTypes.MiraHQ => 2, + MapTypes.Polus => 12, + _ => throw new ArgumentOutOfRangeException() + }; + + _doors = new Dictionary<int, bool>(doorCount); + + for (var i = 0; i < doorCount; i++) + { + _doors.Add(i, false); + } + } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + if (initialState) + { + for (var i = 0; i < _doors.Count; i++) + { + _doors[i] = reader.ReadBoolean(); + } + } + else + { + var num = reader.ReadPackedUInt32(); + + for (var i = 0; i < _doors.Count; i++) + { + if ((num & 1 << i) != 0) + { + _doors[i] = reader.ReadBoolean(); + } + } + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs new file mode 100644 index 0000000..42aa8d3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/HudOverrideSystemType.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class HudOverrideSystemType : ISystemType, IActivatable + { + public bool IsActive { get; private set; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + IsActive = reader.ReadBoolean(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs new file mode 100644 index 0000000..f644024 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/LifeSuppSystemType.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class LifeSuppSystemType : ISystemType, IActivatable + { + public LifeSuppSystemType() + { + Countdown = 10000f; + CompletedConsoles = new HashSet<int>(); + } + + public float Countdown { get; private set; } + + public HashSet<int> CompletedConsoles { get; } + + public bool IsActive => Countdown < 10000.0; + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + Countdown = reader.ReadSingle(); + + if (reader.Position >= reader.Length) + { + return; + } + + CompletedConsoles.Clear(); // TODO: Thread safety + + var num = reader.ReadPackedInt32(); + + for (var i = 0; i < num; i++) + { + CompletedConsoles.Add(reader.ReadPackedInt32()); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs new file mode 100644 index 0000000..007b9d0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/MedScanSystem.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class MedScanSystem : ISystemType + { + public MedScanSystem() + { + UsersList = new List<byte>(); + } + + public List<byte> UsersList { get; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + UsersList.Clear(); + + var num = reader.ReadPackedInt32(); + + for (var i = 0; i < num; i++) + { + UsersList.Add(reader.ReadByte()); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs new file mode 100644 index 0000000..4380918 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/ReactorSystemType.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class ReactorSystemType : ISystemType, IActivatable + { + public ReactorSystemType() + { + Countdown = 10000f; + UserConsolePairs = new HashSet<Tuple<byte, byte>>(); + } + + public float Countdown { get; private set; } + + public HashSet<Tuple<byte, byte>> UserConsolePairs { get; } + + public bool IsActive => Countdown < 10000.0; + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + Countdown = reader.ReadSingle(); + UserConsolePairs.Clear(); // TODO: Thread safety + + var count = reader.ReadPackedInt32(); + + for (var i = 0; i < count; i++) + { + UserConsolePairs.Add(new Tuple<byte, byte>(reader.ReadByte(), reader.ReadByte())); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs new file mode 100644 index 0000000..193cfe8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SabotageSystemType.cs @@ -0,0 +1,26 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class SabotageSystemType : ISystemType + { + private readonly IActivatable[] _specials; + + public SabotageSystemType(IActivatable[] specials) + { + _specials = specials; + } + + public float Timer { get; set; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + Timer = reader.ReadSingle(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs new file mode 100644 index 0000000..df41b68 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SecurityCameraSystemType.cs @@ -0,0 +1,19 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class SecurityCameraSystemType : ISystemType + { + public byte InUse { get; internal set; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + InUse = reader.ReadByte(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs new file mode 100644 index 0000000..925774a --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/Objects/Systems/ShipStatus/SwitchSystem.cs @@ -0,0 +1,27 @@ +using Impostor.Api.Net.Messages; + +namespace Impostor.Server.Net.Inner.Objects.Systems.ShipStatus +{ + public class SwitchSystem : ISystemType, IActivatable + { + public byte ExpectedSwitches { get; set; } + + public byte ActualSwitches { get; set; } + + public byte Value { get; set; } = byte.MaxValue; + + public bool IsActive { get; } + + public void Serialize(IMessageWriter writer, bool initialState) + { + throw new System.NotImplementedException(); + } + + public void Deserialize(IMessageReader reader, bool initialState) + { + ExpectedSwitches = reader.ReadByte(); + ActualSwitches = reader.ReadByte(); + Value = reader.ReadByte(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs new file mode 100644 index 0000000..ce48965 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/RpcCalls.cs @@ -0,0 +1,37 @@ +namespace Impostor.Server.Net.Inner +{ + public enum RpcCalls : byte + { + PlayAnimation = 0, + CompleteTask = 1, + SyncSettings = 2, + SetInfected = 3, + Exiled = 4, + CheckName = 5, + SetName = 6, + CheckColor = 7, + SetColor = 8, + SetHat = 9, + SetSkin = 10, + ReportDeadBody = 11, + MurderPlayer = 12, + SendChat = 13, + StartMeeting = 14, + SetScanner = 15, + SendChatNote = 16, + SetPet = 17, + SetStartCounter = 18, + EnterVent = 19, + ExitVent = 20, + SnapTo = 21, + Close = 22, + VotingComplete = 23, + CastVote = 24, + ClearVote = 25, + AddVote = 26, + CloseDoorsOfType = 27, + RepairSystem = 28, + SetTasks = 29, + UpdateGameData = 30, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs b/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs new file mode 100644 index 0000000..1860098 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Inner/SpawnFlags.cs @@ -0,0 +1,11 @@ +using System; + +namespace Impostor.Server.Net.Inner +{ + [Flags] + public enum SpawnFlags : byte + { + None = 0, + IsClientCharacter = 1, + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs new file mode 100644 index 0000000..6cbe5bf --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.Api.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Impostor.Api.Net; +using Impostor.Api.Net.Manager; + +namespace Impostor.Server.Net.Manager +{ + internal partial class ClientManager : IClientManager + { + IEnumerable<IClient> IClientManager.Clients => _clients.Values; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs new file mode 100644 index 0000000..51e22d8 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Manager/ClientManager.cs @@ -0,0 +1,103 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Config; +using Impostor.Server.Net.Factories; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Manager +{ + internal partial class ClientManager + { + private static HashSet<int> SupportedVersions { get; } = new HashSet<int> + { + GameVersion.GetVersion(2020, 09, 07), // 2020.09.07 - 2020.09.22 + GameVersion.GetVersion(2020, 10, 08), // 2020.10.08 + GameVersion.GetVersion(2020, 11, 17), // 2020.11.17 + }; + + private readonly ILogger<ClientManager> _logger; + private readonly ConcurrentDictionary<int, ClientBase> _clients; + private readonly IClientFactory _clientFactory; + private int _idLast; + + public ClientManager(ILogger<ClientManager> logger, IClientFactory clientFactory) + { + _logger = logger; + _clientFactory = clientFactory; + _clients = new ConcurrentDictionary<int, ClientBase>(); + } + + public IEnumerable<ClientBase> Clients => _clients.Values; + + public int NextId() + { + var clientId = Interlocked.Increment(ref _idLast); + + if (clientId < 1) + { + // Super rare but reset the _idLast because of overflow. + _idLast = 0; + + // And get a new id. + clientId = Interlocked.Increment(ref _idLast); + } + + return clientId; + } + + public async ValueTask RegisterConnectionAsync(IHazelConnection connection, string name, int clientVersion) + { + if (!SupportedVersions.Contains(clientVersion)) + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.IncorrectVersion); + await connection.SendAsync(packet); + return; + } + + if (name.Length > 10) + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.UsernameLength); + await connection.SendAsync(packet); + return; + } + + if (string.IsNullOrWhiteSpace(name) || !name.All(TextBox.IsCharAllowed)) + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.UsernameIllegalCharacters); + await connection.SendAsync(packet); + return; + } + + var client = _clientFactory.Create(connection, name, clientVersion); + var id = NextId(); + + client.Id = id; + _logger.LogTrace("Client connected."); + _clients.TryAdd(id, client); + } + + public void Remove(IClient client) + { + _logger.LogTrace("Client disconnected."); + _clients.TryRemove(client.Id, out _); + } + + public bool Validate(IClient client) + { + return client.Id != 0 + && _clients.TryGetValue(client.Id, out var registeredClient) + && ReferenceEquals(client, registeredClient); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs b/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs new file mode 100644 index 0000000..a37829e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Manager/GameManager.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Games.Managers; +using Impostor.Api.Innersloth; +using Impostor.Server.Config; +using Impostor.Server.Events; +using Impostor.Server.Net.Redirector; +using Impostor.Server.Net.State; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Manager +{ + internal class GameManager : IGameManager + { + private readonly ILogger<GameManager> _logger; + private readonly INodeLocator _nodeLocator; + private readonly IPEndPoint _publicIp; + private readonly ConcurrentDictionary<int, Game> _games; + private readonly IServiceProvider _serviceProvider; + private readonly IEventManager _eventManager; + private readonly IGameCodeFactory _gameCodeFactory; + + public GameManager(ILogger<GameManager> logger, IOptions<ServerConfig> config, INodeLocator nodeLocator, IServiceProvider serviceProvider, IEventManager eventManager, IGameCodeFactory gameCodeFactory) + { + _logger = logger; + _nodeLocator = nodeLocator; + _serviceProvider = serviceProvider; + _eventManager = eventManager; + _gameCodeFactory = gameCodeFactory; + _publicIp = new IPEndPoint(IPAddress.Parse(config.Value.ResolvePublicIp()), config.Value.PublicPort); + _games = new ConcurrentDictionary<int, Game>(); + } + + IEnumerable<IGame> IGameManager.Games => _games.Select(kv => kv.Value); + + IGame IGameManager.Find(GameCode code) => Find(code); + + public async ValueTask<IGame> CreateAsync(GameOptionsData options) + { + // TODO: Prevent duplicates when using server redirector using INodeProvider. + var (success, game) = await TryCreateAsync(options); + + for (int i = 0; i < 10 && !success; i++) + { + (success, game) = await TryCreateAsync(options); + } + + if (!success) + { + throw new ImpostorException("Could not create new game"); // TODO: Fix generic exception. + } + + return game; + } + + private async ValueTask<(bool success, Game game)> TryCreateAsync(GameOptionsData options) + { + var gameCode = _gameCodeFactory.Create(); + var gameCodeStr = gameCode.Code; + var game = ActivatorUtilities.CreateInstance<Game>(_serviceProvider, _publicIp, gameCode, options); + + if (await _nodeLocator.ExistsAsync(gameCodeStr) || !_games.TryAdd(gameCode, game)) + { + return (false, null); + } + + await _nodeLocator.SaveAsync(gameCodeStr, _publicIp); + _logger.LogDebug("Created game with code {0}.", game.Code); + + await _eventManager.CallAsync(new GameCreatedEvent(game)); + + return (true, game); + } + + public Game Find(GameCode code) + { + _games.TryGetValue(code, out var game); + return game; + } + + public IEnumerable<Game> FindListings(MapFlags map, int impostorCount, GameKeywords language, int count = 10) + { + var results = 0; + + // Find games that have not started yet. + foreach (var (_, game) in _games.Where(x => + x.Value.IsPublic && + x.Value.GameState == GameStates.NotStarted && + x.Value.PlayerCount < x.Value.Options.MaxPlayers)) + { + // Check for options. + if (!map.HasFlag((MapFlags)(1 << game.Options.MapId))) + { + continue; + } + + if (!language.HasFlag(game.Options.Keywords)) + { + continue; + } + + if (impostorCount != 0 && game.Options.NumImpostors != impostorCount) + { + continue; + } + + // Add to result. + yield return game; + + // Break out if we have enough. + if (++results == count) + { + yield break; + } + } + } + + public async ValueTask RemoveAsync(GameCode gameCode) + { + if (_games.TryGetValue(gameCode, out var game) && game.PlayerCount > 0) + { + foreach (var player in game.Players) + { + await player.KickAsync(); + } + + return; + } + + if (!_games.TryRemove(gameCode, out game)) + { + return; + } + + _logger.LogDebug("Remove game with code {0} ({1}).", GameCodeParser.IntToGameName(gameCode), gameCode); + await _nodeLocator.RemoveAsync(GameCodeParser.IntToGameName(gameCode)); + + await _eventManager.CallAsync(new GameDestroyedEvent(game)); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs b/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs new file mode 100644 index 0000000..64ece55 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Matchmaker.cs @@ -0,0 +1,66 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Impostor.Hazel; +using Impostor.Hazel.Udp; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Server.Net +{ + internal class Matchmaker + { + private readonly ClientManager _clientManager; + private readonly ObjectPool<MessageReader> _readerPool; + private readonly ILogger<Matchmaker> _logger; + private readonly ILogger<HazelConnection> _connectionLogger; + private UdpConnectionListener _connection; + + public Matchmaker( + ILogger<Matchmaker> logger, + ClientManager clientManager, + ObjectPool<MessageReader> readerPool, + ILogger<HazelConnection> connectionLogger) + { + _logger = logger; + _clientManager = clientManager; + _readerPool = readerPool; + _connectionLogger = connectionLogger; + } + + public async ValueTask StartAsync(IPEndPoint ipEndPoint) + { + var mode = ipEndPoint.AddressFamily switch + { + AddressFamily.InterNetwork => IPMode.IPv4, + AddressFamily.InterNetworkV6 => IPMode.IPv6, + _ => throw new InvalidOperationException() + }; + + _connection = new UdpConnectionListener(ipEndPoint, _readerPool, mode); + _connection.NewConnection = OnNewConnection; + + await _connection.StartAsync(); + } + + public async ValueTask StopAsync() + { + await _connection.DisposeAsync(); + } + + private async ValueTask OnNewConnection(NewConnectionEventArgs e) + { + // Handshake. + var clientVersion = e.HandshakeData.ReadInt32(); + var name = e.HandshakeData.ReadString(); + + var connection = new HazelConnection(e.Connection, _connectionLogger); + + // Register client + await _clientManager.RegisterConnectionAsync(connection, name, clientVersion); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs b/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs new file mode 100644 index 0000000..bd9a855 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/MatchmakerService.cs @@ -0,0 +1,57 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Config; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net +{ + internal class MatchmakerService : IHostedService + { + private readonly ILogger<MatchmakerService> _logger; + private readonly ServerConfig _serverConfig; + private readonly ServerRedirectorConfig _redirectorConfig; + private readonly Matchmaker _matchmaker; + + public MatchmakerService( + ILogger<MatchmakerService> logger, + IOptions<ServerConfig> serverConfig, + IOptions<ServerRedirectorConfig> redirectorConfig, + Matchmaker matchmaker) + { + _logger = logger; + _serverConfig = serverConfig.Value; + _redirectorConfig = redirectorConfig.Value; + _matchmaker = matchmaker; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + var endpoint = new IPEndPoint(IPAddress.Parse(_serverConfig.ResolveListenIp()), _serverConfig.ListenPort); + + await _matchmaker.StartAsync(endpoint); + + _logger.LogInformation( + "Matchmaker is listening on {0}:{1}, the public server ip is {2}:{3}.", + endpoint.Address, + endpoint.Port, + _serverConfig.ResolvePublicIp(), + _serverConfig.PublicPort); + + if (_redirectorConfig.Enabled) + { + _logger.LogWarning(_redirectorConfig.Master + ? "Server redirection is enabled as master, this instance will redirect clients to other nodes." + : "Server redirection is enabled as node, this instance will accept clients."); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + _logger.LogWarning("Matchmaker is shutting down!"); + await _matchmaker.StopAsync(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs b/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs new file mode 100644 index 0000000..707860e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Messages/MessageWriterProvider.cs @@ -0,0 +1,13 @@ +using Impostor.Api.Net.Messages; +using Impostor.Hazel; + +namespace Impostor.Server.Net.Messages +{ + public class MessageWriterProvider : IMessageWriterProvider + { + public IMessageWriter Get(MessageType sendOption = MessageType.Unreliable) + { + return MessageWriter.Get(sendOption); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs new file mode 100644 index 0000000..3dad4b3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/ClientRedirector.cs @@ -0,0 +1,97 @@ +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Config; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Serilog; +using ILogger = Serilog.ILogger; + +namespace Impostor.Server.Net.Redirector +{ + internal class ClientRedirector : ClientBase + { + private static readonly ILogger Logger = Log.ForContext<ClientRedirector>(); + + private readonly ClientManager _clientManager; + private readonly INodeProvider _nodeProvider; + private readonly INodeLocator _nodeLocator; + + public ClientRedirector( + string name, + HazelConnection connection, + ClientManager clientManager, + INodeProvider nodeProvider, + INodeLocator nodeLocator) + : base(name, connection) + { + _clientManager = clientManager; + _nodeProvider = nodeProvider; + _nodeLocator = nodeLocator; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + var flag = reader.Tag; + + Logger.Verbose("Server got {0}.", flag); + + switch (flag) + { + case MessageFlags.HostGame: + { + using var packet = MessageWriter.Get(MessageType.Reliable); + Message13RedirectS2C.Serialize(packet, false, _nodeProvider.Get()); + await Connection.SendAsync(packet); + break; + } + + case MessageFlags.JoinGame: + { + Message01JoinGameC2S.Deserialize( + reader, + out var gameCode, + out _); + + using var packet = MessageWriter.Get(MessageType.Reliable); + var endpoint = await _nodeLocator.FindAsync(GameCodeParser.IntToGameName(gameCode)); + if (endpoint == null) + { + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.GameMissing); + } + else + { + Message13RedirectS2C.Serialize(packet, false, endpoint); + } + + await Connection.SendAsync(packet); + break; + } + + case MessageFlags.GetGameListV2: + { + // TODO: Implement. + using var packet = MessageWriter.Get(MessageType.Reliable); + Message01JoinGameS2C.SerializeError(packet, false, DisconnectReason.Custom, DisconnectMessages.NotImplemented); + await Connection.SendAsync(packet); + break; + } + + default: + { + Logger.Warning("Received unsupported message flag on the redirector ({0}).", flag); + break; + } + } + } + + public override ValueTask HandleDisconnectAsync(string reason) + { + _clientManager.Remove(this); + return default; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs new file mode 100644 index 0000000..12563b9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeLocator.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Redirector +{ + public interface INodeLocator + { + ValueTask<IPEndPoint> FindAsync(string gameCode); + + ValueTask SaveAsync(string gameCode, IPEndPoint endPoint); + + ValueTask RemoveAsync(string gameCode); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs new file mode 100644 index 0000000..318fcb2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/INodeProvider.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace Impostor.Server.Net.Redirector +{ + internal interface INodeProvider + { + IPEndPoint Get(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs new file mode 100644 index 0000000..fd4cd56 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorNoOp.cs @@ -0,0 +1,14 @@ +using System.Net; +using System.Threading.Tasks; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorNoOp : INodeLocator + { + public ValueTask<IPEndPoint> FindAsync(string gameCode) => ValueTask.FromResult(default(IPEndPoint)); + + public ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) => ValueTask.CompletedTask; + + public ValueTask RemoveAsync(string gameCode) => ValueTask.CompletedTask; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs new file mode 100644 index 0000000..0b6fdff --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorRedis.cs @@ -0,0 +1,43 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorRedis : INodeLocator + { + private readonly IDistributedCache _cache; + + public NodeLocatorRedis(ILogger<NodeLocatorRedis> logger, IDistributedCache cache) + { + logger.LogWarning("Using the redis NodeLocator."); + _cache = cache; + } + + public async ValueTask<IPEndPoint> FindAsync(string gameCode) + { + var entry = await _cache.GetStringAsync(gameCode); + if (entry == null) + { + return null; + } + + return IPEndPoint.Parse(entry); + } + + public async ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) + { + await _cache.SetStringAsync(gameCode, endPoint.ToString(), new DistributedCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromHours(1), + }); + } + + public async ValueTask RemoveAsync(string gameCode) + { + await _cache.RemoveAsync(gameCode); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs new file mode 100644 index 0000000..2539a8f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDP.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; +using Impostor.Server.Config; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUdp : INodeLocator, IDisposable + { + private readonly ILogger<NodeLocatorUdp> _logger; + private readonly bool _isMaster; + private readonly IPEndPoint _server; + private readonly UdpClient _client; + private readonly ConcurrentDictionary<string, AvailableNode> _availableNodes; + + public NodeLocatorUdp(ILogger<NodeLocatorUdp> logger, IOptions<ServerRedirectorConfig> config) + { + _logger = logger; + + if (config.Value.Master) + { + _isMaster = true; + _availableNodes = new ConcurrentDictionary<string, AvailableNode>(); + } + else + { + _isMaster = false; + + if (!IPEndPoint.TryParse(config.Value.Locator.UdpMasterEndpoint, out var endpoint)) + { + throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format."); + } + + _logger.LogWarning("Node server will send updates to {0}.", endpoint); + _server = endpoint; + _client = new UdpClient(); + + try + { + _client.DontFragment = true; + } + catch (SocketException) + { + } + } + } + + public void Update(IPEndPoint ip, string gameCode) + { + _logger.LogDebug("Received update {0} -> {1}", gameCode, ip); + + _availableNodes.AddOrUpdate( + gameCode, + s => new AvailableNode + { + Endpoint = ip, + LastUpdated = DateTimeOffset.UtcNow, + }, + (s, node) => + { + node.Endpoint = ip; + node.LastUpdated = DateTimeOffset.UtcNow; + + return node; + }); + + foreach (var (key, value) in _availableNodes) + { + if (value.Expired) + { + _availableNodes.TryRemove(key, out _); + } + } + } + + public ValueTask<IPEndPoint> FindAsync(string gameCode) + { + if (!_isMaster) + { + return ValueTask.FromResult(default(IPEndPoint)); + } + + if (_availableNodes.TryGetValue(gameCode, out var node)) + { + if (node.Expired) + { + _availableNodes.TryRemove(gameCode, out _); + return ValueTask.FromResult(default(IPEndPoint)); + } + + return ValueTask.FromResult(node.Endpoint); + } + + return ValueTask.FromResult(default(IPEndPoint)); + } + + public ValueTask RemoveAsync(string gameCode) + { + if (!_isMaster) + { + return ValueTask.CompletedTask; + } + + _availableNodes.TryRemove(gameCode, out _); + return ValueTask.CompletedTask; + } + + public ValueTask SaveAsync(string gameCode, IPEndPoint endPoint) + { + var data = Encoding.UTF8.GetBytes($"{gameCode},{endPoint}"); + _client.Send(data, data.Length, _server); + return ValueTask.CompletedTask; + } + + public void Dispose() + { + _client?.Dispose(); + } + + private class AvailableNode + { + public IPEndPoint Endpoint { get; set; } + + public DateTimeOffset LastUpdated { get; set; } + + public bool Expired => LastUpdated < DateTimeOffset.UtcNow.AddHours(-1); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs new file mode 100644 index 0000000..3706bb4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeLocatorUDPService.cs @@ -0,0 +1,101 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Server.Config; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + public class NodeLocatorUdpService : BackgroundService + { + private readonly NodeLocatorUdp _nodeLocator; + private readonly ILogger<NodeLocatorUdpService> _logger; + private readonly UdpClient _client; + + public NodeLocatorUdpService( + INodeLocator nodeLocator, + ILogger<NodeLocatorUdpService> logger, + IOptions<ServerRedirectorConfig> options) + { + _nodeLocator = (NodeLocatorUdp)nodeLocator; + _logger = logger; + + if (!IPEndPoint.TryParse(options.Value.Locator.UdpMasterEndpoint, out var endpoint)) + { + throw new ArgumentException("UdpMasterEndpoint should be in the ip:port format."); + } + + _client = new UdpClient(endpoint); + + try + { + _client.DontFragment = true; + } + catch (SocketException) + { + } + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogWarning("Master server is listening for node updates on {0}.", _client.Client.LocalEndPoint); + + stoppingToken.Register(() => + { + _client.Close(); + _client.Dispose(); + }); + + try + { + while (!stoppingToken.IsCancellationRequested) + { + // Receive data from a node. + UdpReceiveResult data; + + try + { + data = await _client.ReceiveAsync(); + } + catch (ObjectDisposedException) + { + break; + } + + // Check if data is valid. + if (data.Buffer.Length == 0) + { + break; + } + + // Parse the data. + var message = Encoding.UTF8.GetString(data.Buffer); + var parts = message.Split(',', 2); + if (parts.Length != 2) + { + continue; + } + + if (!IPEndPoint.TryParse(parts[1], out var ipEndPoint)) + { + continue; + } + + // Update the NodeLocator. + _nodeLocator.Update(ipEndPoint, parts[0]); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error in NodeLocatorUDPService."); + } + + _logger.LogWarning("Master server node update listener is stopping."); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs new file mode 100644 index 0000000..4e19c43 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/Redirector/NodeProviderConfig.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Net; +using Impostor.Server.Config; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Net.Redirector +{ + internal class NodeProviderConfig : INodeProvider + { + private readonly List<IPEndPoint> _nodes; + private readonly object _lock; + private int _currentIndex; + + public NodeProviderConfig(IOptions<ServerRedirectorConfig> redirectorConfig) + { + _nodes = new List<IPEndPoint>(); + _lock = new object(); + + if (redirectorConfig.Value.Nodes != null) + { + foreach (var node in redirectorConfig.Value.Nodes) + { + _nodes.Add(new IPEndPoint(IPAddress.Parse(node.Ip), node.Port)); + } + } + } + + public IPEndPoint Get() + { + lock (_lock) + { + var node = _nodes[_currentIndex++]; + + if (_currentIndex == _nodes.Count) + { + _currentIndex = 0; + } + + return node; + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs new file mode 100644 index 0000000..2e2467e --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.Api.cs @@ -0,0 +1,18 @@ +using Impostor.Api.Games; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.State +{ + internal partial class ClientPlayer + { + /// <inheritdoc /> + IClient IClientPlayer.Client => Client; + + /// <inheritdoc /> + IGame IClientPlayer.Game => Game; + + /// <inheritdoc /> + IInnerPlayerControl? IClientPlayer.Character => Character; + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs new file mode 100644 index 0000000..107edbf --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/ClientPlayer.cs @@ -0,0 +1,88 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Server.Net.Inner.Objects; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class ClientPlayer : IClientPlayer + { + private readonly ILogger<ClientPlayer> _logger; + private readonly Timer _spawnTimeout; + + public ClientPlayer(ILogger<ClientPlayer> logger, ClientBase client, Game game) + { + _logger = logger; + _spawnTimeout = new Timer(RunSpawnTimeout, null, -1, -1); + + Game = game; + Client = client; + Limbo = LimboStates.PreSpawn; + } + + public ClientBase Client { get; } + + public Game Game { get; } + + /// <inheritdoc /> + public LimboStates Limbo { get; set; } + + public InnerPlayerControl? Character { get; internal set; } + + public bool IsHost => Game?.Host == this; + + public string Scene { get; internal set; } + + public void InitializeSpawnTimeout() + { + _spawnTimeout.Change(Constants.SpawnTimeout, -1); + } + + public void DisableSpawnTimeout() + { + _spawnTimeout.Change(-1, -1); + } + + /// <inheritdoc /> + public bool IsOwner(IInnerNetObject netObject) + { + return Client.Id == netObject.OwnerId; + } + + /// <inheritdoc /> + public ValueTask KickAsync() + { + return Game.HandleKickPlayer(Client.Id, false); + } + + /// <inheritdoc /> + public ValueTask BanAsync() + { + return Game.HandleKickPlayer(Client.Id, true); + } + + private async void RunSpawnTimeout(object state) + { + try + { + if (Character == null) + { + _logger.LogInformation("{0} - Player {1} spawn timed out, kicking.", Game.Code, Client.Id); + + await KickAsync(); + } + } + catch (Exception e) + { + _logger.LogError(e, "Exception caught while kicking player for spawn timeout."); + } + finally + { + await _spawnTimeout.DisposeAsync(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs new file mode 100644 index 0000000..f395be2 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Api.cs @@ -0,0 +1,70 @@ +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; +using Impostor.Server.Net.Inner; + +namespace Impostor.Server.Net.State +{ + internal partial class Game : IGame + { + IClientPlayer IGame.Host => Host; + + IGameNet IGame.GameNet => GameNet; + + public void BanIp(IPAddress ipAddress) + { + _bannedIps.Add(ipAddress); + } + + public async ValueTask SyncSettingsAsync() + { + if (Host.Character == null) + { + throw new ImpostorException("Attempted to set infected when the host was not spawned."); + } + + using (var writer = StartRpc(Host.Character.NetId, RpcCalls.SyncSettings)) + { + // Someone will probably forget to do this, so we include it here. + // If this is not done, the host will overwrite changes later with the defaults. + Options.IsDefaults = false; + + await using (var memory = new MemoryStream()) + await using (var writerBin = new BinaryWriter(memory)) + { + Options.Serialize(writerBin, GameOptionsData.LatestVersion); + writer.WriteBytesAndSize(memory.ToArray()); + } + + await FinishRpcAsync(writer); + } + } + + public async ValueTask SetInfectedAsync(IEnumerable<IInnerPlayerControl> players) + { + if (Host.Character == null) + { + throw new ImpostorException("Attempted to set infected when the host was not spawned."); + } + + using (var writer = StartRpc(Host.Character.NetId, RpcCalls.SetInfected)) + { + writer.Write((byte)Host.Character.NetId); + + foreach (var player in players) + { + writer.Write((byte)player.PlayerId); + } + + await FinishRpcAsync(writer); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs new file mode 100644 index 0000000..a84d9b5 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Data.cs @@ -0,0 +1,449 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Events.Meeting; +using Impostor.Server.Events.Player; +using Impostor.Server.Net.Inner; +using Impostor.Server.Net.Inner.Objects; +using Impostor.Server.Net.Inner.Objects.Components; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private const int FakeClientId = int.MaxValue - 1; + + /// <summary> + /// Used for global object, spawned by the host. + /// </summary> + private const int InvalidClient = -2; + + /// <summary> + /// Used internally to set the OwnerId to the current ClientId. + /// i.e: <code>ownerId = ownerId == -3 ? this.ClientId : ownerId;</code> + /// </summary> + private const int CurrentClient = -3; + + private static readonly Type[] SpawnableObjects = + { + typeof(InnerShipStatus), // ShipStatus + typeof(InnerMeetingHud), + typeof(InnerLobbyBehaviour), + typeof(InnerGameData), + typeof(InnerPlayerControl), + typeof(InnerShipStatus), // HeadQuarters + typeof(InnerShipStatus), // PlanetMap + typeof(InnerShipStatus), // AprilShipStatus + }; + + private readonly List<InnerNetObject> _allObjects = new List<InnerNetObject>(); + private readonly Dictionary<uint, InnerNetObject> _allObjectsFast = new Dictionary<uint, InnerNetObject>(); + + private int _gamedataInitialized; + private bool _gamedataFakeReceived; + + private async ValueTask OnSpawnAsync(InnerNetObject netObj) + { + switch (netObj) + { + case InnerLobbyBehaviour lobby: + { + GameNet.LobbyBehaviour = lobby; + break; + } + + case InnerGameData data: + { + GameNet.GameData = data; + break; + } + + case InnerVoteBanSystem voteBan: + { + GameNet.VoteBan = voteBan; + break; + } + + case InnerShipStatus shipStatus: + { + GameNet.ShipStatus = shipStatus; + break; + } + + case InnerPlayerControl control: + { + // Hook up InnerPlayerControl <-> IClientPlayer. + if (!TryGetPlayer(control.OwnerId, out var player)) + { + throw new ImpostorException("Failed to find player that spawned the InnerPlayerControl"); + } + + player.Character = control; + player.DisableSpawnTimeout(); + + // Hook up InnerPlayerControl <-> InnerPlayerControl.PlayerInfo. + control.PlayerInfo = GameNet.GameData.GetPlayerById(control.PlayerId)!; + + if (control.PlayerInfo == null) + { + GameNet.GameData.AddPlayer(control); + } + + if (control.PlayerInfo != null) + { + control.PlayerInfo!.Controller = control; + } + + await _eventManager.CallAsync(new PlayerSpawnedEvent(this, player, control)); + + break; + } + + case InnerMeetingHud meetingHud: + { + await _eventManager.CallAsync(new MeetingStartedEvent(this, meetingHud)); + break; + } + } + } + + private async ValueTask OnDestroyAsync(InnerNetObject netObj) + { + switch (netObj) + { + case InnerLobbyBehaviour: + { + GameNet.LobbyBehaviour = null; + break; + } + + case InnerGameData: + { + GameNet.GameData = null; + break; + } + + case InnerVoteBanSystem: + { + GameNet.VoteBan = null; + break; + } + + case InnerShipStatus: + { + GameNet.ShipStatus = null; + break; + } + + case InnerPlayerControl control: + { + // Remove InnerPlayerControl <-> IClientPlayer. + if (TryGetPlayer(control.OwnerId, out var player)) + { + player.Character = null; + } + + await _eventManager.CallAsync(new PlayerDestroyedEvent(this, player, control)); + + break; + } + } + } + + private async ValueTask InitGameDataAsync(ClientPlayer player) + { + if (Interlocked.Exchange(ref _gamedataInitialized, 1) != 0) + { + return; + } + + /* + * The Among Us client on 20.9.22i spawns some components on the host side and + * only spawns these on other clients when someone else connects. This means that we can't + * parse data until someone connects because we don't know which component belongs to the NetId. + * + * We solve this by spawning a fake player and removing the player when the spawn GameData + * is received in HandleGameDataAsync. + */ + using (var message = MessageWriter.Get(MessageType.Reliable)) + { + // Spawn a fake player. + Message01JoinGameS2C.SerializeJoin(message, false, Code, FakeClientId, HostId); + + message.StartMessage(MessageFlags.GameData); + message.Write(Code); + message.StartMessage(GameDataTag.SceneChangeFlag); + message.WritePacked(FakeClientId); + message.Write("OnlineGame"); + message.EndMessage(); + message.EndMessage(); + + await player.Client.Connection.SendAsync(message); + } + } + + public async ValueTask<bool> HandleGameDataAsync(IMessageReader parent, ClientPlayer sender, bool toPlayer) + { + // Find target player. + ClientPlayer target = null; + + if (toPlayer) + { + var targetId = parent.ReadPackedInt32(); + if (targetId == FakeClientId && !_gamedataFakeReceived && sender.IsHost) + { + _gamedataFakeReceived = true; + + // Remove the fake client, we received the data. + using (var message = MessageWriter.Get(MessageType.Reliable)) + { + WriteRemovePlayerMessage(message, false, FakeClientId, (byte)DisconnectReason.ExitGame); + + await sender.Client.Connection.SendAsync(message); + } + } + else if (!TryGetPlayer(targetId, out target)) + { + _logger.LogWarning("Player {0} tried to send GameData to unknown player {1}.", sender.Client.Id, targetId); + return false; + } + + _logger.LogTrace("Received GameData for target {0}.", targetId); + } + + // Parse GameData messages. + while (parent.Position < parent.Length) + { + using var reader = parent.ReadMessage(); + + switch (reader.Tag) + { + case GameDataTag.DataFlag: + { + var netId = reader.ReadPackedUInt32(); + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + obj.Deserialize(sender, target, reader, false); + } + else + { + _logger.LogWarning("Received DataFlag for unregistered NetId {0}.", netId); + } + + break; + } + + case GameDataTag.RpcFlag: + { + var netId = reader.ReadPackedUInt32(); + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + await obj.HandleRpc(sender, target, (RpcCalls) reader.ReadByte(), reader); + } + else + { + _logger.LogWarning("Received RpcFlag for unregistered NetId {0}.", netId); + } + + break; + } + + case GameDataTag.SpawnFlag: + { + // Only the host is allowed to despawn objects. + if (!sender.IsHost) + { + throw new ImpostorCheatException("Tried to send SpawnFlag as non-host."); + } + + var objectId = reader.ReadPackedUInt32(); + if (objectId < SpawnableObjects.Length) + { + var innerNetObject = (InnerNetObject) ActivatorUtilities.CreateInstance(_serviceProvider, SpawnableObjects[objectId], this); + var ownerClientId = reader.ReadPackedInt32(); + + // Prevent fake client from being broadcasted. + // TODO: Remove message from stream properly. + if (ownerClientId == FakeClientId) + { + return false; + } + + innerNetObject.SpawnFlags = (SpawnFlags) reader.ReadByte(); + + var components = innerNetObject.GetComponentsInChildren<InnerNetObject>(); + var componentsCount = reader.ReadPackedInt32(); + + if (componentsCount != components.Count) + { + _logger.LogError( + "Children didn't match for spawnable {0}, name {1} ({2} != {3})", + objectId, + innerNetObject.GetType().Name, + componentsCount, + components.Count); + continue; + } + + _logger.LogDebug( + "Spawning {0} components, SpawnFlags {1}", + innerNetObject.GetType().Name, + innerNetObject.SpawnFlags); + + for (var i = 0; i < componentsCount; i++) + { + var obj = components[i]; + + obj.NetId = reader.ReadPackedUInt32(); + obj.OwnerId = ownerClientId; + + _logger.LogDebug( + "- {0}, NetId {1}, OwnerId {2}", + obj.GetType().Name, + obj.NetId, + obj.OwnerId); + + if (!AddNetObject(obj)) + { + _logger.LogTrace("Failed to AddNetObject, it already exists."); + + obj.NetId = uint.MaxValue; + break; + } + + using var readerSub = reader.ReadMessage(); + if (readerSub.Length > 0) + { + obj.Deserialize(sender, target, readerSub, true); + } + + await OnSpawnAsync(obj); + } + + continue; + } + + _logger.LogError("Couldn't find spawnable object {0}.", objectId); + break; + } + + // Only the host is allowed to despawn objects. + case GameDataTag.DespawnFlag: + { + var netId = reader.ReadPackedUInt32(); + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + if (sender.Client.Id != obj.OwnerId && !sender.IsHost) + { + _logger.LogWarning( + "Player {0} ({1}) tried to send DespawnFlag for {2} but was denied.", + sender.Client.Name, + sender.Client.Id, + netId); + return false; + } + + RemoveNetObject(obj); + await OnDestroyAsync(obj); + _logger.LogDebug("Destroyed InnerNetObject {0} ({1}), OwnerId {2}", obj.GetType().Name, netId, obj.OwnerId); + } + else + { + _logger.LogDebug( + "Player {0} ({1}) sent DespawnFlag for unregistered NetId {2}.", + sender.Client.Name, + sender.Client.Id, + netId); + } + + break; + } + + case GameDataTag.SceneChangeFlag: + { + // Sender is only allowed to change his own scene. + var clientId = reader.ReadPackedInt32(); + if (clientId != sender.Client.Id) + { + _logger.LogWarning( + "Player {0} ({1}) tried to send SceneChangeFlag for another player.", + sender.Client.Name, + sender.Client.Id); + return false; + } + + sender.Scene = reader.ReadString(); + + _logger.LogTrace("> Scene {0} to {1}", clientId, sender.Scene); + break; + } + + case GameDataTag.ReadyFlag: + { + var clientId = reader.ReadPackedInt32(); + _logger.LogTrace("> IsReady {0}", clientId); + break; + } + + default: + { + _logger.LogTrace("Bad GameData tag {0}", reader.Tag); + break; + } + } + + if (sender.Client.Player == null) + { + // Disconnect handler was probably invoked, cancel the rest. + return false; + } + } + + return true; + } + + private bool AddNetObject(InnerNetObject obj) + { + if (_allObjectsFast.ContainsKey(obj.NetId)) + { + return false; + } + + _allObjects.Add(obj); + _allObjectsFast.Add(obj.NetId, obj); + return true; + } + + private void RemoveNetObject(InnerNetObject obj) + { + var index = _allObjects.IndexOf(obj); + if (index > -1) + { + _allObjects.RemoveAt(index); + } + + _allObjectsFast.Remove(obj.NetId); + + obj.NetId = uint.MaxValue; + } + + public T FindObjectByNetId<T>(uint netId) + where T : InnerNetObject + { + if (_allObjectsFast.TryGetValue(netId, out var obj)) + { + return (T) obj; + } + + return null; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs new file mode 100644 index 0000000..4bf1c43 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Incoming.cs @@ -0,0 +1,240 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Impostor.Server.Events; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private readonly SemaphoreSlim _clientAddLock = new SemaphoreSlim(1, 1); + + public async ValueTask HandleStartGame(IMessageReader message) + { + GameState = GameStates.Starting; + + using var packet = MessageWriter.Get(MessageType.Reliable); + message.CopyTo(packet); + await SendToAllAsync(packet); + + await _eventManager.CallAsync(new GameStartingEvent(this)); + } + + public async ValueTask<GameJoinResult> AddClientAsync(ClientBase client) + { + var hasLock = false; + + try + { + hasLock = await _clientAddLock.WaitAsync(TimeSpan.FromMinutes(1)); + + if (hasLock) + { + return await AddClientSafeAsync(client); + } + } + finally + { + if (hasLock) + { + _clientAddLock.Release(); + } + } + + return GameJoinResult.FromError(GameJoinError.InvalidClient); + } + + private async ValueTask<GameJoinResult> AddClientSafeAsync(ClientBase client) + { + // Check if the IP of the player is banned. + if (client.Connection != null && _bannedIps.Contains(client.Connection.EndPoint.Address)) + { + return GameJoinResult.FromError(GameJoinError.Banned); + } + + var player = client.Player; + + // Check if; + // - The player is already in this game. + // - The game is full. + if (player?.Game != this && _players.Count >= Options.MaxPlayers) + { + return GameJoinResult.FromError(GameJoinError.GameFull); + } + + if (GameState == GameStates.Starting || GameState == GameStates.Started) + { + return GameJoinResult.FromError(GameJoinError.GameStarted); + } + + if (GameState == GameStates.Destroyed) + { + return GameJoinResult.FromError(GameJoinError.GameDestroyed); + } + + var isNew = false; + + if (player == null || player.Game != this) + { + var clientPlayer = new ClientPlayer(_serviceProvider.GetRequiredService<ILogger<ClientPlayer>>(), client, this); + + if (!_clientManager.Validate(client)) + { + return GameJoinResult.FromError(GameJoinError.InvalidClient); + } + + isNew = true; + player = clientPlayer; + client.Player = clientPlayer; + } + + // Check current player state. + if (player.Limbo == LimboStates.NotLimbo) + { + return GameJoinResult.FromError(GameJoinError.InvalidLimbo); + } + + if (GameState == GameStates.Ended) + { + await HandleJoinGameNext(player, isNew); + return GameJoinResult.CreateSuccess(player); + } + + await HandleJoinGameNew(player, isNew); + return GameJoinResult.CreateSuccess(player); + } + + public async ValueTask HandleEndGame(IMessageReader message, GameOverReason gameOverReason) + { + GameState = GameStates.Ended; + + // Broadcast end of the game. + using (var packet = MessageWriter.Get(MessageType.Reliable)) + { + message.CopyTo(packet); + await SendToAllAsync(packet); + } + + // Put all players in the correct limbo state. + foreach (var player in _players) + { + player.Value.Limbo = LimboStates.PreSpawn; + } + + await _eventManager.CallAsync(new GameEndedEvent(this, gameOverReason)); + } + + public async ValueTask HandleAlterGame(IMessageReader message, IClientPlayer sender, bool isPublic) + { + IsPublic = isPublic; + + using var packet = MessageWriter.Get(MessageType.Reliable); + message.CopyTo(packet); + await SendToAllExceptAsync(packet, sender.Client.Id); + + await _eventManager.CallAsync(new GameAlterEvent(this, isPublic)); + } + + public async ValueTask HandleRemovePlayer(int playerId, DisconnectReason reason) + { + await PlayerRemove(playerId); + + // It's possible that the last player was removed, so check if the game is still around. + if (GameState == GameStates.Destroyed) + { + return; + } + + using var packet = MessageWriter.Get(MessageType.Reliable); + WriteRemovePlayerMessage(packet, false, playerId, reason); + await SendToAllExceptAsync(packet, playerId); + } + + public async ValueTask HandleKickPlayer(int playerId, bool isBan) + { + _logger.LogInformation("{0} - Player {1} has left.", Code, playerId); + + using var message = MessageWriter.Get(MessageType.Reliable); + + // Send message to everyone that this player was kicked. + WriteKickPlayerMessage(message, false, playerId, isBan); + + await SendToAllAsync(message); + await PlayerRemove(playerId, isBan); + + // Remove the player from everyone's game. + WriteRemovePlayerMessage( + message, + true, + playerId, + isBan ? DisconnectReason.Banned : DisconnectReason.Kicked); + + await SendToAllExceptAsync(message, playerId); + } + + private async ValueTask HandleJoinGameNew(ClientPlayer sender, bool isNew) + { + _logger.LogInformation("{0} - Player {1} ({2}) is joining.", Code, sender.Client.Name, sender.Client.Id); + + // Add player to the game. + if (isNew) + { + await PlayerAdd(sender); + } + + sender.InitializeSpawnTimeout(); + + using (var message = MessageWriter.Get(MessageType.Reliable)) + { + WriteJoinedGameMessage(message, false, sender); + WriteAlterGameMessage(message, false, IsPublic); + + sender.Limbo = LimboStates.NotLimbo; + + await SendToAsync(message, sender.Client.Id); + await BroadcastJoinMessage(message, true, sender); + } + } + + private async ValueTask HandleJoinGameNext(ClientPlayer sender, bool isNew) + { + _logger.LogInformation("{0} - Player {1} ({2}) is rejoining.", Code, sender.Client.Name, sender.Client.Id); + + // Add player to the game. + if (isNew) + { + await PlayerAdd(sender); + } + + // Check if the host joined and let everyone join. + if (sender.Client.Id == HostId) + { + GameState = GameStates.NotStarted; + + // Spawn the host. + await HandleJoinGameNew(sender, false); + + // Pull players out of limbo. + await CheckLimboPlayers(); + return; + } + + sender.Limbo = LimboStates.WaitingForHost; + + using (var packet = MessageWriter.Get(MessageType.Reliable)) + { + WriteWaitForHostMessage(packet, false, sender); + + await SendToAsync(packet, sender.Client.Id); + await BroadcastJoinMessage(packet, true, sender); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs new file mode 100644 index 0000000..21b6067 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.Outgoing.cs @@ -0,0 +1,103 @@ +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Hazel; +using Impostor.Server.Net.Inner; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + public async ValueTask SendToAllAsync(IMessageWriter writer, LimboStates states = LimboStates.NotLimbo) + { + foreach (var connection in GetConnections(x => x.Limbo.HasFlag(states))) + { + await connection.SendAsync(writer); + } + } + + public async ValueTask SendToAllExceptAsync(IMessageWriter writer, int senderId, LimboStates states = LimboStates.NotLimbo) + { + foreach (var connection in GetConnections(x => + x.Limbo.HasFlag(states) && + x.Client.Id != senderId)) + { + await connection.SendAsync(writer); + } + } + + public async ValueTask SendToAsync(IMessageWriter writer, int id) + { + if (TryGetPlayer(id, out var player)) + { + await player.Client.Connection.SendAsync(writer); + } + } + + internal IMessageWriter StartRpc(uint targetNetId, RpcCalls callId, int targetClientId = -1, MessageType type = MessageType.Reliable) + { + var writer = MessageWriter.Get(type); + + if (targetClientId < 0) + { + writer.StartMessage(MessageFlags.GameData); + writer.Write(Code); + } + else + { + writer.StartMessage(MessageFlags.GameDataTo); + writer.Write(Code); + writer.WritePacked(targetClientId); + } + + writer.StartMessage(GameDataTag.RpcFlag); + writer.WritePacked(targetNetId); + writer.Write((byte) callId); + + return writer; + } + + internal ValueTask FinishRpcAsync(IMessageWriter writer, int? targetClientId = null) + { + writer.EndMessage(); + writer.EndMessage(); + + return targetClientId.HasValue + ? SendToAsync(writer, targetClientId.Value) + : SendToAllAsync(writer); + } + + private void WriteRemovePlayerMessage(IMessageWriter message, bool clear, int playerId, DisconnectReason reason) + { + Message04RemovePlayerS2C.Serialize(message, clear, Code, playerId, HostId, reason); + } + + private void WriteJoinedGameMessage(IMessageWriter message, bool clear, IClientPlayer player) + { + var playerIds = _players + .Where(x => x.Value != player) + .Select(x => x.Key) + .ToArray(); + + Message07JoinedGameS2C.Serialize(message, clear, Code, player.Client.Id, HostId, playerIds); + } + + private void WriteAlterGameMessage(IMessageWriter message, bool clear, bool isPublic) + { + Message10AlterGameS2C.Serialize(message, clear, Code, isPublic); + } + + private void WriteKickPlayerMessage(IMessageWriter message, bool clear, int playerId, bool isBan) + { + Message11KickPlayerS2C.Serialize(message, clear, Code, playerId, isBan); + } + + private void WriteWaitForHostMessage(IMessageWriter message, bool clear, IClientPlayer player) + { + Message12WaitForHostS2C.Serialize(message, clear, Code, player.Client.Id); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs new file mode 100644 index 0000000..e311776 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.State.cs @@ -0,0 +1,136 @@ +using System.Linq; +using System.Threading.Tasks; +using Impostor.Api; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Impostor.Server.Events; +using Impostor.Server.Net.Hazel; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private async ValueTask PlayerAdd(ClientPlayer player) + { + // Store player. + if (!_players.TryAdd(player.Client.Id, player)) + { + throw new ImpostorException("Failed to add player to game."); + } + + // Assign hostId if none is set. + if (HostId == -1) + { + HostId = player.Client.Id; + await InitGameDataAsync(player); + } + + await _eventManager.CallAsync(new GamePlayerJoinedEvent(this, player)); + } + + private async ValueTask<bool> PlayerRemove(int playerId, bool isBan = false) + { + if (!_players.TryRemove(playerId, out var player)) + { + return false; + } + + _logger.LogInformation("{0} - Player {1} ({2}) has left.", Code, player.Client.Name, playerId); + + if (GameState == GameStates.Starting || GameState == GameStates.Started) + { + if (player.Character?.PlayerInfo != null) + { + player.Character.PlayerInfo.Disconnected = true; + player.Character.PlayerInfo.LastDeathReason = DeathReason.Disconnect; + } + } + + player.Client.Player = null; + + // Game is empty, remove it. + if (_players.IsEmpty) + { + GameState = GameStates.Destroyed; + + // Remove instance reference. + await _gameManager.RemoveAsync(Code); + return true; + } + + // Host migration. + if (HostId == playerId) + { + await MigrateHost(); + } + + if (isBan && player.Client.Connection != null) + { + BanIp(player.Client.Connection.EndPoint.Address); + } + + await _eventManager.CallAsync(new GamePlayerLeftEvent(this, player, isBan)); + + // Player can refuse to be kicked and keep the connection open, check for this. + _ = Task.Run(async () => + { + await Task.Delay(Constants.ConnectionTimeout); + + if (player.Client.Connection.IsConnected && player.Client.Connection is HazelConnection hazel) + { + _logger.LogInformation("{0} - Player {1} ({2}) kept connection open after leaving, disposing.", Code, player.Client.Name, playerId); + hazel.DisposeInnerConnection(); + } + }); + + return true; + } + + private async ValueTask MigrateHost() + { + // Pick the first player as new host. + var host = _players + .Select(p => p.Value) + .FirstOrDefault(); + + if (host == null) + { + await EndAsync(); + return; + } + + HostId = host.Client.Id; + _logger.LogInformation("{0} - Assigned {1} ({2}) as new host.", Code, host.Client.Name, host.Client.Id); + + // Check our current game state. + if (GameState == GameStates.Ended && host.Limbo == LimboStates.WaitingForHost) + { + GameState = GameStates.NotStarted; + + // Spawn the host. + await HandleJoinGameNew(host, false); + + // Pull players out of limbo. + await CheckLimboPlayers(); + } + } + + private async ValueTask CheckLimboPlayers() + { + using var message = MessageWriter.Get(MessageType.Reliable); + + foreach (var (_, player) in _players.Where(x => x.Value.Limbo == LimboStates.WaitingForHost)) + { + WriteJoinedGameMessage(message, true, player); + WriteAlterGameMessage(message, false, IsPublic); + + player.Limbo = LimboStates.NotLimbo; + + await SendToAsync(message, player.Client.Id); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/State/Game.cs b/Impostor-dev/src/Impostor.Server/Net/State/Game.cs new file mode 100644 index 0000000..1919867 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/Game.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.S2C; +using Impostor.Server.Events; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Net.State +{ + internal partial class Game + { + private readonly ILogger<Game> _logger; + private readonly IServiceProvider _serviceProvider; + private readonly GameManager _gameManager; + private readonly ClientManager _clientManager; + private readonly ConcurrentDictionary<int, ClientPlayer> _players; + private readonly HashSet<IPAddress> _bannedIps; + private readonly IEventManager _eventManager; + + public Game( + ILogger<Game> logger, + IServiceProvider serviceProvider, + GameManager gameManager, + IPEndPoint publicIp, + GameCode code, + GameOptionsData options, + ClientManager clientManager, + IEventManager eventManager) + { + _logger = logger; + _serviceProvider = serviceProvider; + _gameManager = gameManager; + _players = new ConcurrentDictionary<int, ClientPlayer>(); + _bannedIps = new HashSet<IPAddress>(); + + PublicIp = publicIp; + Code = code; + HostId = -1; + GameState = GameStates.NotStarted; + GameNet = new GameNet(); + Options = options; + _clientManager = clientManager; + _eventManager = eventManager; + Items = new ConcurrentDictionary<object, object>(); + } + + public IPEndPoint PublicIp { get; } + + public GameCode Code { get; } + + public bool IsPublic { get; private set; } + + public int HostId { get; private set; } + + public GameStates GameState { get; private set; } + + internal GameNet GameNet { get; } + + public GameOptionsData Options { get; } + + public IDictionary<object, object> Items { get; } + + public int PlayerCount => _players.Count; + + public ClientPlayer Host => _players[HostId]; + + public IEnumerable<IClientPlayer> Players => _players.Select(p => p.Value); + + public bool TryGetPlayer(int id, out ClientPlayer player) + { + if (_players.TryGetValue(id, out var result)) + { + player = result; + return true; + } + + player = default; + return false; + } + + public IClientPlayer GetClientPlayer(int clientId) + { + return _players.TryGetValue(clientId, out var clientPlayer) ? clientPlayer : null; + } + + internal ValueTask StartedAsync() + { + if (GameState == GameStates.Starting) + { + GameState = GameStates.Started; + + return _eventManager.CallAsync(new GameStartedEvent(this)); + } + + return default; + } + + public ValueTask EndAsync() + { + return _gameManager.RemoveAsync(Code); + } + + private ValueTask BroadcastJoinMessage(IMessageWriter message, bool clear, ClientPlayer player) + { + Message01JoinGameS2C.SerializeJoin(message, clear, Code, player.Client.Id, HostId); + + return SendToAllExceptAsync(message, player.Client.Id); + } + + private IEnumerable<IHazelConnection> GetConnections(Func<IClientPlayer, bool> filter) + { + return Players + .Where(filter) + .Select(p => p.Client.Connection); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs new file mode 100644 index 0000000..34ea0fe --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.Api.cs @@ -0,0 +1,17 @@ +using Impostor.Api.Net.Inner; +using Impostor.Api.Net.Inner.Objects; + +namespace Impostor.Server.Net.State +{ + /// <inheritdoc /> + internal partial class GameNet : IGameNet + { + IInnerLobbyBehaviour IGameNet.LobbyBehaviour => LobbyBehaviour; + + IInnerGameData IGameNet.GameData => GameData; + + IInnerVoteBanSystem IGameNet.VoteBan => VoteBan; + + IInnerShipStatus IGameNet.ShipStatus => ShipStatus; + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs new file mode 100644 index 0000000..c5542f9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Net/State/GameNet.cs @@ -0,0 +1,16 @@ +using Impostor.Server.Net.Inner.Objects; +using Impostor.Server.Net.Inner.Objects.Components; + +namespace Impostor.Server.Net.State +{ + internal partial class GameNet + { + public InnerLobbyBehaviour LobbyBehaviour { get; internal set; } + + public InnerGameData GameData { get; internal set; } + + public InnerVoteBanSystem VoteBan { get; internal set; } + + public InnerShipStatus ShipStatus { get; internal set; } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs new file mode 100644 index 0000000..5f6aee1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/AssemblyInformation.cs @@ -0,0 +1,38 @@ +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public class AssemblyInformation : IAssemblyInformation + { + private Assembly _assembly; + + public AssemblyInformation(AssemblyName assemblyName, string path, bool isPlugin) + { + AssemblyName = assemblyName; + Path = path; + IsPlugin = isPlugin; + } + + public string Path { get; } + + public bool IsPlugin { get; } + + public AssemblyName AssemblyName { get; } + + public Assembly Load(AssemblyLoadContext context) + { + if (_assembly != null) + { + return _assembly; + } + + using var stream = File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read); + + _assembly = context.LoadFromStream(stream); + + return _assembly; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs new file mode 100644 index 0000000..fb36e92 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/IAssemblyInformation.cs @@ -0,0 +1,14 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public interface IAssemblyInformation + { + AssemblyName AssemblyName { get; } + + bool IsPlugin { get; } + + Assembly Load(AssemblyLoadContext context); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs new file mode 100644 index 0000000..720367c --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/LoadedAssemblyInformation.cs @@ -0,0 +1,25 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Impostor.Server.Plugins +{ + public class LoadedAssemblyInformation : IAssemblyInformation + { + private readonly Assembly _assembly; + + public LoadedAssemblyInformation(Assembly assembly) + { + AssemblyName = assembly.GetName(); + _assembly = assembly; + } + + public AssemblyName AssemblyName { get; } + + public bool IsPlugin => false; + + public Assembly Load(AssemblyLoadContext context) + { + return _assembly; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs new file mode 100644 index 0000000..22dc9e9 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginConfig.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Impostor.Server.Plugins +{ + public class PluginConfig + { + public List<string> Paths { get; set; } = new List<string>(); + + public List<string> LibraryPaths { get; set; } = new List<string>(); + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs new file mode 100644 index 0000000..e6a5b6c --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginInformation.cs @@ -0,0 +1,38 @@ +using System; +using System.Reflection; +using Impostor.Api.Plugins; + +namespace Impostor.Server.Plugins +{ + public class PluginInformation + { + private readonly ImpostorPluginAttribute _attribute; + + public PluginInformation(IPluginStartup startup, Type pluginType) + { + _attribute = pluginType.GetCustomAttribute<ImpostorPluginAttribute>(); + + Startup = startup; + PluginType = pluginType; + } + + public string Package => _attribute.Package; + + public string Name => _attribute.Name; + + public string Author => _attribute.Author; + + public string Version => _attribute.Version; + + public IPluginStartup Startup { get; } + + public Type PluginType { get; } + + public IPlugin Instance { get; set; } + + public override string ToString() + { + return $"{Package} {Name} ({Version}) by {Author}"; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs new file mode 100644 index 0000000..4e72886 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoader.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Impostor.Api.Plugins; +using Impostor.Server.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileSystemGlobbing; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace Impostor.Server.Plugins +{ + public static class PluginLoader + { + private static readonly ILogger Logger = Log.ForContext(typeof(PluginLoader)); + + public static IHostBuilder UsePluginLoader(this IHostBuilder builder, PluginConfig config) + { + var assemblyInfos = new List<IAssemblyInformation>(); + var context = AssemblyLoadContext.Default; + + // Add the plugins and libraries. + var pluginPaths = new List<string>(config.Paths); + var libraryPaths = new List<string>(config.LibraryPaths); + + var rootFolder = Directory.GetCurrentDirectory(); + + pluginPaths.Add(Path.Combine(rootFolder, "plugins")); + libraryPaths.Add(Path.Combine(rootFolder, "libraries")); + + var matcher = new Matcher(StringComparison.OrdinalIgnoreCase); + matcher.AddInclude("*.dll"); + matcher.AddExclude("Impostor.Api.dll"); + + RegisterAssemblies(pluginPaths, matcher, assemblyInfos, true); + RegisterAssemblies(libraryPaths, matcher, assemblyInfos, false); + + // Register the resolver to the current context. + // TODO: Move this to a new context so we can unload/reload plugins. + context.Resolving += (loadContext, name) => + { + Logger.Verbose("Loading assembly {0} v{1}", name.Name, name.Version); + + // Some plugins may be referencing another Impostor.Api version and try to load it. + // We want to only use the one shipped with the server. + if (name.Name.Equals("Impostor.Api")) + { + return typeof(IPlugin).Assembly; + } + + var info = assemblyInfos.FirstOrDefault(a => a.AssemblyName.Name == name.Name); + + return info?.Load(loadContext); + }; + + // TODO: Catch uncaught exceptions. + var assemblies = assemblyInfos + .Where(a => a.IsPlugin) + .Select(a => context.LoadFromAssemblyName(a.AssemblyName)) + .ToList(); + + // Find all plugins. + var plugins = new List<PluginInformation>(); + + foreach (var assembly in assemblies) + { + // Find plugin startup. + var pluginStartup = assembly + .GetTypes() + .Where(t => typeof(IPluginStartup).IsAssignableFrom(t) && t.IsClass) + .ToList(); + + if (pluginStartup.Count > 1) + { + Logger.Warning("A plugin may only define zero or one IPluginStartup implementation ({0}).", assembly); + continue; + } + + // Find plugin. + var plugin = assembly + .GetTypes() + .Where(t => typeof(IPlugin).IsAssignableFrom(t) + && t.IsClass + && !t.IsAbstract + && t.GetCustomAttribute<ImpostorPluginAttribute>() != null) + .ToList(); + + if (plugin.Count != 1) + { + Logger.Warning("A plugin must define exactly one IPlugin or PluginBase implementation ({0}).", assembly); + continue; + } + + // Save plugin. + plugins.Add(new PluginInformation( + pluginStartup + .Select(Activator.CreateInstance) + .Cast<IPluginStartup>() + .FirstOrDefault(), + plugin.First())); + } + + foreach (var plugin in plugins.Where(plugin => plugin.Startup != null)) + { + plugin.Startup.ConfigureHost(builder); + } + + builder.ConfigureServices(services => + { + services.AddHostedService(provider => ActivatorUtilities.CreateInstance<PluginLoaderService>(provider, plugins)); + + foreach (var plugin in plugins.Where(plugin => plugin.Startup != null)) + { + plugin.Startup.ConfigureServices(services); + } + }); + + return builder; + } + + private static void RegisterAssemblies( + IEnumerable<string> paths, + Matcher matcher, + ICollection<IAssemblyInformation> assemblyInfos, + bool isPlugin) + { + foreach (var path in paths.SelectMany(matcher.GetResultsInFullPath)) + { + AssemblyName assemblyName; + + try + { + assemblyName = AssemblyName.GetAssemblyName(path); + } + catch (BadImageFormatException) + { + continue; + } + + assemblyInfos.Add(new AssemblyInformation(assemblyName, path, isPlugin)); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs new file mode 100644 index 0000000..64424a1 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderException.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.Serialization; +using Impostor.Api; + +namespace Impostor.Server.Plugins +{ + public class PluginLoaderException : ImpostorException + { + public PluginLoaderException() + { + } + + protected PluginLoaderException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public PluginLoaderException(string? message) : base(message) + { + } + + public PluginLoaderException(string? message, Exception? innerException) : base(message, innerException) + { + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs new file mode 100644 index 0000000..0afbc22 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Plugins/PluginLoaderService.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Impostor.Api.Plugins; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Impostor.Server.Plugins +{ + public class PluginLoaderService : IHostedService + { + private readonly ILogger<PluginLoaderService> _logger; + private readonly IServiceProvider _serviceProvider; + private readonly List<PluginInformation> _plugins; + + public PluginLoaderService(ILogger<PluginLoaderService> logger, IServiceProvider serviceProvider, List<PluginInformation> plugins) + { + _logger = logger; + _serviceProvider = serviceProvider; + _plugins = plugins; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Loading plugins."); + + foreach (var plugin in _plugins) + { + _logger.LogInformation("Enabling plugin {0}.", plugin); + + // Create instance and inject services. + plugin.Instance = (IPlugin) ActivatorUtilities.CreateInstance(_serviceProvider, plugin.PluginType); + + // Enable plugin. + await plugin.Instance.EnableAsync(); + } + + _logger.LogInformation( + _plugins.Count == 1 + ? "Loaded {0} plugin." + : "Loaded {0} plugins.", _plugins.Count); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + // Disable all plugins with a valid instance set. + // In the case of a failed startup, some can be null. + foreach (var plugin in _plugins.Where(plugin => plugin.Instance != null)) + { + _logger.LogInformation("Disabling plugin {0}.", plugin); + + // Disable plugin. + await plugin.Instance.DisableAsync(); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Program.cs b/Impostor-dev/src/Impostor.Server/Program.cs new file mode 100644 index 0000000..32a28fb --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Program.cs @@ -0,0 +1,207 @@ +using System; +using System.IO; +using System.Linq; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Games.Managers; +using Impostor.Api.Net.Manager; +using Impostor.Api.Net.Messages; +using Impostor.Hazel.Extensions; +using Impostor.Server.Config; +using Impostor.Server.Events; +using Impostor.Server.Net; +using Impostor.Server.Net.Factories; +using Impostor.Server.Net.Manager; +using Impostor.Server.Net.Messages; +using Impostor.Server.Net.Redirector; +using Impostor.Server.Plugins; +using Impostor.Server.Recorder; +using Impostor.Server.Utils; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ObjectPool; +using Serilog; +using Serilog.Events; + +namespace Impostor.Server +{ + internal static class Program + { + private static int Main(string[] args) + { +#if DEBUG + var logLevel = LogEventLevel.Debug; +#else + var logLevel = LogEventLevel.Information; +#endif + + if (args.Contains("--verbose")) + { + logLevel = LogEventLevel.Verbose; + } + else if (args.Contains("--errors-only")) + { + logLevel = LogEventLevel.Error; + } + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(logLevel) +#if DEBUG + .MinimumLevel.Override("Microsoft", LogEventLevel.Debug) +#else + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) +#endif + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + try + { + Log.Information("Starting Impostor v{0}", DotnetUtils.GetVersion()); + CreateHostBuilder(args).Build().Run(); + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Impostor terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + private static IConfiguration CreateConfiguration(string[] args) + { + var configurationBuilder = new ConfigurationBuilder(); + + configurationBuilder.AddJsonFile("config.json", true); + configurationBuilder.AddJsonFile("config.Development.json", true); + configurationBuilder.AddEnvironmentVariables(prefix: "IMPOSTOR_"); + configurationBuilder.AddCommandLine(args); + + return configurationBuilder.Build(); + } + + //c 入口 + private static IHostBuilder CreateHostBuilder(string[] args) + { + var configuration = CreateConfiguration(args); + var pluginConfig = configuration.GetSection("PluginLoader") + .Get<PluginConfig>() ?? new PluginConfig(); + + return Host.CreateDefaultBuilder(args) + .UseContentRoot(Directory.GetCurrentDirectory()) +#if DEBUG + .UseEnvironment(Environment.GetEnvironmentVariable("IMPOSTOR_ENV") ?? "Development") +#else + .UseEnvironment("Production") +#endif + .ConfigureAppConfiguration(builder => + { + builder.AddConfiguration(configuration); + }) + .ConfigureServices((host, services) => + { + var debug = host.Configuration + .GetSection(DebugConfig.Section) + .Get<DebugConfig>() ?? new DebugConfig(); + + var redirector = host.Configuration + .GetSection(ServerRedirectorConfig.Section) + .Get<ServerRedirectorConfig>() ?? new ServerRedirectorConfig(); + + services.Configure<DebugConfig>(host.Configuration.GetSection(DebugConfig.Section)); + services.Configure<AntiCheatConfig>(host.Configuration.GetSection(AntiCheatConfig.Section)); + services.Configure<ServerConfig>(host.Configuration.GetSection(ServerConfig.Section)); + services.Configure<ServerRedirectorConfig>(host.Configuration.GetSection(ServerRedirectorConfig.Section)); + + if (redirector.Enabled) + { + if (!string.IsNullOrEmpty(redirector.Locator.Redis)) + { + // When joining a game, it retrieves the game server ip from redis. + // When a game has been created on this node, it stores the game code with its ip in redis. + services.AddSingleton<INodeLocator, NodeLocatorRedis>(); + + // Dependency for the NodeLocatorRedis. + services.AddStackExchangeRedisCache(options => + { + options.Configuration = redirector.Locator.Redis; + options.InstanceName = "ImpostorRedis"; + }); + } + else if (!string.IsNullOrEmpty(redirector.Locator.UdpMasterEndpoint)) + { + services.AddSingleton<INodeLocator, NodeLocatorUdp>(); + + if (redirector.Master) + { + services.AddHostedService<NodeLocatorUdpService>(); + } + } + else + { + throw new Exception("Missing a valid NodeLocator config."); + } + + // Use the configuration as source for the list of nodes to provide + // when creating a game. + services.AddSingleton<INodeProvider, NodeProviderConfig>(); + } + else + { + // Redirector is not enabled but the dependency is still required. + // So we provide one that ignores all calls. + services.AddSingleton<INodeLocator, NodeLocatorNoOp>(); + } + + services.AddSingleton<ClientManager>(); + services.AddSingleton<IClientManager>(p => p.GetRequiredService<ClientManager>()); + + if (redirector.Enabled && redirector.Master) + { + services.AddSingleton<IClientFactory, ClientFactory<ClientRedirector>>(); + + // For a master server, we don't need a GameManager. + } + else + { + if (debug.GameRecorderEnabled) + { + services.AddSingleton<ObjectPoolProvider>(new DefaultObjectPoolProvider()); + services.AddSingleton<ObjectPool<PacketSerializationContext>>(serviceProvider => + { + var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>(); + var policy = new PacketSerializationContextPooledObjectPolicy(); + return provider.Create(policy); + }); + + services.AddSingleton<PacketRecorder>(); + services.AddHostedService(sp => sp.GetRequiredService<PacketRecorder>()); + services.AddSingleton<IClientFactory, ClientFactory<ClientRecorder>>(); + } + else + { + services.AddSingleton<IClientFactory, ClientFactory<Client>>(); + } + + services.AddSingleton<GameManager>(); + services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>()); + } + + services.AddHazel(); + services.AddSingleton<IMessageWriterProvider, MessageWriterProvider>(); + services.AddSingleton<IGameCodeFactory, GameCodeFactory>(); + services.AddSingleton<IEventManager, EventManager>(); + services.AddSingleton<Matchmaker>(); + services.AddHostedService<MatchmakerService>(); + }) + .UseSerilog() + .UseConsoleLifetime() + .UsePluginLoader(pluginConfig); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset b/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset new file mode 100644 index 0000000..3654bc3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/ProjectRules.ruleset @@ -0,0 +1,21 @@ +<RuleSet Name="Rules for Hello World project" Description="These rules focus on critical issues for the Hello World app." ToolsVersion="10.0"> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules"> + <Rule Id="SA1200" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules"> + <Rule Id="SA1600" Action="None" /> + <Rule Id="SA1601" Action="None" /> + <Rule Id="SA1615" Action="None" /> + <Rule Id="SA1633" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules"> + <Rule Id="SA1101" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules"> + <Rule Id="SA1309" Action="None" /> + </Rules> + <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.SpacingRules"> + <Rule Id="SA1003" Action="None" /> + <Rule Id="SA1009" Action="None" /> + </Rules> +</RuleSet>
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs b/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..84c5158 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Impostor.Benchmarks")] +[assembly:InternalsVisibleTo("Impostor.Tests")] +[assembly:InternalsVisibleTo("Impostor.Tools.ServerReplay")] diff --git a/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs new file mode 100644 index 0000000..5763c70 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/ClientRecorder.cs @@ -0,0 +1,86 @@ +using System.Threading.Tasks; +using Impostor.Api.Net.Messages; +using Impostor.Server.Config; +using Impostor.Server.Net; +using Impostor.Server.Net.Hazel; +using Impostor.Server.Net.Manager; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Recorder +{ + internal class ClientRecorder : Client + { + private readonly PacketRecorder _recorder; + private bool _isFirst; + private bool _createdGame; + private bool _recordAfter; + + public ClientRecorder(ILogger<Client> logger, IOptions<AntiCheatConfig> antiCheatOptions, ClientManager clientManager, GameManager gameManager, string name, HazelConnection connection, PacketRecorder recorder) + : base(logger, antiCheatOptions, clientManager, gameManager, name, connection) + { + _recorder = recorder; + _isFirst = true; + _createdGame = false; + _recordAfter = true; + } + + public override async ValueTask HandleMessageAsync(IMessageReader reader, MessageType messageType) + { + using var messageCopy = reader.Copy(); + + // Trigger connect event. + if (_isFirst) + { + _isFirst = false; + + await _recorder.WriteConnectAsync(this); + } + + // Check if we were in-game before handling the message. + var inGame = Player?.Game != null; + + if (!_recordAfter) + { + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); + } + + // Handle the message. + await base.HandleMessageAsync(reader, messageType); + + // Player created a game. + if (reader.Tag == MessageFlags.HostGame) + { + _createdGame = true; + } + else if (reader.Tag == MessageFlags.JoinGame && _createdGame) + { + _createdGame = false; + + // We created a game and are now in-game, store that event. + if (!inGame && Player?.Game != null) + { + await _recorder.WriteGameCreatedAsync(this, Player.Game.Code); + } + + _recordAfter = false; + + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); + } + + if (_recordAfter) + { + // Trigger message event. + await _recorder.WriteMessageAsync(this, messageCopy, messageType); + } + } + + public override async ValueTask HandleDisconnectAsync(string reason) + { + await _recorder.WriteDisconnectAsync(this, reason); + await base.HandleDisconnectAsync(reason); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs new file mode 100644 index 0000000..af2ea4f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketRecorder.cs @@ -0,0 +1,201 @@ +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Impostor.Api.Games; +using Impostor.Api.Net.Messages; +using Impostor.Server.Config; +using Impostor.Server.Net; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Impostor.Server.Recorder +{ + /// <summary> + /// Records all packets received in <see cref="ClientRecorder.HandleMessageAsync"/>. + /// </summary> + internal class PacketRecorder : BackgroundService + { + private readonly string _path; + private readonly ILogger<PacketRecorder> _logger; + private readonly ObjectPool<PacketSerializationContext> _pool; + private readonly Channel<byte[]> _channel; + + public PacketRecorder(ILogger<PacketRecorder> logger, IOptions<DebugConfig> options, ObjectPool<PacketSerializationContext> pool) + { + var name = $"session_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}.dat"; + + _path = Path.Combine(options.Value.GameRecorderPath, name); + _logger = logger; + _pool = pool; + + _channel = Channel.CreateUnbounded<byte[]>(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false, + }); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("PacketRecorder is enabled, writing packets to {0}.", _path); + + var writer = File.Open(_path, FileMode.CreateNew, FileAccess.Write, FileShare.Read); + + // Handle messages. + try + { + while (!stoppingToken.IsCancellationRequested) + { + var result = await _channel.Reader.ReadAsync(stoppingToken); + + await writer.WriteAsync(result, stoppingToken); + await writer.FlushAsync(stoppingToken); + } + } + catch (TaskCanceledException) + { + } + + // Clean up. + await writer.DisposeAsync(); + } + + public async Task WriteConnectAsync(ClientRecorder client) + { + _logger.LogTrace("Writing Connect."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Connect); + WriteClient(context, client, true); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteDisconnectAsync(ClientRecorder client, string reason) + { + _logger.LogTrace("Writing Disconnect."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Disconnect); + WriteClient(context, client, false); + context.Writer.Write(reason); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteMessageAsync(ClientRecorder client, IMessageReader reader, MessageType messageType) + { + _logger.LogTrace("Writing Message."); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.Message); + WriteClient(context, client, false); + WritePacket(context, reader, messageType); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + public async Task WriteGameCreatedAsync(ClientRecorder client, GameCode gameCode) + { + _logger.LogTrace("Writing GameCreated {0}.", gameCode); + + var context = _pool.Get(); + + try + { + WriteHeader(context, RecordedPacketType.GameCreated); + WriteClient(context, client, false); + WriteGameCode(context, gameCode); + WriteLength(context); + + await WriteAsync(context.Stream); + } + finally + { + _pool.Return(context); + } + } + + private static void WriteHeader(PacketSerializationContext context, RecordedPacketType type) + { + // Length placeholder. + context.Writer.Write((int) 0); + context.Writer.Write((byte) type); + } + + private static void WriteClient(PacketSerializationContext context, ClientBase client, bool full) + { + var address = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345); + var addressBytes = address.Address.GetAddressBytes(); + + context.Writer.Write(client.Id); + + if (full) + { + context.Writer.Write((byte) addressBytes.Length); + context.Writer.Write(addressBytes); + context.Writer.Write((ushort) address.Port); + context.Writer.Write(client.Name); + } + } + + private static void WritePacket(PacketSerializationContext context, IMessageReader reader, MessageType messageType) + { + context.Writer.Write((byte) messageType); + context.Writer.Write((byte) reader.Tag); + context.Writer.Write((int) reader.Length); + context.Writer.Write(reader.Buffer, reader.Offset, reader.Length); + } + + private static void WriteGameCode(PacketSerializationContext context, in GameCode gameCode) + { + context.Writer.Write(gameCode.Code); + } + + private static void WriteLength(PacketSerializationContext context) + { + var length = context.Stream.Position; + + context.Stream.Position = 0; + context.Writer.Write((int) length); + context.Stream.Position = length; + } + + private async Task WriteAsync(MemoryStream data) + { + await _channel.Writer.WriteAsync(data.ToArray()); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs new file mode 100644 index 0000000..07755f6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContext.cs @@ -0,0 +1,56 @@ +using System.IO; +using System.Text; + +namespace Impostor.Server.Recorder +{ + public class PacketSerializationContext + { + private const int InitialStreamSize = 0x100; + private const int MaximumStreamSize = 0x100000; + + private MemoryStream _memory; + private BinaryWriter _writer; + + public MemoryStream Stream + { + get + { + if (_memory == null) + { + _memory = new MemoryStream(InitialStreamSize); + } + + return _memory; + } + private set => _memory = value; + } + + public BinaryWriter Writer + { + get + { + if (_writer == null) + { + _writer = new BinaryWriter(Stream, Encoding.UTF8, true); + } + + return _writer; + } + private set => _writer = value; + } + + public void Reset() + { + if (Stream.Capacity > MaximumStreamSize) + { + Stream = null; + Writer = null; + } + else + { + Stream.Position = 0L; + Stream.SetLength(0L); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs new file mode 100644 index 0000000..17b355f --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/PacketSerializationContextPooledObjectPolicy.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.ObjectPool; + +namespace Impostor.Server.Recorder +{ + public class PacketSerializationContextPooledObjectPolicy : IPooledObjectPolicy<PacketSerializationContext> + { + public PacketSerializationContext Create() + { + return new PacketSerializationContext(); + } + + public bool Return(PacketSerializationContext obj) + { + obj.Reset(); + return true; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs new file mode 100644 index 0000000..a8a20bc --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Recorder/RecordedPacketType.cs @@ -0,0 +1,10 @@ +namespace Impostor.Server.Recorder +{ + internal enum RecordedPacketType : byte + { + Connect = 1, + Disconnect = 2, + Message = 3, + GameCreated = 4 + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs b/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs new file mode 100644 index 0000000..48a0ac0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Utils/DotnetUtils.cs @@ -0,0 +1,20 @@ +using System.Reflection; + +namespace Impostor.Server.Utils +{ + internal static class DotnetUtils + { + private const string DefaultUnknownBuild = "UNKNOWN"; + + public static string GetVersion() + { + var attribute = typeof(DotnetUtils).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>(); + if (attribute != null) + { + return attribute.InformationalVersion; + } + + return DefaultUnknownBuild; + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs b/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs new file mode 100644 index 0000000..649d45a --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/Utils/IpUtils.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Net; +using System.Net.Sockets; +using Impostor.Api; + +namespace Impostor.Server.Utils +{ + internal static class IpUtils + { + public static string ResolveIp(string ip) + { + // Check if valid ip was entered. + if (!IPAddress.TryParse(ip, out var ipAddress)) + { + // Attempt to resolve DNS. + try + { + var hostAddresses = Dns.GetHostAddresses(ip); + if (hostAddresses.Length == 0) + { + throw new ImpostorConfigException($"Invalid IP Address entered '{ip}'."); + } + + // Use first IPv4 result. + ipAddress = hostAddresses.First(x => x.AddressFamily == AddressFamily.InterNetwork); + } + catch (SocketException) + { + throw new ImpostorConfigException($"Failed to resolve hostname '{ip}'."); + } + } + + // Only IPv4. + if (ipAddress.AddressFamily == AddressFamily.InterNetworkV6) + { + throw new ImpostorConfigException($"Invalid IP Address entered '{ipAddress}', only IPv4 is supported by Among Us."); + } + + return ipAddress.ToString(); + } + } +} diff --git a/Impostor-dev/src/Impostor.Server/config-full.json b/Impostor-dev/src/Impostor.Server/config-full.json new file mode 100644 index 0000000..dfee1bc --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/config-full.json @@ -0,0 +1,29 @@ +{ + "Server": { + "PublicIp": "127.0.0.1", + "PublicPort": 22023, + "ListenIp": "0.0.0.0", + "ListenPort": 22023 + }, + "AntiCheat": { + "BanIpFromGame": true + }, + "ServerRedirector": { + "Enabled": false, + "Master": true, + "Locator": { + "Redis": "127.0.0.1.6379", + "UdpMasterEndpoint": "127.0.0.1:32320" + }, + "Nodes": [ + { + "Ip": "127.0.0.1", + "Port": 22024 + } + ] + }, + "Debug": { + "GameRecorderEnabled": true, + "GameRecorderPath": "" + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/config.json b/Impostor-dev/src/Impostor.Server/config.json new file mode 100644 index 0000000..d477c74 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/config.json @@ -0,0 +1,11 @@ +{ + "Server": { + "PublicIp": "127.0.0.1", + "PublicPort": 22023, + "ListenIp": "0.0.0.0", + "ListenPort": 22023 + }, + "AntiCheat": { + "BanIpFromGame": true + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Server/icon.ico b/Impostor-dev/src/Impostor.Server/icon.ico Binary files differnew file mode 100644 index 0000000..8cc46f3 --- /dev/null +++ b/Impostor-dev/src/Impostor.Server/icon.ico diff --git a/Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs b/Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs new file mode 100644 index 0000000..d222d79 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tests/Events/EventManagerTests.cs @@ -0,0 +1,175 @@ +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading.Tasks; +using Impostor.Api.Events; +using Impostor.Api.Events.Managers; +using Impostor.Server.Events; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Impostor.Tests.Events +{ + public class EventManagerTests + { + public static readonly IEnumerable<object[]> TestModes = new [] + { + new object[] { TestMode.Service }, + new object[] { TestMode.Temporary } + }; + + [Theory] + [MemberData(nameof(TestModes))] + public async ValueTask CallEvent(TestMode mode) + { + var listener = new EventListener(); + var eventManager = CreatEventManager(mode, listener); + + await eventManager.CallAsync(new SetValueEvent(1)); + + Assert.Equal(1, listener.Value); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async Task CallPriority(TestMode mode) + { + var listener = new PriorityEventListener(); + var eventManager = CreatEventManager(mode, listener); + + await eventManager.CallAsync(new SetValueEvent(1)); + + Assert.Equal(new [] + { + EventPriority.Monitor, + EventPriority.Highest, + EventPriority.High, + EventPriority.Normal, + EventPriority.Low, + EventPriority.Lowest + }, listener.Priorities); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async ValueTask CancelEvent(TestMode mode) + { + var listener = new EventListener(); + var eventManager = CreatEventManager( + mode, + new CancelAtHighEventListener(), + listener + ); + + await eventManager.CallAsync(new SetValueEvent(1)); + + Assert.Equal(0, listener.Value); + } + + [Theory] + [MemberData(nameof(TestModes))] + public async Task CancelPriority(TestMode mode) + { + var listener = new PriorityEventListener(); + var eventManager = CreatEventManager( + mode, + new CancelAtHighEventListener(), + listener + ); + + await eventManager.CallAsync(new SetValueEvent(1)); + + Assert.Equal(new [] + { + EventPriority.Monitor, + EventPriority.Highest + }, listener.Priorities); + } + + private static IEventManager CreatEventManager(TestMode mode, params IEventListener[] listeners) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSingleton<IEventManager, EventManager>(); + + if (mode == TestMode.Service) + { + foreach (var listener in listeners) + { + services.AddSingleton(listener); + } + } + + var eventManager = services.BuildServiceProvider().GetRequiredService<IEventManager>(); + + if (mode == TestMode.Temporary) + { + foreach (var listener in listeners) + { + eventManager.RegisterListener(listener); + } + } + + return eventManager; + } + + public enum TestMode + { + Service, + Temporary + } + + public interface ISetValueEvent : IEventCancelable + { + int Value { get; } + } + + public class SetValueEvent : ISetValueEvent + { + public SetValueEvent(int value) + { + Value = value; + } + + public int Value { get; } + + public bool IsCancelled { get; set; } + } + + private class CancelAtHighEventListener : IEventListener + { + [EventListener(Priority = EventPriority.High)] + public void OnSetCalled(ISetValueEvent e) => e.IsCancelled = true; + } + + private class EventListener : IEventListener + { + public int Value { get; private set; } + + [EventListener] + public void OnSetCalled(ISetValueEvent e) => Value = e.Value; + } + + private class PriorityEventListener : IEventListener + { + public List<EventPriority> Priorities { get; } = new List<EventPriority>(); + + [EventListener(EventPriority.Lowest)] + public void OnLowest(ISetValueEvent e) => Priorities.Add(EventPriority.Lowest); + + [EventListener(EventPriority.Low)] + public void OnLow(ISetValueEvent e) => Priorities.Add(EventPriority.Low); + + [EventListener] + public void OnNormal(ISetValueEvent e) => Priorities.Add(EventPriority.Normal); + + [EventListener(EventPriority.High)] + public void OnHigh(ISetValueEvent e) => Priorities.Add(EventPriority.High); + + [EventListener(EventPriority.Highest)] + public void OnHighest(ISetValueEvent e) => Priorities.Add(EventPriority.Highest); + + [EventListener(EventPriority.Monitor)] + public void OnMonitor(ISetValueEvent e) => Priorities.Add(EventPriority.Monitor); + } + } +} diff --git a/Impostor-dev/src/Impostor.Tests/GameCodeTests.cs b/Impostor-dev/src/Impostor.Tests/GameCodeTests.cs new file mode 100644 index 0000000..de98123 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tests/GameCodeTests.cs @@ -0,0 +1,29 @@ +using Impostor.Api.Innersloth; + +using Xunit; + +namespace Impostor.Tests +{ + public class GameCodeTests + { + [Fact] + public void CodeV1() + { + const string code = "ABCD"; + const int codeInt = 0x44434241; + + Assert.Equal(code, GameCodeParser.IntToGameName(codeInt)); + Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code)); + } + + [Fact] + public void CodeV2() + { + const string code = "ABCDEF"; + const int codeInt = -1943683525; + + Assert.Equal(code, GameCodeParser.IntToGameName(codeInt)); + Assert.Equal(codeInt, GameCodeParser.GameNameToInt(code)); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs b/Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs new file mode 100644 index 0000000..e7fa0df --- /dev/null +++ b/Impostor-dev/src/Impostor.Tests/Hazel/MessageReaderTests.cs @@ -0,0 +1,372 @@ +using System; +using System.Linq; +using Impostor.Api; +using Impostor.Hazel; +using Impostor.Hazel.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Impostor.Tests.Hazel +{ + public class MessageReaderTests + { + private ObjectPool<MessageReader> CreateReaderPool() + { + var services = new ServiceCollection(); + services.AddHazel(); + return services.BuildServiceProvider().GetRequiredService<ObjectPool<MessageReader>>(); + } + + [Fact] + public void ReadProperInt() + { + const int Test1 = int.MaxValue; + const int Test2 = int.MinValue; + + var msg = new MessageWriter(128); + msg.StartMessage(1); + msg.Write(Test1); + msg.Write(Test2); + msg.EndMessage(); + + Assert.Equal(11, msg.Length); + Assert.Equal(msg.Length, msg.Position); + + var readerPool = CreateReaderPool(); + var reader = readerPool.Get(); + reader.Update(msg.Buffer); + Assert.Equal(byte.MaxValue, reader.Tag); + var message = reader.ReadMessage(); + Assert.Equal(1, message.Tag); + Assert.Equal(Test1, message.ReadInt32()); + Assert.Equal(Test2, message.ReadInt32()); + } + + [Fact] + public void ReadProperBool() + { + const bool Test1 = true; + const bool Test2 = false; + + var msg = new MessageWriter(128); + msg.StartMessage(1); + msg.Write(Test1); + msg.Write(Test2); + msg.EndMessage(); + + Assert.Equal(5, msg.Length); + Assert.Equal(msg.Length, msg.Position); + + var readerPool = CreateReaderPool(); + var reader = readerPool.Get(); + reader.Update(msg.Buffer); + Assert.Equal(byte.MaxValue, reader.Tag); + var message = reader.ReadMessage(); + Assert.Equal(1, message.Tag); + Assert.Equal(Test1, message.ReadBoolean()); + Assert.Equal(Test2, message.ReadBoolean()); + } + + [Fact] + public void ReadProperString() + { + const string Test1 = "Hello"; + string Test2 = new string(' ', 1024); + var msg = new MessageWriter(2048); + msg.StartMessage(1); + msg.Write(Test1); + msg.Write(Test2); + msg.Write(string.Empty); + msg.EndMessage(); + + Assert.Equal(msg.Length, msg.Position); + + var readerPool = CreateReaderPool(); + var reader = readerPool.Get(); + reader.Update(msg.Buffer); + Assert.Equal(byte.MaxValue, reader.Tag); + var message = reader.ReadMessage(); + Assert.Equal(1, message.Tag); + Assert.Equal(Test1, message.ReadString()); + Assert.Equal(Test2, message.ReadString()); + Assert.Equal(string.Empty, message.ReadString()); + } + + [Fact] + public void ReadProperFloat() + { + const float Test1 = 12.34f; + + var msg = new MessageWriter(2048); + msg.StartMessage(1); + msg.Write(Test1); + msg.EndMessage(); + + Assert.Equal(7, msg.Length); + Assert.Equal(msg.Length, msg.Position); + + var readerPool = CreateReaderPool(); + var reader = readerPool.Get(); + reader.Update(msg.Buffer); + Assert.Equal(byte.MaxValue, reader.Tag); + var message = reader.ReadMessage(); + Assert.Equal(1, message.Tag); + Assert.Equal(Test1, message.ReadSingle()); + } + + [Fact] + public void CopyMessage() + { + var readerPool = CreateReaderPool(); + + // Create message. + const int msgLength = 18; + const byte Test1 = 12; + const byte Test2 = 146; + + var msg = new MessageWriter(2048); + + msg.StartMessage(1); + msg.StartMessage(2); + msg.Write(Test1); + msg.Write(Test2); + msg.StartMessage(2); + msg.Write(Test1); + msg.Write(Test2); + msg.StartMessage(2); + msg.Write(Test1); + msg.Write(Test2); + msg.EndMessage(); + msg.EndMessage(); + msg.EndMessage(); + msg.EndMessage(); + + // Read message. + using var reader = readerPool.Get(); + + reader.Update(msg.Buffer); + + // Read first message. + using var messageOne = reader.ReadMessage(); + + Assert.Equal(1, messageOne.Tag); + Assert.Equal(0, messageOne.Position); + Assert.Equal(3, messageOne.Offset); + Assert.Equal(msgLength - 3, messageOne.Length); + + using var messageTwo = messageOne.ReadMessage(); + + Assert.Equal(2, messageTwo.Tag); + Assert.Equal(0, messageTwo.Position); + Assert.Equal(6, messageTwo.Offset); + Assert.Equal(msgLength - 6, messageTwo.Length); + Assert.Equal(Test1, messageTwo.ReadByte()); + Assert.Equal(Test2, messageTwo.ReadByte()); + + using var messageThree = messageTwo.ReadMessage(); + + Assert.Equal(2, messageThree.Tag); + Assert.Equal(0, messageThree.Position); + Assert.Equal(11, messageThree.Offset); + Assert.Equal(msgLength - 11, messageThree.Length); + Assert.Equal(Test1, messageThree.ReadByte()); + Assert.Equal(Test2, messageThree.ReadByte()); + } + + [Fact] + public void CopySubMessage() + { + const byte Test1 = 12; + const byte Test2 = 146; + + var msg = new MessageWriter(2048); + msg.StartMessage(1); + + msg.StartMessage(2); + msg.Write(Test1); + msg.Write(Test2); + msg.EndMessage(); + + msg.EndMessage(); + + var readerPool = CreateReaderPool(); + var handleReader = readerPool.Get(); + handleReader.Update(msg.Buffer); + var handleMessage = handleReader.ReadMessage(); + Assert.Equal(1, handleMessage.Tag); + + using var parentReader = handleMessage.Copy(); + + Assert.Equal(1, parentReader.Tag); + + var reader = parentReader.ReadMessage(); + + Assert.Equal(2, reader.Tag); + Assert.Equal(Test1, reader.ReadByte()); + Assert.Equal(Test2, reader.ReadByte()); + } + + [Fact] + public void CopyToMessage() + { + var expected = new byte[] + { + 0x2A, 0x00, 0x01, 0x27, 0x00, 0x02, 0x26, 0x54, + 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, + 0x20, 0x6C, 0x6F, 0x6E, 0x67, 0x20, 0x70, 0x61, + 0x63, 0x6B, 0x65, 0x74, 0x20, 0x74, 0x6F, 0x20, + 0x74, 0x65, 0x73, 0x74, 0x20, 0x63, 0x6F, 0x70, + 0x79, 0x69, 0x6E, 0x67, 0x2E + }; + + var readerPool = CreateReaderPool(); + + // Create packet. + var msg = new MessageWriter(2048); + msg.StartMessage(1); + msg.StartMessage(2); + msg.Write("This is a long packet to test copying."); + msg.EndMessage(); + msg.EndMessage(); + + // Create a reader. + var reader = readerPool.Get(); + + reader.Update(msg.Buffer); + + // Read the initial message. + var message = reader.ReadMessage(); + + // Copy the message to a new writer. + var writer = new MessageWriter(2048); + + message.CopyTo(writer); + + // Compare. + Assert.Equal(expected, writer.ToByteArray(true)); + } + + [Fact] + public void ReadMessageLength() + { + var msg = new MessageWriter(2048); + msg.StartMessage(1); + msg.Write(65534); + msg.StartMessage(2); + msg.Write("HO"); + msg.EndMessage(); + msg.StartMessage(2); + msg.Write("NO"); + msg.EndMessage(); + msg.EndMessage(); + + Assert.Equal(msg.Length, msg.Position); + + var readerPool = CreateReaderPool(); + var reader = readerPool.Get(); + reader.Update(msg.Buffer); + Assert.Equal(byte.MaxValue, reader.Tag); + var message = reader.ReadMessage(); + Assert.Equal(1, message.Tag); + Assert.Equal(65534, message.ReadInt32()); // Content + + var sub = message.ReadMessage(); + Assert.Equal(3, sub.Length); + Assert.Equal(2, sub.Tag); + Assert.Equal("HO", sub.ReadString()); + + sub = message.ReadMessage(); + Assert.Equal(3, sub.Length); + Assert.Equal(2, sub.Tag); + Assert.Equal("NO", sub.ReadString()); + } + + [Fact] + public void RemoveMessage() + { + // Create expected message. + var messageExpected = new MessageWriter(1024); + + messageExpected.StartMessage(0); + messageExpected.StartMessage(1); + messageExpected.Write("HiTest1"); + messageExpected.EndMessage(); + messageExpected.StartMessage(2); + messageExpected.Write("HiTest2"); + messageExpected.EndMessage(); + messageExpected.EndMessage(); + + // Create message. + var messageWriter = new MessageWriter(1024); + + messageWriter.StartMessage(0); + messageWriter.StartMessage(1); + messageWriter.Write("HiTest1"); + messageWriter.StartMessage(2); + messageWriter.Write("RemoveMe!"); + messageWriter.EndMessage(); + messageWriter.EndMessage(); + messageWriter.StartMessage(2); + messageWriter.Write("HiTest2"); + messageWriter.EndMessage(); + messageWriter.EndMessage(); + + // Copy buffer. + var bufferCopy = new byte[messageWriter.Length]; + Buffer.BlockCopy(messageWriter.Buffer, 0, bufferCopy, 0, bufferCopy.Length); + + var bufferCopyTwo = new byte[messageWriter.Length]; + Buffer.BlockCopy(messageWriter.Buffer, 0, bufferCopyTwo, 0, bufferCopyTwo.Length); + + // Do the magic. + var readerPool = CreateReaderPool(); + var reader = readerPool.Get(); + reader.Update(bufferCopy); + var inner = reader.ReadMessage(); + + while (inner.Position < inner.Length) + { + var message = inner.ReadMessage(); + if (message.Tag == 1) + { + Assert.Equal("HiTest1", message.ReadString()); + + var messageSub = message.ReadMessage(); + if (messageSub.Tag == 2) + { + Assert.Equal("RemoveMe!", messageSub.ReadString()); + + // Remove this message. + inner.RemoveMessage(messageSub); + } + } + else if (message.Tag == 2) + { + Assert.Equal("HiTest2", message.ReadString()); + } + else + { + Assert.True(false, "Invalid tag was read."); + } + } + + // Check if the magic was successful. + Assert.Equal(messageExpected.Length, reader.Length); + Assert.Equal(messageExpected.ToByteArray(true), reader.Buffer.Take(reader.Length).ToArray()); + + // Test ownership. + var readerTwo = readerPool.Get(); + + readerTwo.Update(bufferCopyTwo); + + Assert.Throws<ImpostorProtocolException>(() => reader.RemoveMessage(readerTwo.ReadMessage())); + } + + [Fact] + public void GetLittleEndian() + { + Assert.True(MessageWriter.IsLittleEndian()); + } + } +} diff --git a/Impostor-dev/src/Impostor.Tests/Hazel/MessageWriterTests.cs b/Impostor-dev/src/Impostor.Tests/Hazel/MessageWriterTests.cs new file mode 100644 index 0000000..83d67da --- /dev/null +++ b/Impostor-dev/src/Impostor.Tests/Hazel/MessageWriterTests.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using Impostor.Hazel; +using Xunit; + +namespace Impostor.Tests.Hazel +{ + public class MessageWriterTests + { + [Fact] + public void ReadOnlyMemoryWriteWorksTheSameAsArray() + { + var oldVer = new MessageWriter(1024); + var newVer = new MessageWriter(1024); + + var data = Enumerable.Repeat + ( + Enumerable.Range(0, byte.MaxValue) + .Select(x => (byte)x), + 2 + ).SelectMany(x => x).ToArray(); + + WriteSomeData(oldVer); + WriteSomeData(newVer); + + oldVer.Write(data); + newVer.Write(data.AsMemory()); + + Assert.True(oldVer.Buffer.AsSpan().SequenceEqual(newVer.Buffer.AsSpan())); + + static void WriteSomeData(MessageWriter oldVer) + { + oldVer.WritePacked(99); + oldVer.WritePacked(101); + } + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj b/Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj new file mode 100644 index 0000000..3f96bf6 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tests/Impostor.Tests.csproj @@ -0,0 +1,20 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net5.0</TargetFramework> + <IsPackable>false</IsPackable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" /> + <PackageReference Include="xunit" Version="2.4.1" /> + <PackageReference Include="xunit.runner.reporters" Version="2.4.1" /> + <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> + <PackageReference Include="coverlet.collector" Version="1.3.0" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs b/Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs new file mode 100644 index 0000000..79517b4 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.Proxy/HexUtils.cs @@ -0,0 +1,70 @@ +using System; +using System.Text; + +namespace Impostor.Tools.Proxy +{ + public static class HexUtils + { + public static string HexDump(byte[] bytes, int bytesPerLine = 16) + { + if (bytes == null) return "<null>"; + int bytesLength = bytes.Length; + + char[] HexChars = "0123456789ABCDEF".ToCharArray(); + + int firstHexColumn = + 8 // 8 characters for the address + + 3; // 3 spaces + + int firstCharColumn = firstHexColumn + + bytesPerLine * 3 // - 2 digit for the hexadecimal value and 1 space + + (bytesPerLine - 1) / 8 // - 1 extra space every 8 characters from the 9th + + 2; // 2 spaces + + int lineLength = firstCharColumn + + bytesPerLine // - characters to show the ascii value + + Environment.NewLine.Length; // Carriage return and line feed (should normally be 2) + + char[] line = (new String(' ', lineLength - Environment.NewLine.Length) + Environment.NewLine).ToCharArray(); + int expectedLines = (bytesLength + bytesPerLine - 1) / bytesPerLine; + StringBuilder result = new StringBuilder(expectedLines * lineLength); + + for (int i = 0; i < bytesLength; i += bytesPerLine) + { + line[0] = HexChars[(i >> 28) & 0xF]; + line[1] = HexChars[(i >> 24) & 0xF]; + line[2] = HexChars[(i >> 20) & 0xF]; + line[3] = HexChars[(i >> 16) & 0xF]; + line[4] = HexChars[(i >> 12) & 0xF]; + line[5] = HexChars[(i >> 8) & 0xF]; + line[6] = HexChars[(i >> 4) & 0xF]; + line[7] = HexChars[(i >> 0) & 0xF]; + + int hexColumn = firstHexColumn; + int charColumn = firstCharColumn; + + for (int j = 0; j < bytesPerLine; j++) + { + if (j > 0 && (j & 7) == 0) hexColumn++; + if (i + j >= bytesLength) + { + line[hexColumn] = ' '; + line[hexColumn + 1] = ' '; + line[charColumn] = ' '; + } + else + { + byte b = bytes[i + j]; + line[hexColumn] = HexChars[(b >> 4) & 0xF]; + line[hexColumn + 1] = HexChars[b & 0xF]; + line[charColumn] = (b < 32 ? '·' : (char)b); + } + hexColumn += 3; + charColumn++; + } + result.Append(line); + } + return result.ToString(); + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj b/Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj new file mode 100644 index 0000000..8f523dc --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.Proxy/Impostor.Tools.Proxy.csproj @@ -0,0 +1,17 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.0" /> + <PackageReference Include="Pcap.Net.x64" Version="1.0.4.1" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Hazel\Impostor.Hazel.csproj" /> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Tools.Proxy/Program.cs b/Impostor-dev/src/Impostor.Tools.Proxy/Program.cs new file mode 100644 index 0000000..9e765f0 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.Proxy/Program.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Impostor.Api.Net.Messages; +using Impostor.Hazel; +using Impostor.Hazel.Extensions; +using Impostor.Hazel.Udp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.ObjectPool; +using PcapDotNet.Core; +using PcapDotNet.Packets; + +namespace Impostor.Tools.Proxy +{ + internal static class Program + { + private const string DeviceName = "Intel(R) I211 Gigabit Network Connection"; + + private static readonly Dictionary<byte, string> TagMap = new Dictionary<byte, string> + { + {0, "HostGame"}, + {1, "JoinGame"}, + {2, "StartGame"}, + {3, "RemoveGame"}, + {4, "RemovePlayer"}, + {5, "GameData"}, + {6, "GameDataTo"}, + {7, "JoinedGame"}, + {8, "EndGame"}, + {9, "GetGameList"}, + {10, "AlterGame"}, + {11, "KickPlayer"}, + {12, "WaitForHost"}, + {13, "Redirect"}, + {14, "ReselectServer"}, + {16, "GetGameListV2"} + }; + + private static IServiceProvider _serviceProvider; + private static ObjectPool<MessageReader> _readerPool; + + //c 服务器入口 + private static void Main(string[] args) + { + var services = new ServiceCollection(); + services.AddHazel(); + + _serviceProvider = services.BuildServiceProvider(); + _readerPool = _serviceProvider.GetRequiredService<ObjectPool<MessageReader>>(); + + var devices = LivePacketDevice.AllLocalMachine; + if (devices.Count == 0) + { + Console.WriteLine("No interfaces found! Make sure WinPcap is installed."); + return; + } + + var device = devices.FirstOrDefault(x => x.Description.Contains(DeviceName)); + if (device == null) + { + Console.WriteLine("Unable to find configured device."); + return; + } + + using (var communicator = device.Open(65536, PacketDeviceOpenAttributes.Promiscuous, 1000)) + { + using (var filter = communicator.CreateFilter("udp and port 22023")) + { + communicator.SetFilter(filter); + } + + communicator.ReceivePackets(0, PacketHandler); + } + } + + private static void PacketHandler(Packet packet) + { + var ip = packet.Ethernet.IpV4; + var ipSrc = ip.Source.ToString(); + var udp = ip.Udp; + + // True if this is our own packet. + using (var stream = udp.Payload.ToMemoryStream()) + { + using var reader = _readerPool.Get(); + + reader.Update(stream.ToArray()); + + var option = reader.Buffer[0]; + if (option == (byte) MessageType.Reliable) + { + reader.Seek(reader.Position + 3); + } + else if (option == (byte) UdpSendOption.Acknowledgement || + option == (byte) UdpSendOption.Ping || + option == (byte) UdpSendOption.Hello || + option == (byte) UdpSendOption.Disconnect) + { + return; + } + else + { + reader.Seek(reader.Position + 1); + } + + var isSent = ipSrc.StartsWith("192."); + + while (true) + { + if (reader.Position >= reader.Length) + { + break; + } + + //c 消息 + using var message = reader.ReadMessage(); + if (isSent) + { + HandleToServer(ipSrc, message); + } + else + { + HandleToClient(ipSrc, message); + } + + if (message.Position < message.Length) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("- Did not consume all bytes."); + } + } + } + } + + private static void HandleToClient(string source, IMessageReader packet) + { + var tagName = TagMap.ContainsKey(packet.Tag) ? TagMap[packet.Tag] : "Unknown"; + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"{source,-15} Client received: {packet.Tag,-2} {tagName}"); + + switch (packet.Tag) + { + case 14: + case 13: + // packet.Position = packet.Length; + break; + case 0: + Console.WriteLine("- GameCode " + packet.ReadInt32()); + break; + case 5: + case 6: + Console.WriteLine(HexUtils.HexDump(packet.Buffer.ToArray().Take(packet.Length).ToArray())); + // packet.Position = packet.Length; + break; + case 7: + Console.WriteLine("- GameCode " + packet.ReadInt32()); + Console.WriteLine("- PlayerId " + packet.ReadInt32()); + Console.WriteLine("- Host " + packet.ReadInt32()); + var playerCount = packet.ReadPackedInt32(); + Console.WriteLine("- PlayerCount " + playerCount); + for (var i = 0; i < playerCount; i++) + { + Console.WriteLine("- PlayerId " + packet.ReadPackedInt32()); + } + break; + case 10: + Console.WriteLine("- GameCode " + packet.ReadInt32()); + Console.WriteLine("- Flag " + packet.ReadSByte()); + Console.WriteLine("- Value " + packet.ReadBoolean()); + break; + } + } + + private static void HandleToServer(string source, IMessageReader packet) + { + var tagName = TagMap.ContainsKey(packet.Tag) ? TagMap[packet.Tag] : "Unknown"; + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($"{source,-15} Server received: {packet.Tag,-2} {tagName}"); + + switch (packet.Tag) + { + case 0: + Console.WriteLine("- GameInfo length " + packet.ReadBytesAndSize().Length); + break; + case 1: + Console.WriteLine("- GameCode " + packet.ReadInt32()); + Console.WriteLine("- Unknown " + packet.ReadByte()); + break; + case 5: + case 6: + Console.WriteLine("- GameCode " + packet.ReadInt32()); + Console.WriteLine(HexUtils.HexDump(packet.Buffer.ToArray().Take(packet.Length).ToArray())); + // packet.Position = packet.Length; + break; + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj b/Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj new file mode 100644 index 0000000..98fa689 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Impostor.Tools.ServerReplay.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net5.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Impostor.Server\Impostor.Server.csproj"/> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1"/> + </ItemGroup> + +</Project> diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs new file mode 100644 index 0000000..1111b7e --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockGameCodeFactory.cs @@ -0,0 +1,14 @@ +using Impostor.Api.Games; + +namespace Impostor.Tools.ServerReplay.Mocks +{ + public class MockGameCodeFactory : IGameCodeFactory + { + public GameCode Result { get; set; } + + public GameCode Create() + { + return Result; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs new file mode 100644 index 0000000..43f0257 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Mocks/MockHazelConnection.cs @@ -0,0 +1,31 @@ +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; + +namespace Impostor.Tools.ServerReplay.Mocks +{ + public class MockHazelConnection : IHazelConnection + { + public MockHazelConnection(IPEndPoint endPoint) + { + EndPoint = endPoint; + IsConnected = true; + Client = null; + } + + public IPEndPoint EndPoint { get; } + public bool IsConnected { get; } + public IClient? Client { get; set; } + + public ValueTask SendAsync(IMessageWriter writer) + { + return ValueTask.CompletedTask; + } + + public ValueTask DisconnectAsync(string reason) + { + return ValueTask.CompletedTask; + } + } +}
\ No newline at end of file diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs b/Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs new file mode 100644 index 0000000..5aaa954 --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/Program.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Net; +using System.Threading.Tasks; +using Impostor.Api.Events.Managers; +using Impostor.Api.Games; +using Impostor.Api.Games.Managers; +using Impostor.Api.Innersloth; +using Impostor.Api.Net; +using Impostor.Api.Net.Messages; +using Impostor.Api.Net.Messages.C2S; +using Impostor.Hazel; +using Impostor.Hazel.Extensions; +using Impostor.Server.Events; +using Impostor.Server.Net; +using Impostor.Server.Net.Factories; +using Impostor.Server.Net.Manager; +using Impostor.Server.Net.Redirector; +using Impostor.Server.Recorder; +using Impostor.Tools.ServerReplay.Mocks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Serilog; +using ILogger = Serilog.ILogger; + +namespace Impostor.Tools.ServerReplay +{ + internal static class Program + { + private static readonly ILogger Logger = Log.ForContext(typeof(Program)); + private static readonly Dictionary<int, IHazelConnection> Connections = new Dictionary<int, IHazelConnection>(); + private static readonly Dictionary<int, GameOptionsData> GameOptions = new Dictionary<int, GameOptionsData>(); + + private static ServiceProvider _serviceProvider; + + private static ObjectPool<MessageReader> _readerPool; + private static MockGameCodeFactory _gameCodeFactory; + private static ClientManager _clientManager; + private static GameManager _gameManager; + + private static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + // .MinimumLevel.Verbose() + .MinimumLevel.Debug() + .WriteTo.Console() + .CreateLogger(); + + var stopwatch = Stopwatch.StartNew(); + + foreach (var file in Directory.GetFiles(args[0])) + { + // Clear. + Connections.Clear(); + GameOptions.Clear(); + + // Create service provider. + _serviceProvider = BuildServices(); + + // Create required instances. + _readerPool = _serviceProvider.GetRequiredService<ObjectPool<MessageReader>>(); + _gameCodeFactory = _serviceProvider.GetRequiredService<MockGameCodeFactory>(); + _clientManager = _serviceProvider.GetRequiredService<ClientManager>(); + _gameManager = _serviceProvider.GetRequiredService<GameManager>(); + + await using (var stream = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (var reader = new BinaryReader(stream)) + { + await ParseSession(reader); + } + } + + var elapsedMilliseconds = stopwatch.ElapsedMilliseconds; + + Logger.Information($"Took {elapsedMilliseconds}ms."); + } + + private static ServiceProvider BuildServices() + { + var services = new ServiceCollection(); + + services.AddLogging(builder => + { + builder.ClearProviders(); + builder.AddSerilog(); + }); + + services.AddSingleton<GameManager>(); + services.AddSingleton<IGameManager>(p => p.GetRequiredService<GameManager>()); + + services.AddSingleton<MockGameCodeFactory>(); + services.AddSingleton<IGameCodeFactory>(p => p.GetRequiredService<MockGameCodeFactory>()); + + services.AddSingleton<ClientManager>(); + services.AddSingleton<IClientFactory, ClientFactory<Client>>(); + services.AddSingleton<INodeLocator, NodeLocatorNoOp>(); + services.AddSingleton<IEventManager, EventManager>(); + + services.AddHazel(); + + return services.BuildServiceProvider(); + } + + private static async Task ParseSession(BinaryReader reader) + { + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + var dataLength = reader.ReadInt32(); + var data = reader.ReadBytes(dataLength - 4); + + await using (var stream = new MemoryStream(data)) + using (var readerInner = new BinaryReader(stream)) + { + await ParsePacket(readerInner); + } + } + } + + private static async Task ParsePacket(BinaryReader reader) + { + var dataType = (RecordedPacketType) reader.ReadByte(); + + // Read client id. + var clientId = reader.ReadInt32(); + + switch (dataType) + { + case RecordedPacketType.Connect: + // Read data. + var addressLength = reader.ReadByte(); + var addressBytes = reader.ReadBytes(addressLength); + var addressPort = reader.ReadUInt16(); + var address = new IPEndPoint(new IPAddress(addressBytes), addressPort); + var name = reader.ReadString(); + + // Create and register connection. + var connection = new MockHazelConnection(address); + + await _clientManager.RegisterConnectionAsync(connection, name, 50516550); + + // Store reference for ourselfs. + Connections.Add(clientId, connection); + break; + + case RecordedPacketType.Disconnect: + string reason = null; + + if (reader.BaseStream.Position < reader.BaseStream.Length) + { + reason = reader.ReadString(); + } + + await Connections[clientId].Client!.HandleDisconnectAsync(reason); + Connections.Remove(clientId); + break; + + case RecordedPacketType.Message: + { + var messageType = (MessageType)reader.ReadByte(); + var tag = reader.ReadByte(); + var length = reader.ReadInt32(); + var buffer = reader.ReadBytes(length); + using var message = _readerPool.Get(); + + message.Update(buffer, tag: tag); + + if (tag == MessageFlags.HostGame) + { + GameOptions.Add(clientId, Message00HostGameC2S.Deserialize(message)); + } + else if (Connections.TryGetValue(clientId, out var client)) + { + await client.Client!.HandleMessageAsync(message, messageType); + } + + break; + } + + case RecordedPacketType.GameCreated: + _gameCodeFactory.Result = GameCode.From(reader.ReadString()); + + await _gameManager.CreateAsync(GameOptions[clientId]); + + GameOptions.Remove(clientId); + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + } +} diff --git a/Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat b/Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat Binary files differnew file mode 100644 index 0000000..e5f441b --- /dev/null +++ b/Impostor-dev/src/Impostor.Tools.ServerReplay/sessions/session_1604255331821_dead_player_exception.dat diff --git a/Impostor-dev/src/Impostor.sln b/Impostor-dev/src/Impostor.sln new file mode 100644 index 0000000..b9706de --- /dev/null +++ b/Impostor-dev/src/Impostor.sln @@ -0,0 +1,177 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30503.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tests", "Impostor.Tests\Impostor.Tests.csproj", "{C1385C67-A7DD-46D2-80F6-FA428B85FB22}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{56DD9707-D811-4056-9E2C-8A9CC2479B07}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.Proxy", "Impostor.Tools.Proxy\Impostor.Tools.Proxy.csproj", "{D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Server", "Impostor.Server\Impostor.Server.csproj", "{1B0390AF-A4F3-4FE4-B093-708B0135C0B3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Api", "Impostor.Api\Impostor.Api.csproj", "{E096A7D7-D693-4A13-A526-38CC574D84F8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "patcher", "patcher", "{94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Shared", "Impostor.Patcher\Impostor.Patcher.Shared\Impostor.Patcher.Shared.csproj", "{7C3EB599-2292-4532-B280-D5BED1094DD4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.WinForms", "Impostor.Patcher\Impostor.Patcher.WinForms\Impostor.Patcher.WinForms.csproj", "{804CF172-0C87-4423-9688-BD97D549891E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Patcher.Cli", "Impostor.Patcher\Impostor.Patcher.Cli\Impostor.Patcher.Cli.csproj", "{82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Debugger", "Impostor.Plugins.Debugger\Impostor.Plugins.Debugger.csproj", "{ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Hazel", "Impostor.Hazel\Impostor.Hazel.csproj", "{671B753B-31AE-4C36-AD71-09CF00FA17CA}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "client", "client", "{9F1919B0-915B-4749-9944-697DF7E7F67F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client", "Impostor.Client\Impostor.Client.csproj", "{BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Client.App", "Impostor.Client.App\Impostor.Client.App.csproj", "{3DF86F12-7099-44F6-B98B-A148213D60B1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Tools.ServerReplay", "Impostor.Tools.ServerReplay\Impostor.Tools.ServerReplay.csproj", "{4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Plugins.Example", "Impostor.Plugins.Example\Impostor.Plugins.Example.csproj", "{70F97BB7-12A1-4C7A-B2A5-B962D14B813E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Impostor.Benchmarks", "Impostor.Benchmarks\Impostor.Benchmarks.csproj", "{EA04E386-6CCB-4C52-8C82-64F32C7EB377}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|x86.ActiveCfg = Debug|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Debug|x86.Build.0 = Debug|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|Any CPU.Build.0 = Release|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|x86.ActiveCfg = Release|Any CPU + {1B0390AF-A4F3-4FE4-B093-708B0135C0B3}.Release|x86.Build.0 = Release|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|x86.ActiveCfg = Debug|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Debug|x86.Build.0 = Debug|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|Any CPU.Build.0 = Release|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|x86.ActiveCfg = Release|Any CPU + {C1385C67-A7DD-46D2-80F6-FA428B85FB22}.Release|x86.Build.0 = Release|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Debug|x86.Build.0 = Debug|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|Any CPU.Build.0 = Release|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|x86.ActiveCfg = Release|Any CPU + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D}.Release|x86.Build.0 = Release|Any CPU + {804CF172-0C87-4423-9688-BD97D549891E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {804CF172-0C87-4423-9688-BD97D549891E}.Debug|x86.ActiveCfg = Debug|Any CPU + {804CF172-0C87-4423-9688-BD97D549891E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {804CF172-0C87-4423-9688-BD97D549891E}.Release|x86.ActiveCfg = Release|Any CPU + {804CF172-0C87-4423-9688-BD97D549891E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.ActiveCfg = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Debug|x86.Build.0 = Debug|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|Any CPU.Build.0 = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.ActiveCfg = Release|Any CPU + {E096A7D7-D693-4A13-A526-38CC574D84F8}.Release|x86.Build.0 = Release|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.ActiveCfg = Debug|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Debug|x86.Build.0 = Debug|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|Any CPU.Build.0 = Release|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|x86.ActiveCfg = Release|Any CPU + {7C3EB599-2292-4532-B280-D5BED1094DD4}.Release|x86.Build.0 = Release|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|x86.ActiveCfg = Debug|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Debug|x86.Build.0 = Debug|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|Any CPU.Build.0 = Release|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.ActiveCfg = Release|Any CPU + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69}.Release|x86.Build.0 = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.ActiveCfg = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Debug|x86.Build.0 = Debug|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|Any CPU.Build.0 = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|x86.ActiveCfg = Release|Any CPU + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7}.Release|x86.Build.0 = Release|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|x86.ActiveCfg = Debug|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Debug|x86.Build.0 = Debug|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|Any CPU.Build.0 = Release|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.ActiveCfg = Release|Any CPU + {671B753B-31AE-4C36-AD71-09CF00FA17CA}.Release|x86.Build.0 = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.ActiveCfg = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Debug|x86.Build.0 = Debug|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|Any CPU.Build.0 = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.ActiveCfg = Release|Any CPU + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB}.Release|x86.Build.0 = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Debug|x86.Build.0 = Debug|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|Any CPU.Build.0 = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.ActiveCfg = Release|Any CPU + {3DF86F12-7099-44F6-B98B-A148213D60B1}.Release|x86.Build.0 = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|x86.ActiveCfg = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Debug|x86.Build.0 = Debug|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|Any CPU.Build.0 = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.ActiveCfg = Release|Any CPU + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99}.Release|x86.Build.0 = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|x86.ActiveCfg = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Debug|x86.Build.0 = Debug|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|Any CPU.Build.0 = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|x86.ActiveCfg = Release|Any CPU + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E}.Release|x86.Build.0 = Release|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|x86.ActiveCfg = Debug|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Debug|x86.Build.0 = Debug|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|Any CPU.Build.0 = Release|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|x86.ActiveCfg = Release|Any CPU + {EA04E386-6CCB-4C52-8C82-64F32C7EB377}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {06245255-E585-483C-A4AD-71B7B568C6C8} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D16B8DE9-8EE2-4F6A-9352-305D5AB0233D} = {56DD9707-D811-4056-9E2C-8A9CC2479B07} + {804CF172-0C87-4423-9688-BD97D549891E} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} + {7C3EB599-2292-4532-B280-D5BED1094DD4} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} + {82B36C4C-4EBD-49B7-A2DB-0B3308FBEF69} = {94FAED42-15BB-40CF-BE64-5D5C3B4ABC8F} + {ECBCAA3B-B974-41CF-AFFC-6F5AA4C42FA7} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124} + {BE44C4A8-A202-4B63-A3BA-AC0AB5E7A0EB} = {9F1919B0-915B-4749-9944-697DF7E7F67F} + {3DF86F12-7099-44F6-B98B-A148213D60B1} = {9F1919B0-915B-4749-9944-697DF7E7F67F} + {4DB56ADD-6D3D-4D0E-A047-9D7E7D40EF99} = {56DD9707-D811-4056-9E2C-8A9CC2479B07} + {70F97BB7-12A1-4C7A-B2A5-B962D14B813E} = {36AA9913-E6EA-4A6C-90E6-2FD3CC2E3124} + EndGlobalSection +EndGlobal |