This commit is contained in:
nizar 2026-02-10 18:21:30 +01:00
commit 2c6320512f
3516 changed files with 194596 additions and 0 deletions

65
.editorconfig Normal file
View file

@ -0,0 +1,65 @@
[*]
charset = utf-8
end_of_line = crlf
trim_trailing_whitespace = false
insert_final_newline = false
indent_style = space
indent_size = 4
# Microsoft .NET properties
csharp_new_line_before_members_in_object_initializers = false
csharp_preferred_modifier_order = public, private, protected, internal, new, abstract, virtual, sealed, override, static, readonly, extern, unsafe, volatile, async:suggestion
csharp_prefer_braces = true:hint
csharp_space_after_cast = false
csharp_style_var_elsewhere = false:error
csharp_style_var_for_built_in_types = false:error
csharp_style_var_when_type_is_apparent = true:error
dotnet_style_predefined_type_for_locals_parameters_members = true:hint
dotnet_style_predefined_type_for_member_access = true:hint
dotnet_style_qualification_for_event = false:hint
dotnet_style_qualification_for_field = false:hint
dotnet_style_qualification_for_method = false:hint
dotnet_style_qualification_for_property = false:hint
dotnet_style_require_accessibility_modifiers = for_non_interface_members:hint
# ReSharper properties
resharper_align_linq_query = true
resharper_align_multiline_binary_expressions_chain = false
resharper_align_multline_type_parameter_constrains = true
resharper_align_multline_type_parameter_list = true
resharper_apply_on_completion = true
resharper_autodetect_indent_settings = true
resharper_braces_redundant = true
resharper_constructor_or_destructor_body = expression_body
resharper_csharp_max_line_length = 200
resharper_csharp_stick_comment = false
resharper_indent_type_constraints = false
resharper_local_function_body = expression_body
resharper_method_or_operator_body = expression_body
resharper_parentheses_non_obvious_operations = equality
resharper_place_accessorholder_attribute_on_same_line = False
resharper_space_within_single_line_array_initializer_braces = true
resharper_use_indent_from_vs = false
# ReSharper inspection severities
resharper_arrange_constructor_or_destructor_body_highlighting = hint
resharper_arrange_local_function_body_highlighting = hint
resharper_arrange_method_or_operator_body_highlighting = hint
resharper_arrange_missing_parentheses_highlighting = hint
resharper_redundant_base_qualifier_highlighting = warning
resharper_web_config_module_not_resolved_highlighting = warning
resharper_web_config_type_not_resolved_highlighting = warning
resharper_web_config_wrong_module_highlighting = warning
[{*.yml,*.yaml}]
indent_style = space
indent_size = 2
[{.eslintrc,.babelrc,.stylelintrc,jest.config,bowerrc,*.jsb3,*.jsb2,*.json}]
indent_style = space
indent_size = 2
[*.{appxmanifest,asax,ascx,aspx,build,cs,cshtml,dtd,fs,fsi,fsscript,fsx,master,ml,mli,nuspec,razor,resw,resx,skin,vb,xaml,xamlx,xoml,xsd}]
indent_style = space
indent_size = 4
tab_width = 4

63
.gitattributes vendored Normal file
View file

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

660
.gitignore vendored Normal file
View file

@ -0,0 +1,660 @@
### ASPNETCore ###
## 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/
dist/
[Bb]in/
[Oo]bj/
[Ll]og/
output/
# 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
# Visual Studio code coverage results
*.coverage
*.coveragexml
# 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
# Cake - Uncomment if you are using it
# tools/
### Csharp ###
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
# User-specific files (MonoDevelop/Xamarin Studio)
# Mono auto generated files
mono_crash.*
# Build results
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
# Visual Studio 2015/2017 cache/options directory
# Uncomment if you have tasks that create the project's static files in wwwroot
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
# NUNIT
# Build Results of an ATL Project
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_h.h
*.iobj
*.ipdb
*_wpftmp.csproj
# Chutzpah Test files
# Visual C++ cache files
# Visual Studio profiler
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
# Guidance Automation Toolkit
# ReSharper is a .NET coding add-in
# JustCode is a .NET coding add-in
# TeamCity is a build add-in
# DotCover is a Code Coverage Tool
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
# NCrunch
# MightyMoose
# Web workbench (sass)
# Installshield output folder
# DocProject is a documentation generator add-in
# Click-Once directory
# Publish Web Output
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
# 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
# NuGet Packages
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
# Microsoft Azure Build Output
# Microsoft Azure Emulator
# Windows Store app package directories and files
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
# RIA/Silverlight projects
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.ndf
# Business Intelligence projects
*.rptproj.rsuser
*- Backup*.rdl
# Microsoft Fakes
# GhostDoc plugin setting file
# Node.js Tools for Visual Studio
# Visual Studio 6 build log
# Visual Studio 6 workspace options file
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
# Paket dependency manager
# FAKE - F# Make
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
### Go ###
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
### Go Patch ###
/vendor/
/Godeps/
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
### VisualStudio ###
# User-specific files
# User-specific files (MonoDevelop/Xamarin Studio)
# Mono auto generated files
# Build results
# Visual Studio 2015/2017 cache/options directory
# Uncomment if you have tasks that create the project's static files in wwwroot
# Visual Studio 2017 auto generated files
# MSTest test Results
# NUNIT
# Build Results of an ATL Project
# Benchmark Results
# .NET Core
# StyleCop
# Files built by Visual Studio
# Chutzpah Test files
# Visual C++ cache files
# Visual Studio profiler
# Visual Studio Trace Files
# TFS 2012 Local Workspace
# Guidance Automation Toolkit
# ReSharper is a .NET coding add-in
# JustCode is a .NET coding add-in
# TeamCity is a build add-in
# DotCover is a Code Coverage Tool
# AxoCover is a Code Coverage Tool
# Visual Studio code coverage results
# NCrunch
# MightyMoose
# Web workbench (sass)
# Installshield output folder
# DocProject is a documentation generator add-in
# Click-Once directory
# Publish Web Output
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
# 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
# NuGet Packages
# The packages folder can be ignored because of Package Restore
# except build/, which is used as an MSBuild target.
# Uncomment if necessary however generally it will be regenerated when needed
# NuGet v3's project.json files produces more ignorable files
# Microsoft Azure Build Output
# Microsoft Azure Emulator
# Windows Store app package directories and files
# Visual Studio cache files
# files ending in .cache can be ignored
# but keep track of directories ending in .cache
# Others
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
# RIA/Silverlight projects
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
# SQL Server files
# Business Intelligence projects
# Microsoft Fakes
# GhostDoc plugin setting file
# Node.js Tools for Visual Studio
# Visual Studio 6 build log
# Visual Studio 6 workspace options file
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
# Visual Studio LightSwitch build output
# Paket dependency manager
# FAKE - F# Make
# CodeRush personal settings
# Python Tools for Visual Studio (PTVS)
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
# Telerik's JustMock configuration file
# BizTalk build output
# OpenCover UI analysis results
# Azure Stream Analytics local run output
# MSBuild Binary and Structured Log
# NVidia Nsight GPU debugger configuration file
# MFractors (Xamarin productivity tool) working folder
# Local History for Visual Studio
# BeatPulse healthcheck temp database
# Backup folder for Package Reference Convert tool in Visual Studio 2017
configs/parse_command.yaml
# Config files
srcs/GameChannel/config/

1192
README.md Normal file

File diff suppressed because it is too large Load diff

867
WingsEmu.sln Normal file
View file

@ -0,0 +1,867 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31919.166
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Executables", "Executables", "{0D88B727-CA5D-48DB-835B-E90C8E282DA7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Communication.gRPC", "srcs\WingsEmu.Communication.gRPC\WingsEmu.Communication.gRPC.csproj", "{15948D13-B424-4DF3-B341-36CCB97D4D9F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WingsAPI", "WingsAPI", "{404B2AB4-CAFA-4AF7-9593-F231A25A973C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Packets", "srcs\WingsAPI.Packets\WingsAPI.Packets.csproj", "{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Data", "srcs\WingsAPI.Data\WingsAPI.Data.csproj", "{0F1F75EF-8755-499B-8A28-F6653EB29FED}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Game", "srcs\WingsAPI.Game\WingsAPI.Game.csproj", "{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Toolkit", "srcs\Toolkit\Toolkit.csproj", "{05451402-CA5D-4474-A8BE-8E534AD17748}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{823AD0CF-E764-4113-A166-6A615A4B928F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{F872A038-D325-4718-B0A0-4CBCEA3714ED}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Game.Extensions", "srcs\WingsAPI.Game.Extensions\WingsAPI.Game.Extensions.csproj", "{D425AE9F-F25D-4B64-9983-FE76778524A7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.DB.EF", "srcs\_plugins\Plugin.DB.EF\Plugin.DB.EF.csproj", "{8ED45EF3-0271-4867-9330-80D300029D8C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Commands", "srcs\WingsAPI.Commands\WingsAPI.Commands.csproj", "{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.Essentials", "srcs\_plugins\WingsEmu.Plugins.Essentials\WingsEmu.Plugins.Essentials.csproj", "{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Master", "srcs\Master\Master.csproj", "{19A46232-72C5-4BB7-AE6B-A9062BE259A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameChannel", "srcs\GameChannel\GameChannel.csproj", "{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LoginServer", "srcs\LoginServer\LoginServer.csproj", "{D4506804-B9A1-4E1A-B97C-0F815814FE62}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.BasicImplementations", "srcs\_plugins\WingsEmu.Plugins.BasicImplementation\WingsEmu.Plugins.BasicImplementations.csproj", "{D6B19207-B3E3-4B53-9964-19DEFB2089EF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.PacketHandling", "srcs\_plugins\WingsEmu.Plugins.PacketHandling\WingsEmu.Plugins.PacketHandling.csproj", "{F839354B-16C3-4EEF-8872-768B6D832BB2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.DistributedGameEvents", "srcs\_plugins\WingsEmu.Plugins.DistributedGameEvents\WingsEmu.Plugins.DistributedGameEvents.csproj", "{5707003E-0835-4E7A-BAD8-E55F4AE0D583}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Communication", "srcs\WingsAPI.Communication\WingsAPI.Communication.csproj", "{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Plugins.GameEvents", "srcs\_plugins\WingsEmu.Plugins.GameEvents\WingsEmu.Plugins.GameEvents.csproj", "{A57EA67A-133B-4217-BE17-9E25622A95DD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scheduler", "srcs\Scheduler\Scheduler.csproj", "{105F39CC-E6C1-4365-B200-75D451E5747F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordNotifier", "srcs\DiscordNotifier\DiscordNotifier.csproj", "{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "srcs", "srcs", "{322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FamilyServer", "srcs\FamilyServer\FamilyServer.csproj", "{5F2C76AE-81B2-4A24-80D2-086EFC41627B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseServer", "srcs\DatabaseServer\DatabaseServer.csproj", "{18F61C6F-997C-459F-A3CE-63AEE311315E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LogsServer", "srcs\LogsServer\LogsServer.csproj", "{7B5A36CE-10AE-4915-84C2-61C205F4C004}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MailServer", "srcs\MailServer\MailServer.csproj", "{58B6177B-2C56-4B37-88AB-8C454767D427}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BazaarServer", "srcs\BazaarServer\BazaarServer.csproj", "{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Scripting.LUA", "srcs\WingsAPI.Scripting.LUA\WingsAPI.Scripting.LUA.csproj", "{D7F1A231-791A-4B34-A9B2-21282EADA82C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RelationServer", "srcs\RelationServer\RelationServer.csproj", "{42CDD6F5-A858-4A32-8982-E5A63689CD58}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.PlayerLogs", "srcs\_plugins\Plugin.PlayerLogs\Plugin.PlayerLogs.csproj", "{779A5516-5209-491C-878A-63086DB3F566}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Raids", "srcs\_plugins\Plugin.Raids\Plugin.Raids.csproj", "{8293EEC9-C747-46C3-B3D2-30C552620269}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.Act4", "srcs\_plugins\Plugin.Act4\Plugin.Act4.csproj", "{DA4852B6-2FBD-4E5C-972C-049626152E4E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Scripting", "srcs\WingsAPI.Scripting\WingsAPI.Scripting.csproj", "{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.TimeSpaces", "srcs\_plugins\Plugin.TimeSpaces\Plugin.TimeSpaces.csproj", "{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.CoreImpl", "srcs\_plugins\Plugin.CoreImpl\Plugin.CoreImpl.csproj", "{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.FamilyImpl", "srcs\_plugins\Plugin.FamilyImpl\Plugin.FamilyImpl.csproj", "{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.QuestImpl", "srcs\_plugins\Plugin.QuestImpl\Plugin.QuestImpl.csproj", "{793F6EA6-6842-4E8F-908D-C7423734E385}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Plugins", "srcs\WingsAPI.Plugins\WingsAPI.Plugins.csproj", "{8D22A73C-581D-4A85-917F-C56DFB386C6F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsEmu.Health", "srcs\WingsEmu.Health\WingsEmu.Health.csproj", "{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WingsAPI.Packets.Handling", "srcs\WingsAPI.Packets.Handling\WingsAPI.Packets.Handling.csproj", "{9892ED34-E662-48A2-95AB-F1655412081C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.MongoLogs", "srcs\_plugins\Plugin.MongoLogs\Plugin.MongoLogs.csproj", "{55094771-469E-4E1A-8E7B-C7B915E023D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.ResourceLoader", "srcs\_plugins\Plugin.ResourceLoader\Plugin.ResourceLoader.csproj", "{50A948F4-EF18-4591-8CA9-564909B630A1}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TranslationsServer", "srcs\TranslationsServer\TranslationsServer.csproj", "{A3BDBA44-A720-48FA-8914-736B79830CE9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugin.RainbowBattle", "srcs\Plugin.RainbowBattle\Plugin.RainbowBattle.csproj", "{192ECC49-3C79-48CD-A49B-296CC47546F5}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PheonixLib", "PheonixLib", "{368BD8A7-67D2-4F24-B655-C3B82BBDA840}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Auth.JWT", "srcs\PhoenixLib.Auth.JWT\PhoenixLib.Auth.JWT.csproj", "{FAC76C2D-D586-4231-BF60-7766969BBD60}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Caching", "srcs\PhoenixLib.Caching\PhoenixLib.Caching.csproj", "{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Configuration", "srcs\PhoenixLib.Configuration\PhoenixLib.Configuration.csproj", "{45104F9C-A786-442B-8202-B965530AE20E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.Abstractions", "srcs\PhoenixLib.DAL.Abstractions\PhoenixLib.DAL.Abstractions.csproj", "{2A8189FE-0E9B-474F-91E7-A92C7C302A43}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.EFCore.PGSQL", "srcs\PhoenixLib.DAL.EFCore.PGSQL\PhoenixLib.DAL.EFCore.PGSQL.csproj", "{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.MongoDB", "srcs\PhoenixLib.DAL.MongoDB\PhoenixLib.DAL.MongoDB.csproj", "{1565B041-B687-4EF1-B27D-B0F9A927847B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.DAL.Redis", "srcs\PhoenixLib.DAL.Redis\PhoenixLib.DAL.Redis.csproj", "{E444200C-5B5D-42C0-88C5-D0E2C11A393F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Events", "srcs\PhoenixLib.Events\PhoenixLib.Events.csproj", "{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Extensions", "srcs\PhoenixLib.Extensions\PhoenixLib.Extensions.csproj", "{F81D94AB-E336-405D-B7E3-8AC97EE4E758}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Logging", "srcs\PhoenixLib.Logging\PhoenixLib.Logging.csproj", "{192C9441-BA98-41D4-A27E-D7DE95BB46F7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Messaging", "srcs\PhoenixLib.Messaging\PhoenixLib.Messaging.csproj", "{D592C239-AFAD-4596-8FE9-BBEA4977B258}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Multilanguage", "srcs\PhoenixLib.Multilanguage\PhoenixLib.Multilanguage.csproj", "{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Scheduler", "srcs\PhoenixLib.Scheduler\PhoenixLib.Scheduler.csproj", "{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PhoenixLib.Scheduler.ReactiveX", "srcs\PhoenixLib.Scheduler.ReactiveX\PhoenixLib.Scheduler.ReactiveX.csproj", "{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x64.ActiveCfg = Debug|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x64.Build.0 = Debug|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x86.ActiveCfg = Debug|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Debug|x86.Build.0 = Debug|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|Any CPU.Build.0 = Release|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x64.ActiveCfg = Release|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x64.Build.0 = Release|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x86.ActiveCfg = Release|Any CPU
{15948D13-B424-4DF3-B341-36CCB97D4D9F}.Release|x86.Build.0 = Release|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|Any CPU.Build.0 = Debug|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x64.ActiveCfg = Debug|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x64.Build.0 = Debug|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x86.ActiveCfg = Debug|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Debug|x86.Build.0 = Debug|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|Any CPU.ActiveCfg = Release|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|Any CPU.Build.0 = Release|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x64.ActiveCfg = Release|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x64.Build.0 = Release|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x86.ActiveCfg = Release|Any CPU
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94}.Release|x86.Build.0 = Release|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x64.ActiveCfg = Debug|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x64.Build.0 = Debug|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x86.ActiveCfg = Debug|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Debug|x86.Build.0 = Debug|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|Any CPU.Build.0 = Release|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x64.ActiveCfg = Release|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x64.Build.0 = Release|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x86.ActiveCfg = Release|Any CPU
{0F1F75EF-8755-499B-8A28-F6653EB29FED}.Release|x86.Build.0 = Release|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x64.ActiveCfg = Debug|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x64.Build.0 = Debug|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x86.ActiveCfg = Debug|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Debug|x86.Build.0 = Debug|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|Any CPU.Build.0 = Release|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x64.ActiveCfg = Release|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x64.Build.0 = Release|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x86.ActiveCfg = Release|Any CPU
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA}.Release|x86.Build.0 = Release|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x64.ActiveCfg = Debug|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x64.Build.0 = Debug|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x86.ActiveCfg = Debug|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Debug|x86.Build.0 = Debug|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Release|Any CPU.Build.0 = Release|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x64.ActiveCfg = Release|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x64.Build.0 = Release|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x86.ActiveCfg = Release|Any CPU
{05451402-CA5D-4474-A8BE-8E534AD17748}.Release|x86.Build.0 = Release|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x64.ActiveCfg = Debug|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x64.Build.0 = Debug|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x86.ActiveCfg = Debug|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Debug|x86.Build.0 = Debug|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|Any CPU.Build.0 = Release|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x64.ActiveCfg = Release|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x64.Build.0 = Release|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x86.ActiveCfg = Release|Any CPU
{D425AE9F-F25D-4B64-9983-FE76778524A7}.Release|x86.Build.0 = Release|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x64.ActiveCfg = Debug|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x64.Build.0 = Debug|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x86.ActiveCfg = Debug|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Debug|x86.Build.0 = Debug|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Release|Any CPU.Build.0 = Release|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x64.ActiveCfg = Release|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x64.Build.0 = Release|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x86.ActiveCfg = Release|Any CPU
{8ED45EF3-0271-4867-9330-80D300029D8C}.Release|x86.Build.0 = Release|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x64.ActiveCfg = Debug|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x64.Build.0 = Debug|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x86.ActiveCfg = Debug|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Debug|x86.Build.0 = Debug|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|Any CPU.Build.0 = Release|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x64.ActiveCfg = Release|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x64.Build.0 = Release|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x86.ActiveCfg = Release|Any CPU
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3}.Release|x86.Build.0 = Release|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x64.ActiveCfg = Debug|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x64.Build.0 = Debug|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x86.ActiveCfg = Debug|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Debug|x86.Build.0 = Debug|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|Any CPU.Build.0 = Release|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x64.ActiveCfg = Release|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x64.Build.0 = Release|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x86.ActiveCfg = Release|Any CPU
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2}.Release|x86.Build.0 = Release|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x64.ActiveCfg = Debug|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x64.Build.0 = Debug|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x86.ActiveCfg = Debug|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Debug|x86.Build.0 = Debug|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|Any CPU.Build.0 = Release|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x64.ActiveCfg = Release|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x64.Build.0 = Release|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x86.ActiveCfg = Release|Any CPU
{19A46232-72C5-4BB7-AE6B-A9062BE259A0}.Release|x86.Build.0 = Release|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x64.ActiveCfg = Debug|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x64.Build.0 = Debug|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x86.ActiveCfg = Debug|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Debug|x86.Build.0 = Debug|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|Any CPU.Build.0 = Release|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x64.ActiveCfg = Release|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x64.Build.0 = Release|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x86.ActiveCfg = Release|Any CPU
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0}.Release|x86.Build.0 = Release|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x64.ActiveCfg = Debug|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x64.Build.0 = Debug|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x86.ActiveCfg = Debug|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Debug|x86.Build.0 = Debug|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|Any CPU.Build.0 = Release|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x64.ActiveCfg = Release|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x64.Build.0 = Release|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x86.ActiveCfg = Release|Any CPU
{D4506804-B9A1-4E1A-B97C-0F815814FE62}.Release|x86.Build.0 = Release|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x64.ActiveCfg = Debug|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x64.Build.0 = Debug|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x86.ActiveCfg = Debug|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Debug|x86.Build.0 = Debug|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|Any CPU.Build.0 = Release|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x64.ActiveCfg = Release|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x64.Build.0 = Release|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x86.ActiveCfg = Release|Any CPU
{D6B19207-B3E3-4B53-9964-19DEFB2089EF}.Release|x86.Build.0 = Release|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x64.ActiveCfg = Debug|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x64.Build.0 = Debug|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x86.ActiveCfg = Debug|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Debug|x86.Build.0 = Debug|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|Any CPU.Build.0 = Release|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x64.ActiveCfg = Release|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x64.Build.0 = Release|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x86.ActiveCfg = Release|Any CPU
{F839354B-16C3-4EEF-8872-768B6D832BB2}.Release|x86.Build.0 = Release|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x64.ActiveCfg = Debug|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x64.Build.0 = Debug|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x86.ActiveCfg = Debug|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Debug|x86.Build.0 = Debug|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|Any CPU.Build.0 = Release|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x64.ActiveCfg = Release|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x64.Build.0 = Release|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x86.ActiveCfg = Release|Any CPU
{5707003E-0835-4E7A-BAD8-E55F4AE0D583}.Release|x86.Build.0 = Release|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x64.ActiveCfg = Debug|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x64.Build.0 = Debug|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x86.ActiveCfg = Debug|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Debug|x86.Build.0 = Debug|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|Any CPU.Build.0 = Release|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x64.ActiveCfg = Release|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x64.Build.0 = Release|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x86.ActiveCfg = Release|Any CPU
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F}.Release|x86.Build.0 = Release|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x64.ActiveCfg = Debug|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x64.Build.0 = Debug|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x86.ActiveCfg = Debug|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Debug|x86.Build.0 = Debug|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|Any CPU.Build.0 = Release|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x64.ActiveCfg = Release|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x64.Build.0 = Release|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x86.ActiveCfg = Release|Any CPU
{A57EA67A-133B-4217-BE17-9E25622A95DD}.Release|x86.Build.0 = Release|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x64.ActiveCfg = Debug|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x64.Build.0 = Debug|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x86.ActiveCfg = Debug|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Debug|x86.Build.0 = Debug|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Release|Any CPU.Build.0 = Release|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x64.ActiveCfg = Release|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x64.Build.0 = Release|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x86.ActiveCfg = Release|Any CPU
{105F39CC-E6C1-4365-B200-75D451E5747F}.Release|x86.Build.0 = Release|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x64.ActiveCfg = Debug|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x64.Build.0 = Debug|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x86.ActiveCfg = Debug|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Debug|x86.Build.0 = Debug|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|Any CPU.Build.0 = Release|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x64.ActiveCfg = Release|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x64.Build.0 = Release|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x86.ActiveCfg = Release|Any CPU
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0}.Release|x86.Build.0 = Release|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x64.ActiveCfg = Debug|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x64.Build.0 = Debug|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x86.ActiveCfg = Debug|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Debug|x86.Build.0 = Debug|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|Any CPU.Build.0 = Release|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x64.ActiveCfg = Release|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x64.Build.0 = Release|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x86.ActiveCfg = Release|Any CPU
{5F2C76AE-81B2-4A24-80D2-086EFC41627B}.Release|x86.Build.0 = Release|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x64.ActiveCfg = Debug|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x64.Build.0 = Debug|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x86.ActiveCfg = Debug|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Debug|x86.Build.0 = Debug|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|Any CPU.Build.0 = Release|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x64.ActiveCfg = Release|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x64.Build.0 = Release|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x86.ActiveCfg = Release|Any CPU
{18F61C6F-997C-459F-A3CE-63AEE311315E}.Release|x86.Build.0 = Release|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x64.ActiveCfg = Debug|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x64.Build.0 = Debug|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x86.ActiveCfg = Debug|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Debug|x86.Build.0 = Debug|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|Any CPU.Build.0 = Release|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x64.ActiveCfg = Release|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x64.Build.0 = Release|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x86.ActiveCfg = Release|Any CPU
{7B5A36CE-10AE-4915-84C2-61C205F4C004}.Release|x86.Build.0 = Release|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|Any CPU.Build.0 = Debug|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x64.ActiveCfg = Debug|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x64.Build.0 = Debug|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x86.ActiveCfg = Debug|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Debug|x86.Build.0 = Debug|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Release|Any CPU.ActiveCfg = Release|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Release|Any CPU.Build.0 = Release|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x64.ActiveCfg = Release|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x64.Build.0 = Release|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x86.ActiveCfg = Release|Any CPU
{58B6177B-2C56-4B37-88AB-8C454767D427}.Release|x86.Build.0 = Release|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x64.ActiveCfg = Debug|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x64.Build.0 = Debug|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x86.ActiveCfg = Debug|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Debug|x86.Build.0 = Debug|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|Any CPU.Build.0 = Release|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x64.ActiveCfg = Release|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x64.Build.0 = Release|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x86.ActiveCfg = Release|Any CPU
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE}.Release|x86.Build.0 = Release|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x64.ActiveCfg = Debug|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x64.Build.0 = Debug|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x86.ActiveCfg = Debug|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Debug|x86.Build.0 = Debug|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|Any CPU.Build.0 = Release|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x64.ActiveCfg = Release|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x64.Build.0 = Release|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x86.ActiveCfg = Release|Any CPU
{D7F1A231-791A-4B34-A9B2-21282EADA82C}.Release|x86.Build.0 = Release|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|Any CPU.Build.0 = Debug|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x64.ActiveCfg = Debug|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x64.Build.0 = Debug|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x86.ActiveCfg = Debug|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Debug|x86.Build.0 = Debug|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|Any CPU.ActiveCfg = Release|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|Any CPU.Build.0 = Release|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x64.ActiveCfg = Release|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x64.Build.0 = Release|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x86.ActiveCfg = Release|Any CPU
{42CDD6F5-A858-4A32-8982-E5A63689CD58}.Release|x86.Build.0 = Release|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Debug|Any CPU.Build.0 = Debug|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Debug|x64.ActiveCfg = Debug|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Debug|x64.Build.0 = Debug|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Debug|x86.ActiveCfg = Debug|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Debug|x86.Build.0 = Debug|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Release|Any CPU.ActiveCfg = Release|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Release|Any CPU.Build.0 = Release|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Release|x64.ActiveCfg = Release|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Release|x64.Build.0 = Release|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Release|x86.ActiveCfg = Release|Any CPU
{779A5516-5209-491C-878A-63086DB3F566}.Release|x86.Build.0 = Release|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x64.ActiveCfg = Debug|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x64.Build.0 = Debug|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x86.ActiveCfg = Debug|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Debug|x86.Build.0 = Debug|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Release|Any CPU.Build.0 = Release|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x64.ActiveCfg = Release|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x64.Build.0 = Release|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x86.ActiveCfg = Release|Any CPU
{8293EEC9-C747-46C3-B3D2-30C552620269}.Release|x86.Build.0 = Release|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x64.ActiveCfg = Debug|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x64.Build.0 = Debug|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x86.ActiveCfg = Debug|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Debug|x86.Build.0 = Debug|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|Any CPU.Build.0 = Release|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x64.ActiveCfg = Release|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x64.Build.0 = Release|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x86.ActiveCfg = Release|Any CPU
{DA4852B6-2FBD-4E5C-972C-049626152E4E}.Release|x86.Build.0 = Release|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x64.ActiveCfg = Debug|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x64.Build.0 = Debug|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x86.ActiveCfg = Debug|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Debug|x86.Build.0 = Debug|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|Any CPU.Build.0 = Release|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x64.ActiveCfg = Release|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x64.Build.0 = Release|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x86.ActiveCfg = Release|Any CPU
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA}.Release|x86.Build.0 = Release|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x64.ActiveCfg = Debug|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x64.Build.0 = Debug|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x86.ActiveCfg = Debug|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Debug|x86.Build.0 = Debug|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|Any CPU.Build.0 = Release|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x64.ActiveCfg = Release|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x64.Build.0 = Release|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x86.ActiveCfg = Release|Any CPU
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5}.Release|x86.Build.0 = Release|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x64.ActiveCfg = Debug|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x64.Build.0 = Debug|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x86.ActiveCfg = Debug|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Debug|x86.Build.0 = Debug|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|Any CPU.Build.0 = Release|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x64.ActiveCfg = Release|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x64.Build.0 = Release|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x86.ActiveCfg = Release|Any CPU
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7}.Release|x86.Build.0 = Release|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x64.ActiveCfg = Debug|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x64.Build.0 = Debug|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x86.ActiveCfg = Debug|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Debug|x86.Build.0 = Debug|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|Any CPU.Build.0 = Release|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x64.ActiveCfg = Release|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x64.Build.0 = Release|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x86.ActiveCfg = Release|Any CPU
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5}.Release|x86.Build.0 = Release|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|Any CPU.Build.0 = Debug|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x64.ActiveCfg = Debug|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x64.Build.0 = Debug|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x86.ActiveCfg = Debug|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Debug|x86.Build.0 = Debug|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Release|Any CPU.ActiveCfg = Release|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Release|Any CPU.Build.0 = Release|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x64.ActiveCfg = Release|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x64.Build.0 = Release|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x86.ActiveCfg = Release|Any CPU
{793F6EA6-6842-4E8F-908D-C7423734E385}.Release|x86.Build.0 = Release|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x64.ActiveCfg = Debug|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x64.Build.0 = Debug|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x86.ActiveCfg = Debug|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Debug|x86.Build.0 = Debug|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|Any CPU.Build.0 = Release|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x64.ActiveCfg = Release|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x64.Build.0 = Release|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x86.ActiveCfg = Release|Any CPU
{8D22A73C-581D-4A85-917F-C56DFB386C6F}.Release|x86.Build.0 = Release|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x64.ActiveCfg = Debug|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x64.Build.0 = Debug|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x86.ActiveCfg = Debug|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Debug|x86.Build.0 = Debug|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|Any CPU.Build.0 = Release|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x64.ActiveCfg = Release|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x64.Build.0 = Release|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x86.ActiveCfg = Release|Any CPU
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8}.Release|x86.Build.0 = Release|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x64.ActiveCfg = Debug|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x64.Build.0 = Debug|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x86.ActiveCfg = Debug|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Debug|x86.Build.0 = Debug|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Release|Any CPU.Build.0 = Release|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Release|x64.ActiveCfg = Release|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Release|x64.Build.0 = Release|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Release|x86.ActiveCfg = Release|Any CPU
{9892ED34-E662-48A2-95AB-F1655412081C}.Release|x86.Build.0 = Release|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x64.ActiveCfg = Debug|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x64.Build.0 = Debug|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x86.ActiveCfg = Debug|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Debug|x86.Build.0 = Debug|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|Any CPU.Build.0 = Release|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x64.ActiveCfg = Release|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x64.Build.0 = Release|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x86.ActiveCfg = Release|Any CPU
{55094771-469E-4E1A-8E7B-C7B915E023D0}.Release|x86.Build.0 = Release|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x64.ActiveCfg = Debug|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x64.Build.0 = Debug|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x86.ActiveCfg = Debug|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Debug|x86.Build.0 = Debug|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Release|Any CPU.Build.0 = Release|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x64.ActiveCfg = Release|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x64.Build.0 = Release|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x86.ActiveCfg = Release|Any CPU
{50A948F4-EF18-4591-8CA9-564909B630A1}.Release|x86.Build.0 = Release|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x64.ActiveCfg = Debug|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x64.Build.0 = Debug|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x86.ActiveCfg = Debug|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Debug|x86.Build.0 = Debug|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|Any CPU.Build.0 = Release|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x64.ActiveCfg = Release|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x64.Build.0 = Release|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x86.ActiveCfg = Release|Any CPU
{A3BDBA44-A720-48FA-8914-736B79830CE9}.Release|x86.Build.0 = Release|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x64.ActiveCfg = Debug|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x64.Build.0 = Debug|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x86.ActiveCfg = Debug|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Debug|x86.Build.0 = Debug|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|Any CPU.Build.0 = Release|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x64.ActiveCfg = Release|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x64.Build.0 = Release|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x86.ActiveCfg = Release|Any CPU
{192ECC49-3C79-48CD-A49B-296CC47546F5}.Release|x86.Build.0 = Release|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x64.ActiveCfg = Debug|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x64.Build.0 = Debug|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x86.ActiveCfg = Debug|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Debug|x86.Build.0 = Debug|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|Any CPU.Build.0 = Release|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x64.ActiveCfg = Release|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x64.Build.0 = Release|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x86.ActiveCfg = Release|Any CPU
{FAC76C2D-D586-4231-BF60-7766969BBD60}.Release|x86.Build.0 = Release|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x64.ActiveCfg = Debug|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x64.Build.0 = Debug|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x86.ActiveCfg = Debug|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Debug|x86.Build.0 = Debug|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|Any CPU.Build.0 = Release|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x64.ActiveCfg = Release|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x64.Build.0 = Release|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x86.ActiveCfg = Release|Any CPU
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143}.Release|x86.Build.0 = Release|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Debug|x64.ActiveCfg = Debug|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Debug|x64.Build.0 = Debug|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Debug|x86.ActiveCfg = Debug|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Debug|x86.Build.0 = Debug|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Release|Any CPU.Build.0 = Release|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Release|x64.ActiveCfg = Release|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Release|x64.Build.0 = Release|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Release|x86.ActiveCfg = Release|Any CPU
{45104F9C-A786-442B-8202-B965530AE20E}.Release|x86.Build.0 = Release|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x64.ActiveCfg = Debug|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x64.Build.0 = Debug|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x86.ActiveCfg = Debug|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Debug|x86.Build.0 = Debug|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|Any CPU.Build.0 = Release|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x64.ActiveCfg = Release|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x64.Build.0 = Release|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x86.ActiveCfg = Release|Any CPU
{2A8189FE-0E9B-474F-91E7-A92C7C302A43}.Release|x86.Build.0 = Release|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x64.ActiveCfg = Debug|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x64.Build.0 = Debug|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x86.ActiveCfg = Debug|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Debug|x86.Build.0 = Debug|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|Any CPU.Build.0 = Release|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x64.ActiveCfg = Release|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x64.Build.0 = Release|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x86.ActiveCfg = Release|Any CPU
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0}.Release|x86.Build.0 = Release|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x64.ActiveCfg = Debug|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x64.Build.0 = Debug|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x86.ActiveCfg = Debug|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Debug|x86.Build.0 = Debug|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|Any CPU.Build.0 = Release|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x64.ActiveCfg = Release|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x64.Build.0 = Release|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x86.ActiveCfg = Release|Any CPU
{1565B041-B687-4EF1-B27D-B0F9A927847B}.Release|x86.Build.0 = Release|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x64.ActiveCfg = Debug|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x64.Build.0 = Debug|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x86.ActiveCfg = Debug|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Debug|x86.Build.0 = Debug|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|Any CPU.Build.0 = Release|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x64.ActiveCfg = Release|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x64.Build.0 = Release|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x86.ActiveCfg = Release|Any CPU
{E444200C-5B5D-42C0-88C5-D0E2C11A393F}.Release|x86.Build.0 = Release|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x64.ActiveCfg = Debug|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x64.Build.0 = Debug|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x86.ActiveCfg = Debug|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Debug|x86.Build.0 = Debug|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|Any CPU.Build.0 = Release|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x64.ActiveCfg = Release|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x64.Build.0 = Release|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x86.ActiveCfg = Release|Any CPU
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A}.Release|x86.Build.0 = Release|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x64.ActiveCfg = Debug|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x64.Build.0 = Debug|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x86.ActiveCfg = Debug|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Debug|x86.Build.0 = Debug|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|Any CPU.Build.0 = Release|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x64.ActiveCfg = Release|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x64.Build.0 = Release|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x86.ActiveCfg = Release|Any CPU
{F81D94AB-E336-405D-B7E3-8AC97EE4E758}.Release|x86.Build.0 = Release|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x64.ActiveCfg = Debug|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x64.Build.0 = Debug|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x86.ActiveCfg = Debug|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Debug|x86.Build.0 = Debug|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|Any CPU.Build.0 = Release|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x64.ActiveCfg = Release|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x64.Build.0 = Release|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x86.ActiveCfg = Release|Any CPU
{192C9441-BA98-41D4-A27E-D7DE95BB46F7}.Release|x86.Build.0 = Release|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x64.ActiveCfg = Debug|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x64.Build.0 = Debug|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x86.ActiveCfg = Debug|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Debug|x86.Build.0 = Debug|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|Any CPU.Build.0 = Release|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x64.ActiveCfg = Release|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x64.Build.0 = Release|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x86.ActiveCfg = Release|Any CPU
{D592C239-AFAD-4596-8FE9-BBEA4977B258}.Release|x86.Build.0 = Release|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x64.ActiveCfg = Debug|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x64.Build.0 = Debug|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x86.ActiveCfg = Debug|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Debug|x86.Build.0 = Debug|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|Any CPU.Build.0 = Release|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x64.ActiveCfg = Release|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x64.Build.0 = Release|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x86.ActiveCfg = Release|Any CPU
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A}.Release|x86.Build.0 = Release|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x64.ActiveCfg = Debug|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x64.Build.0 = Debug|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x86.ActiveCfg = Debug|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Debug|x86.Build.0 = Debug|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|Any CPU.Build.0 = Release|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x64.ActiveCfg = Release|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x64.Build.0 = Release|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x86.ActiveCfg = Release|Any CPU
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D}.Release|x86.Build.0 = Release|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x64.ActiveCfg = Debug|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x64.Build.0 = Debug|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x86.ActiveCfg = Debug|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Debug|x86.Build.0 = Debug|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|Any CPU.Build.0 = Release|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x64.ActiveCfg = Release|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x64.Build.0 = Release|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x86.ActiveCfg = Release|Any CPU
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0D88B727-CA5D-48DB-835B-E90C8E282DA7} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}
{15948D13-B424-4DF3-B341-36CCB97D4D9F} = {823AD0CF-E764-4113-A166-6A615A4B928F}
{404B2AB4-CAFA-4AF7-9593-F231A25A973C} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}
{596A8193-7A5D-4055-BB8C-D2F27E1C6B94} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{0F1F75EF-8755-499B-8A28-F6653EB29FED} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{295450C6-C37F-4AA7-A59A-1BFFF352D0EA} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{05451402-CA5D-4474-A8BE-8E534AD17748} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{823AD0CF-E764-4113-A166-6A615A4B928F} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}
{F872A038-D325-4718-B0A0-4CBCEA3714ED} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}
{D425AE9F-F25D-4B64-9983-FE76778524A7} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{8ED45EF3-0271-4867-9330-80D300029D8C} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{B0D05BBF-F7FA-46EC-9224-182B2B98C4A3} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{3F789B98-5A34-41CF-B4F0-70B03C0C5ED2} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{19A46232-72C5-4BB7-AE6B-A9062BE259A0} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{42DE9E8C-747E-46A4-9AC5-BB7ED1425BF0} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{D4506804-B9A1-4E1A-B97C-0F815814FE62} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{D6B19207-B3E3-4B53-9964-19DEFB2089EF} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{F839354B-16C3-4EEF-8872-768B6D832BB2} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{5707003E-0835-4E7A-BAD8-E55F4AE0D583} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{CA5FFE88-A5FF-42D3-9E88-220A594AC27F} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{A57EA67A-133B-4217-BE17-9E25622A95DD} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{105F39CC-E6C1-4365-B200-75D451E5747F} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{B84EBC46-1CF7-47C9-B38E-F4919A11D6D0} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{5F2C76AE-81B2-4A24-80D2-086EFC41627B} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{18F61C6F-997C-459F-A3CE-63AEE311315E} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{7B5A36CE-10AE-4915-84C2-61C205F4C004} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{58B6177B-2C56-4B37-88AB-8C454767D427} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{61DB7D8D-7903-47EB-85FA-1E21001C5EAE} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{D7F1A231-791A-4B34-A9B2-21282EADA82C} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{42CDD6F5-A858-4A32-8982-E5A63689CD58} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{779A5516-5209-491C-878A-63086DB3F566} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{8293EEC9-C747-46C3-B3D2-30C552620269} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{DA4852B6-2FBD-4E5C-972C-049626152E4E} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{CCBE134E-2DAB-4CE7-B9F2-4F315CDEF1FA} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{E4E9646F-7B85-4446-9B6D-846D32BAC3F5} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{3038B4F6-2CB4-4124-86EB-FE8D7FB3A0D7} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{E27BC1CD-D6DF-4EEE-B340-5424BE442AC5} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{793F6EA6-6842-4E8F-908D-C7423734E385} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{8D22A73C-581D-4A85-917F-C56DFB386C6F} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{DA7F7106-23D0-4DC2-8B71-43A15A6BBDE8} = {823AD0CF-E764-4113-A166-6A615A4B928F}
{9892ED34-E662-48A2-95AB-F1655412081C} = {404B2AB4-CAFA-4AF7-9593-F231A25A973C}
{55094771-469E-4E1A-8E7B-C7B915E023D0} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{50A948F4-EF18-4591-8CA9-564909B630A1} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{A3BDBA44-A720-48FA-8914-736B79830CE9} = {0D88B727-CA5D-48DB-835B-E90C8E282DA7}
{192ECC49-3C79-48CD-A49B-296CC47546F5} = {F872A038-D325-4718-B0A0-4CBCEA3714ED}
{368BD8A7-67D2-4F24-B655-C3B82BBDA840} = {322AAFB0-04E1-42B9-B9F1-B836E12E1FCD}
{FAC76C2D-D586-4231-BF60-7766969BBD60} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{1E379202-AE6A-4A0A-BFE9-C1C0D4605143} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{45104F9C-A786-442B-8202-B965530AE20E} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{2A8189FE-0E9B-474F-91E7-A92C7C302A43} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{00EE8632-007B-406B-9CF8-F0CE38A5FEB0} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{1565B041-B687-4EF1-B27D-B0F9A927847B} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{E444200C-5B5D-42C0-88C5-D0E2C11A393F} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{28D2FC99-9DB4-4F15-A99E-F5A76DE7B13A} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{F81D94AB-E336-405D-B7E3-8AC97EE4E758} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{192C9441-BA98-41D4-A27E-D7DE95BB46F7} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{D592C239-AFAD-4596-8FE9-BBEA4977B258} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{33AA9C01-0E8E-4960-AE8C-550C3B13F84A} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{FD79C25F-87CD-47C6-87B2-F497AEA4A12D} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
{F723B0BB-C5EA-4EBE-B3D5-50682F45EA80} = {368BD8A7-67D2-4F24-B655-C3B82BBDA840}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {63CC6DCC-A35C-402C-8ECB-3CC7EA821994}
EndGlobalSection
EndGlobal

8
nuget.config Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
<add key="myget-discordnet" value="https://www.myget.org/F/discord-net/api/v3/index.json" protocolVersion="3"/>
</packageSources>
</configuration>

Binary file not shown.

View file

@ -0,0 +1,2 @@
# comment
dotnet ef migrations add $Args[0] --project .\srcs\_plugins\Plugin.DB.EF --startup-project .\srcs\_plugins\Plugin.DB.EF

View file

@ -0,0 +1 @@
./dist/toolkit/Toolkit.exe create-accounts

View file

@ -0,0 +1,2 @@
# comment
dotnet ef migrations remove --project .\srcs\_plugins\Plugin.DB.EF --startup-project .\srcs\_plugins\Plugin.DB.EF

View file

@ -0,0 +1,2 @@
# comment
dotnet ef database update $Args[0] --project .\srcs\_plugins\Plugin.DB.EF --startup-project .\srcs\_plugins\Plugin.DB.EF

1
scripts/Docker/MQTT.ps1 Normal file
View file

@ -0,0 +1 @@
docker run -p 18083:18083 -p 1883:1883 --name mqtt-broker --restart always -d eclipse-mosquitto:1.6

View file

@ -0,0 +1 @@
docker run --name mongodb -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=root -e MONGO_INITDB_ROOT_PASSWORD=root --restart always -d mongo:4

View file

@ -0,0 +1 @@
docker run -p 5432:5432 --name postgres-SQL -e POSTGRES_PASSWORD=VaNOSilla2022 --restart always -d postgres:13

1
scripts/Docker/Redis.ps1 Normal file
View file

@ -0,0 +1 @@
docker run -p 6379:6379 --name redis --restart always -d redis:latest redis-server --notify-keyspace-events Kx --appendonly yes

View file

@ -0,0 +1,3 @@
$wingsemu_translations_directory = "../server-translations"
./dist/toolkit/Toolkit.exe translations -i $wingsemu_translations_directory -o $wingsemu_translations_directory

View file

@ -0,0 +1,73 @@
# Translations keys #
$resources_src = '../server-translations'
$files = Get-ChildItem $resources_src -Filter "*.yaml"
$resources_dst = "./dist/translation-server/translations"
New-Item -ItemType Directory -Force -Path $resources_dst
Copy-Item $resources_src\en -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\fr -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\pl -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\es -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\it -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\de -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\tr -Destination $resources_dst -Force -Recurse
Copy-Item $resources_src\cz -Destination $resources_dst -Force -Recurse
Write-Host Copied $files.Count generic translations files to translations
# Dat
$resources_src = '../client-files/dat'
$files = Get-ChildItem $resources_src
$resources_dst = "./dist/game-server/resources/dat"
New-Item -ItemType Directory -Force -Path $resources_dst
Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse
Write-Host Copied $files.Count .dat files
$resources_dst = "./dist/bazaar-server/resources/dat"
New-Item -ItemType Directory -Force -Path $resources_dst
Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse
Write-Host Copied $files.Count .dat files to bazaar
$resources_dst = "./dist/discord-notifier/resources/dat"
New-Item -ItemType Directory -Force -Path $resources_dst
Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse
Write-Host Copied $files.Count .dat files to communicator
# Lang keys #
$resources_src = '../client-files/lang'
$files = Get-ChildItem $resources_src
$resources_dst = "./dist/game-server/resources/lang"
New-Item -ItemType Directory -Force -Path $resources_dst
Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse
Write-Host Copied $files.Count langs files to game server
# Maps
$resources_src = '../client-files/maps'
$files = Get-ChildItem $resources_src
$resources_dst = "./dist/game-server/resources/maps"
New-Item -ItemType Directory -Force -Path $resources_dst
Copy-Item $resources_src\* -Destination $resources_dst -Force -Recurse
Write-Host Copied $files.Count maps files
# Config files
$server_config_src = '../server-files'
$files = Get-ChildItem $server_config_src
$server_config_target = './dist/game-server/config'
New-Item -ItemType Directory -Force -Path $server_config_target
Copy-Item $server_config_src\* -Destination $server_config_target -Force -Recurse
Write-Host Copied $files.Count config files to Game Server
$server_config_target = './dist/family-server/config'
New-Item -ItemType Directory -Force -Path $server_config_target
Copy-Item $server_config_src\* -Destination $server_config_target -Force -Recurse
Write-Host Copied $files.Count config files to Family Server
$server_config_target = './dist/translation-server/config'
New-Item -ItemType Directory -Force -Path $server_config_target
Copy-Item $server_config_src\* -Destination $server_config_target -Force -Recurse
Write-Host Copied $files.Count config files to Translations Server

View file

@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputPath>..\..\dist\bazaar-server\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.38.0" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.152" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PhoenixLib.Caching\PhoenixLib.Caching.csproj" />
<ProjectReference Include="..\PhoenixLib.Events\PhoenixLib.Events.csproj" />
<ProjectReference Include="..\PhoenixLib.Logging\PhoenixLib.Logging.csproj" />
<ProjectReference Include="..\PhoenixLib.Messaging\PhoenixLib.Messaging.csproj" />
<ProjectReference Include="..\WingsEmu.Communication.gRPC\WingsEmu.Communication.gRPC.csproj" />
<ProjectReference Include="..\WingsEmu.Health\WingsEmu.Health.csproj" />
<ProjectReference Include="..\_plugins\Plugin.ResourceLoader\Plugin.ResourceLoader.csproj" />
<ProjectReference Include="..\_plugins\Plugin.DB.EF\Plugin.DB.EF.csproj" />
<ProjectReference Include="..\_plugins\WingsEmu.Plugins.DistributedGameEvents\WingsEmu.Plugins.DistributedGameEvents.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,24 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.Services.Messages;
using WingsEmu.Health;
namespace BazaarServer.Consumers
{
public class ServiceMaintenanceNotificationMessageConsumer : IMessageConsumer<ServiceMaintenanceNotificationMessage>
{
private readonly IMaintenanceManager _maintenanceManager;
public ServiceMaintenanceNotificationMessageConsumer(IMaintenanceManager maintenanceManager) => _maintenanceManager = maintenanceManager;
public async Task HandleAsync(ServiceMaintenanceNotificationMessage notification, CancellationToken token)
{
if (notification.TimeLeft <= TimeSpan.FromMinutes(5))
{
_maintenanceManager.ActivateMaintenance();
}
}
}
}

View file

@ -0,0 +1,54 @@
using System;
using System.Runtime.Loader;
using System.Threading;
using PhoenixLib.Logging;
namespace BazaarServer
{
public class DockerGracefulStopService : IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ManualResetEventSlim _stoppedEvent;
public DockerGracefulStopService()
{
_cancellationTokenSource = new CancellationTokenSource();
_stoppedEvent = new ManualResetEventSlim();
// SIGINT
Console.CancelKeyPress += (sender, eventArgs) =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
// SIGTERM
AssemblyLoadContext.Default.Unloading += context =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Log.Error("UnhandledException", args.ExceptionObject as Exception);
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
}
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
public void Dispose()
{
_stoppedEvent.Set();
}
private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent)
{
Log.Info("DockerGracefulStopService Stopping service");
cancellationTokenSource.Cancel();
stoppedEvent.Wait();
Log.Info("DockerGracefulStopService Stop finished");
}
}
}

View file

@ -0,0 +1,186 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.Caching;
using WingsAPI.Communication.Bazaar;
using WingsAPI.Data.Bazaar;
using WingsAPI.Game.Extensions.Bazaar;
using WingsAPI.Packets.Enums.Bazaar;
using WingsEmu.Core.Extensions;
using WingsEmu.Core.Generics;
using WingsEmu.DTOs.Bazaar;
namespace BazaarServer.Managers
{
public class BazaarManager
{
private readonly IBazaarItemDAO _bazaarItemDao;
private readonly BazaarSearchManager _bazaarSearchManager;
private readonly ILongKeyCachedRepository<BazaarItemDTO> _itemsCache;
private readonly ConcurrentDictionary<long, ThreadSafeHashSet<long>> _itemsIdsByCharacterId = new();
private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
public BazaarManager(IBazaarItemDAO bazaarItemDao, ILongKeyCachedRepository<BazaarItemDTO> itemsCache, BazaarSearchManager bazaarSearchManager)
{
_bazaarItemDao = bazaarItemDao;
_itemsCache = itemsCache;
_bazaarSearchManager = bazaarSearchManager;
}
public async Task<long> CacheAllItemsInDb()
{
IEnumerable<BazaarItemDTO> items = await _bazaarItemDao.GetAllNonDeletedBazaarItems();
int count = 0;
foreach (BazaarItemDTO item in items)
{
AddToCache(item);
count++;
}
await _bazaarSearchManager.Initialize(items);
return count;
}
private void AddToCache(BazaarItemDTO item)
{
_itemsCache.Set(item.Id, item);
_itemsIdsByCharacterId.GetOrAdd(item.CharacterId, new ThreadSafeHashSet<long>()).Add(item.Id);
}
private void RemoveFromCache(BazaarItemDTO item)
{
_itemsCache.Remove(item.Id);
_itemsIdsByCharacterId.GetOrDefault(item.CharacterId)?.Remove(item.Id);
}
public async Task<BazaarItemDTO> SaveAsync(BazaarItemDTO item)
{
await _semaphoreSlim.WaitAsync();
try
{
BazaarItemDTO dto = await _bazaarItemDao.SaveAsync(item);
AddToCache(dto);
_bazaarSearchManager.AddItem(dto);
return dto;
}
finally
{
_semaphoreSlim.Release();
}
}
public async Task<BazaarItemDTO> GetBazaarItemById(long bazaarItemId)
{
await _semaphoreSlim.WaitAsync();
try
{
return _itemsCache.Get(bazaarItemId);
}
finally
{
_semaphoreSlim.Release();
}
}
public ICollection<BazaarItemDTO> GetItemsByCharacterId(long characterId)
{
return _itemsIdsByCharacterId.GetOrDefault(characterId)?.Select(s => _itemsCache.Get(s)).ToList();
}
public async Task<BazaarItemDTO> DeleteItemWithDto(BazaarItemDTO item, long requesterCharacterId)
{
await _semaphoreSlim.WaitAsync();
try
{
BazaarItemDTO cachedItem = _itemsCache.Get(item.Id);
if (cachedItem == null || cachedItem.CharacterId != requesterCharacterId)
{
return null;
}
await _bazaarItemDao.DeleteByIdAsync(item.Id);
RemoveFromCache(item);
_bazaarSearchManager.RemoveItem(item);
return cachedItem;
}
finally
{
_semaphoreSlim.Release();
}
}
public async Task<BazaarItemDTO> ChangeItemPriceWithDto(BazaarItemDTO bazaarItemDto, long characterId, long price, long saleFee)
{
await _semaphoreSlim.WaitAsync();
try
{
BazaarItemDTO cachedItem = _itemsCache.Get(bazaarItemDto.Id);
if (cachedItem == null || cachedItem.SoldAmount > 0 || characterId != cachedItem.CharacterId || cachedItem.GetBazaarItemStatus() != BazaarListedItemType.ForSale
|| BazaarExtensions.PriceOrAmountExceeds(bazaarItemDto.UsedMedal, price, bazaarItemDto.Amount))
{
return null;
}
cachedItem.PricePerItem = price;
cachedItem.SaleFee = saleFee;
BazaarItemDTO savedItem = await _bazaarItemDao.SaveAsync(cachedItem);
return savedItem;
}
finally
{
_semaphoreSlim.Release();
}
}
public IReadOnlyCollection<BazaarItemDTO> SearchBazaarItems(BazaarSearchContext bazaarSearchContext) => _bazaarSearchManager.SearchBazaarItems(bazaarSearchContext);
public async Task<BazaarItemDTO> BuyItemWithExpectedValues(long bazaarItemId, long buyerCharacterId, short amount, long pricePerItem)
{
await _semaphoreSlim.WaitAsync();
try
{
BazaarItemDTO cachedItem = _itemsCache.Get(bazaarItemId);
if (cachedItem == null || buyerCharacterId == cachedItem.CharacterId || amount < 1 || cachedItem.Amount - cachedItem.SoldAmount < amount || pricePerItem != cachedItem.PricePerItem
|| cachedItem.IsPackage && amount != cachedItem.Amount || cachedItem.GetBazaarItemStatus() != BazaarListedItemType.ForSale)
{
return null;
}
cachedItem.SoldAmount += amount;
BazaarItemDTO savedItem = await _bazaarItemDao.SaveAsync(cachedItem);
return savedItem;
}
finally
{
_semaphoreSlim.Release();
}
}
public async Task<List<BazaarItemDTO>> UnlistItemsWithVnums(List<BazaarItemDTO> items)
{
await _semaphoreSlim.WaitAsync();
try
{
foreach (BazaarItemDTO bazaarItemDto in items)
{
bazaarItemDto.ExpiryDate = DateTime.UtcNow.AddDays(-1);
BazaarItemDTO dto = await _bazaarItemDao.SaveAsync(bazaarItemDto);
AddToCache(dto);
_bazaarSearchManager.AddItem(dto);
}
return items;
}
finally
{
_semaphoreSlim.Release();
}
}
}
}

View file

@ -0,0 +1,719 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Communication.Bazaar;
using WingsAPI.Data.Bazaar;
using WingsAPI.Data.GameData;
using WingsAPI.Game.Extensions.Bazaar;
using WingsAPI.Packets.Enums.Bazaar;
using WingsAPI.Packets.Enums.Bazaar.Filter;
using WingsAPI.Packets.Enums.Bazaar.SubFilter;
using WingsEmu.Core.Generics;
using WingsEmu.DTOs.Items;
using WingsEmu.Game._enum;
using WingsEmu.Packets.Enums;
using WingsEmu.Packets.Enums.Battle;
using WingsEmu.Packets.Enums.Character;
namespace BazaarServer.Managers
{
public class BazaarSearchManager
{
private readonly ILongKeyCachedRepository<ConcurrentDictionary<long, BazaarItemDTO>> _bazaarItemByItemVnum;
private readonly ILongKeyCachedRepository<(ItemDTO, BazaarLevelFilterType)> _cachedItems;
private readonly IResourceLoader<ItemDTO> _itemDao;
private readonly Dictionary<BazaarCategoryFilterType, Dictionary<byte, HashSet<int>>> _itemVnumByCategoryAndSubCategory = new();
private readonly ThreadSafeHashSet<int> _itemVnumsRegistered = new();
public BazaarSearchManager(ILongKeyCachedRepository<ConcurrentDictionary<long, BazaarItemDTO>> bazaarItemByItemVnum, IResourceLoader<ItemDTO> itemDao,
ILongKeyCachedRepository<(ItemDTO, BazaarLevelFilterType)> cachedItems)
{
_bazaarItemByItemVnum = bazaarItemByItemVnum;
_itemDao = itemDao;
_cachedItems = cachedItems;
}
public async Task Initialize(IEnumerable<BazaarItemDTO> bazaarItemDtos)
{
await CategorizeItems();
await CacheSearchableBazaarItems(bazaarItemDtos);
}
private async Task CacheSearchableBazaarItems(IEnumerable<BazaarItemDTO> bazaarItemDtos)
{
Log.Info("[BAZAAR_SEARCH_MANAGER] Caching searchable BazaarItems");
int count = 0;
foreach (BazaarItemDTO bazaarItemDto in bazaarItemDtos)
{
if (bazaarItemDto.GetBazaarItemStatus() != BazaarListedItemType.ForSale)
{
continue;
}
AddItem(bazaarItemDto);
count++;
}
Log.Info($"[BAZAAR_SEARCH_MANAGER] Cached: {count} searchable BazaarItems");
}
private async Task CategorizeItems()
{
Log.Info("[BAZAAR_SEARCH_MANAGER] Categorizing items from DB");
IEnumerable<ItemDTO> items = await _itemDao.LoadAsync();
int count = 0;
var itemsToRegister = new Dictionary<BazaarCategoryFilterType, HashSet<int>>();
foreach (ItemDTO item in items)
{
(BazaarCategoryFilterType category, IEnumerable<byte> subCategories, bool registerForAllSubcategories) = GetCategoryAndSubCategoriesByItemVnum(item);
if (registerForAllSubcategories && !itemsToRegister.TryAdd(category, new HashSet<int> { item.Id }))
{
itemsToRegister[category].Add(item.Id);
}
if (!_itemVnumByCategoryAndSubCategory.ContainsKey(category))
{
_itemVnumByCategoryAndSubCategory.Add(category, new Dictionary<byte, HashSet<int>>());
}
foreach (byte subCategory in subCategories)
{
if (!_itemVnumByCategoryAndSubCategory[category].ContainsKey(subCategory))
{
_itemVnumByCategoryAndSubCategory[category].Add(subCategory, new HashSet<int>());
}
_itemVnumByCategoryAndSubCategory[category][subCategory].Add(item.Id);
}
if (!_itemVnumByCategoryAndSubCategory[category].ContainsKey(0))
{
_itemVnumByCategoryAndSubCategory[category].Add(0, new HashSet<int>());
}
_itemVnumByCategoryAndSubCategory[category][0].Add(item.Id);
_cachedItems.Set(item.Id, (item, GetItemLevelFilterByLevel(item.LevelMinimum, item.IsHeroic)));
count++;
}
foreach (KeyValuePair<BazaarCategoryFilterType, HashSet<int>> itemToRegister in itemsToRegister)
{
Type type = GetSubCategoryTypeByCategory(itemToRegister.Key);
if (type == null)
{
continue;
}
foreach (byte value in Enum.GetValues(type))
{
if (!_itemVnumByCategoryAndSubCategory[itemToRegister.Key].ContainsKey(value))
{
_itemVnumByCategoryAndSubCategory[itemToRegister.Key].Add(value, new HashSet<int>());
}
foreach (int vnum in itemToRegister.Value)
{
_itemVnumByCategoryAndSubCategory[itemToRegister.Key][value].Add(vnum);
}
}
}
Log.Info($"[BAZAAR_SEARCH_MANAGER] Categorized: {count.ToString()} items");
}
private static Type GetSubCategoryTypeByCategory(BazaarCategoryFilterType category)
{
switch (category)
{
case BazaarCategoryFilterType.Specialist:
return typeof(BazaarCategorySpecialistSubFilterType);
case BazaarCategoryFilterType.Partner:
return typeof(BazaarCategoryPartnerSubFilterType);
case BazaarCategoryFilterType.Pet:
return typeof(BazaarCategoryPetSubFilterType);
case BazaarCategoryFilterType.StoreMount:
return typeof(BazaarCategoryConsumerItemSubFilterType);
default:
return null;
}
}
public void AddItem(BazaarItemDTO bazaarItemDto)
{
ItemInstanceDTO itemInstanceDto = bazaarItemDto?.ItemInstance;
if (itemInstanceDto == null)
{
return;
}
ConcurrentDictionary<long, BazaarItemDTO> dictionary = _bazaarItemByItemVnum.GetOrSet(itemInstanceDto.ItemVNum, () => new ConcurrentDictionary<long, BazaarItemDTO>());
dictionary[bazaarItemDto.Id] = bazaarItemDto;
_itemVnumsRegistered.Add(itemInstanceDto.ItemVNum);
}
public void RemoveItem(BazaarItemDTO bazaarItemDto)
{
if (bazaarItemDto == null)
{
return;
}
ConcurrentDictionary<long, BazaarItemDTO> dictionary = _bazaarItemByItemVnum.GetOrSet(bazaarItemDto.ItemInstance.ItemVNum, () => new ConcurrentDictionary<long, BazaarItemDTO>());
dictionary.Remove(bazaarItemDto.Id, out _);
if (!dictionary.IsEmpty)
{
return;
}
_bazaarItemByItemVnum.Remove(bazaarItemDto.ItemInstance.ItemVNum);
_itemVnumsRegistered.Remove(bazaarItemDto.ItemInstance.ItemVNum);
}
public IReadOnlyCollection<BazaarItemDTO> SearchBazaarItems(BazaarSearchContext bazaarSearchContext)
{
IReadOnlyCollection<int> desiredItemVNums =
(bazaarSearchContext.ItemVNumFilter ?? GetItemVNumsByCategoryAndSubCategory(bazaarSearchContext.CategoryFilterType, bazaarSearchContext.SubTypeFilter)) ??
_itemVnumsRegistered.ToArray();
var itemList = new List<BazaarItemDTO>();
var tempItemList = new List<BazaarItemDTO>();
int ignoreCounter = bazaarSearchContext.Index * bazaarSearchContext.AmountOfItemsPerIndex;
int sendCounter = 0;
foreach (int itemVnum in desiredItemVNums.OrderBy(x => x))
{
ConcurrentDictionary<long, BazaarItemDTO> dictionary = _bazaarItemByItemVnum.Get(itemVnum);
if (dictionary == null)
{
continue;
}
if (ignoreCounter > dictionary.Count && bazaarSearchContext.LevelFilter == BazaarLevelFilterType.All && bazaarSearchContext.RareFilter == BazaarRarityFilterType.All &&
bazaarSearchContext.UpgradeFilter == BazaarUpgradeFilterType.All)
{
ignoreCounter -= dictionary.Count;
continue;
}
(ItemDTO itemDto, BazaarLevelFilterType baseItemLevelFilter) = _cachedItems.Get(itemVnum);
bool itemLevelIsInstanceDependant = false;
if (baseItemLevelFilter == BazaarLevelFilterType.All)
{
itemLevelIsInstanceDependant = true;
}
else
{
if (bazaarSearchContext.LevelFilter != BazaarLevelFilterType.All && !ItemLevelFilterChecker(bazaarSearchContext.LevelFilter, baseItemLevelFilter))
{
continue;
}
}
IOrderedEnumerable<KeyValuePair<long, BazaarItemDTO>> values;
switch (bazaarSearchContext.OrderFilter)
{
case BazaarSortFilterType.PriceAscending:
values = dictionary.OrderBy(x => x.Value.PricePerItem);
break;
case BazaarSortFilterType.PriceDescending:
values = dictionary.OrderByDescending(x => x.Value.PricePerItem);
break;
case BazaarSortFilterType.AmountAscending:
values = dictionary.OrderBy(x => x.Value.Amount - x.Value.SoldAmount);
break;
case BazaarSortFilterType.AmountDescending:
values = dictionary.OrderByDescending(x => x.Value.Amount - x.Value.SoldAmount);
break;
default:
return null;
}
foreach ((long _, BazaarItemDTO bazaarItemDto) in values)
{
BazaarListedItemType itemStatus = bazaarItemDto.GetBazaarItemStatus();
if (itemStatus != BazaarListedItemType.ForSale)
{
RemoveItem(bazaarItemDto);
continue;
}
if (bazaarItemDto.ItemInstance.Type == ItemInstanceType.BoxInstance)
{
BazaarPerfectionFilterType itemRarityFilter = GetPerfectionFilterByInstance(bazaarItemDto.ItemInstance);
if ((int)bazaarSearchContext.RareFilter != (int)BazaarPerfectionFilterType.All && (int)itemRarityFilter != (int)bazaarSearchContext.RareFilter)
{
continue;
}
}
else
{
BazaarRarityFilterType itemRarityFilter = GetRarityFilterByInstance(bazaarItemDto.ItemInstance);
if (bazaarSearchContext.RareFilter != BazaarRarityFilterType.All && itemRarityFilter != bazaarSearchContext.RareFilter)
{
continue;
}
}
BazaarUpgradeFilterType itemUpgradeFilter = GetUpgradeFilterByInstance(bazaarItemDto.ItemInstance);
if (bazaarSearchContext.UpgradeFilter != BazaarUpgradeFilterType.All && itemUpgradeFilter != bazaarSearchContext.UpgradeFilter)
{
continue;
}
if (itemLevelIsInstanceDependant)
{
BazaarLevelFilterType itemLevelFilter = GetItemLevelFilterByInstance(bazaarItemDto.ItemInstance, bazaarItemDto.ItemInstance.Type);
if (bazaarSearchContext.LevelFilter != BazaarLevelFilterType.All && itemLevelFilter != bazaarSearchContext.LevelFilter)
{
continue;
}
}
if (bazaarSearchContext.SubTypeFilter != 0 && bazaarItemDto.ItemInstance.Type == ItemInstanceType.BoxInstance)
{
if ((UsableItemSubType)itemDto.ItemSubType != UsableItemSubType.RaidBoxOrSealedJajamaruSpOrSealedSakuraBead)
{
ItemInstanceDTO boxInstanceDto = bazaarItemDto.ItemInstance;
if (!GetBoxInstanceSubCategories(boxInstanceDto, itemDto).Contains(bazaarSearchContext.SubTypeFilter))
{
continue;
}
}
}
if (ignoreCounter > 0)
{
ignoreCounter--;
continue;
}
if (sendCounter >= bazaarSearchContext.AmountOfItemsPerIndex)
{
break;
}
sendCounter++;
tempItemList.Add(bazaarItemDto);
}
itemList.AddRange(tempItemList);
tempItemList.Clear();
if (sendCounter >= bazaarSearchContext.AmountOfItemsPerIndex)
{
break;
}
}
return itemList;
}
private List<byte> GetBoxInstanceSubCategories(ItemInstanceDTO boxInstanceDto, ItemDTO itemInfo)
{
var subCategory = new List<byte>();
switch ((UsableItemSubType)itemInfo.ItemSubType)
{
case UsableItemSubType.PetBead:
subCategory.Add(
(byte)(boxInstanceDto.HoldingVNum is 0 or null ? BazaarCategoryPetSubFilterType.EmptyPetBead : BazaarCategoryPetSubFilterType.PetBead));
break;
case UsableItemSubType.PartnerBead:
if (boxInstanceDto.HoldingVNum is 0 or null)
{
subCategory.Add((byte)BazaarCategoryPartnerSubFilterType.EmptyPartnerBead);
break;
}
subCategory.Add((byte)BazaarCategoryPartnerSubFilterType.PartnerBead);
subCategory.Add((byte)GetPartnerSubCategoryOfAttack(_cachedItems.Get(boxInstanceDto.HoldingVNum.Value).Item1));
break;
case UsableItemSubType.PartnerSpHolder:
if (boxInstanceDto.HoldingVNum is 0 or null)
{
subCategory.Add((byte)BazaarCategoryPartnerSubFilterType.EmptyCardHolder);
break;
}
subCategory.Add((byte)GetPartnerSubCategoryOfAttack(_cachedItems.Get(boxInstanceDto.HoldingVNum.Value).Item1));
break;
case UsableItemSubType.SpecialistHolder:
if (boxInstanceDto.HoldingVNum is 0 or null)
{
subCategory.Add((byte)BazaarCategorySpecialistSubFilterType.EmptyCardHolder);
break;
}
subCategory.Add(SpMorphToSubCategory(_cachedItems.Get(boxInstanceDto.HoldingVNum.Value).Item1));
break;
case UsableItemSubType.VehicleBead:
subCategory.Add((byte)(boxInstanceDto.HoldingVNum is 0 or null ? BazaarCategoryStoreMountSubFilterType.EmptyMountBead : BazaarCategoryStoreMountSubFilterType.MountBead));
break;
}
return subCategory;
}
private (BazaarCategoryFilterType, IEnumerable<byte>, bool) GetCategoryAndSubCategoriesByItemVnum(ItemDTO itemDto)
{
BazaarCategoryFilterType category = BazaarCategoryFilterType.Miscellaneous;
var subCategories = new List<byte>();
bool registerForAllSubcategories = false;
switch (itemDto.ItemType)
{
case ItemType.Weapon:
category = BazaarCategoryFilterType.Weapon;
subCategories.Add(ItemTypeClassToSubCategory(itemDto.Class));
break;
case ItemType.Armor:
category = BazaarCategoryFilterType.Armour;
subCategories.Add(ItemTypeClassToSubCategory(itemDto.Class));
break;
case ItemType.Fashion:
category = BazaarCategoryFilterType.Equipment;
subCategories.Add(EquipmentTypeToSubCategory1(itemDto.EquipmentSlot));
byte? secondSubCategory = EquipmentTypeToSubCategory2(itemDto.EquipmentSlot, itemDto.Sex switch
{
0 => GenderType.Unisex,
1 => GenderType.Male,
_ => GenderType.Female
});
if (secondSubCategory != null)
{
subCategories.Add(secondSubCategory.Value);
}
break;
case ItemType.Jewelry:
category = BazaarCategoryFilterType.Accessories;
subCategories.Add(EquipmentTypeToSubCategory3(itemDto.EquipmentSlot));
break;
case ItemType.Specialist:
category = BazaarCategoryFilterType.Specialist;
subCategories.Add(SpMorphToSubCategory(itemDto));
break;
case ItemType.Box:
var usableItemSubType = (UsableItemSubType)itemDto.ItemSubType;
switch (usableItemSubType)
{
case UsableItemSubType.PetBead:
category = BazaarCategoryFilterType.Pet;
registerForAllSubcategories = true;
break;
case UsableItemSubType.PartnerBead:
category = BazaarCategoryFilterType.Partner;
registerForAllSubcategories = true;
break;
case UsableItemSubType.PartnerSpHolder:
category = BazaarCategoryFilterType.Partner;
registerForAllSubcategories = true;
break;
case UsableItemSubType.SpecialistHolder:
category = BazaarCategoryFilterType.Specialist;
registerForAllSubcategories = true;
break;
case UsableItemSubType.FairyBead:
category = BazaarCategoryFilterType.Accessories;
subCategories.Add((byte)BazaarCategoryAccessoriesSubFilterType.Fairy);
break;
case UsableItemSubType.VehicleBead:
category = BazaarCategoryFilterType.StoreMount;
registerForAllSubcategories = true;
break;
}
break;
case ItemType.Shell:
category = BazaarCategoryFilterType.Shell;
var shellItemSubType = (ShellItemSubType)itemDto.ItemSubType;
subCategories.Add(shellItemSubType switch
{
ShellItemSubType.Weapon => (byte)BazaarCategoryShellSubFilterType.Weapon,
ShellItemSubType.Armor => (byte)BazaarCategoryShellSubFilterType.Clothing,
_ => default
});
break;
case ItemType.Main:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.GeneralItems);
break;
case ItemType.Upgrade:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.Material);
break;
case ItemType.Production:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.ProductionItem);
break;
case ItemType.Special:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.SpecialItems);
break;
case ItemType.Potion:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.HealingPotion);
break;
case ItemType.Event:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.Event);
break;
case ItemType.Title:
category = BazaarCategoryFilterType.MainItem;
subCategories.Add((byte)BazaarCategoryMainItemSubFilterType.Title);
break;
case ItemType.Sell:
category = BazaarCategoryFilterType.ConsumerItem;
subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.SaleItem);
break;
case ItemType.Food:
category = BazaarCategoryFilterType.ConsumerItem;
subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.Food);
break;
case ItemType.Snack:
category = BazaarCategoryFilterType.ConsumerItem;
subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.Snack);
break;
case ItemType.Magical:
category = BazaarCategoryFilterType.ConsumerItem;
subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.MagicItem);
break;
case ItemType.Material:
category = BazaarCategoryFilterType.ConsumerItem;
subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.Ingredients);
break;
case ItemType.PetPartnerItem:
category = BazaarCategoryFilterType.ConsumerItem;
subCategories.Add((byte)BazaarCategoryConsumerItemSubFilterType.PartnerItem);
break;
}
return (category, subCategories, registerForAllSubcategories);
}
private static byte ItemTypeClassToSubCategory(byte itemTypeClass)
{
return itemTypeClass switch
{
(int)ItemClassType.Adventurer => (byte)BazaarCategoryWeaponArmourSubFilterType.Adventurer,
(int)ItemClassType.Swordsman => (byte)BazaarCategoryWeaponArmourSubFilterType.Swordsman,
(int)ItemClassType.Archer => (byte)BazaarCategoryWeaponArmourSubFilterType.Archer,
(int)ItemClassType.Mage => (byte)BazaarCategoryWeaponArmourSubFilterType.Magician,
(int)ItemClassType.MartialArtist => (byte)BazaarCategoryWeaponArmourSubFilterType.MartialArtist,
_ => default
};
}
private static byte EquipmentTypeToSubCategory1(EquipmentType equipmentType)
{
return equipmentType switch
{
EquipmentType.Hat => (byte)BazaarCategoryEquipmentSubFilterType.Hat,
EquipmentType.Mask => (byte)BazaarCategoryEquipmentSubFilterType.Accessory,
EquipmentType.Gloves => (byte)BazaarCategoryEquipmentSubFilterType.Gloves,
EquipmentType.Boots => (byte)BazaarCategoryEquipmentSubFilterType.Shoes,
EquipmentType.CostumeSuit => (byte)BazaarCategoryEquipmentSubFilterType.Costume,
EquipmentType.CostumeHat => (byte)BazaarCategoryEquipmentSubFilterType.CostumeHat,
EquipmentType.WeaponSkin => (byte)BazaarCategoryEquipmentSubFilterType.CostumeWeapon,
EquipmentType.Wings => (byte)BazaarCategoryEquipmentSubFilterType.CostumeWings,
_ => default
};
}
private static byte? EquipmentTypeToSubCategory2(EquipmentType equipmentType, GenderType genderType)
{
if (genderType == GenderType.Unisex)
{
return null;
}
bool isMale = genderType == GenderType.Male;
return equipmentType switch
{
EquipmentType.CostumeSuit => (byte)(isMale ? BazaarCategoryEquipmentSubFilterType.CostumeMale : BazaarCategoryEquipmentSubFilterType.CostumeFemale),
EquipmentType.CostumeHat => (byte)(isMale ? BazaarCategoryEquipmentSubFilterType.CostumeHatMale : BazaarCategoryEquipmentSubFilterType.CostumeHatFemale),
_ => null
};
}
private static byte EquipmentTypeToSubCategory3(EquipmentType equipmentType)
{
return equipmentType switch
{
EquipmentType.Necklace => (byte)BazaarCategoryAccessoriesSubFilterType.Necklace,
EquipmentType.Ring => (byte)BazaarCategoryAccessoriesSubFilterType.Ring,
EquipmentType.Bracelet => (byte)BazaarCategoryAccessoriesSubFilterType.Bracelet,
EquipmentType.Fairy => (byte)BazaarCategoryAccessoriesSubFilterType.Fairy,
EquipmentType.Amulet => (byte)BazaarCategoryAccessoriesSubFilterType.Amulet,
_ => default
};
}
private static byte SpMorphToSubCategory(ItemDTO item)
{
int morphId = Convert.ToInt32(item.Morph);
if (!Enum.IsDefined(typeof(MorphIdType), morphId))
{
return default;
}
var morph = (MorphIdType)morphId;
return morph switch
{
MorphIdType.Pyjama => (byte)BazaarCategorySpecialistSubFilterType.Pyjama,
MorphIdType.Warrior => (byte)BazaarCategorySpecialistSubFilterType.Warrior,
MorphIdType.Ninja => (byte)BazaarCategorySpecialistSubFilterType.Ninja,
MorphIdType.Ranger => (byte)BazaarCategorySpecialistSubFilterType.Ranger,
MorphIdType.Assassin => (byte)BazaarCategorySpecialistSubFilterType.Assassin,
MorphIdType.RedMagician => (byte)BazaarCategorySpecialistSubFilterType.RedMagician,
MorphIdType.HolyMage => (byte)BazaarCategorySpecialistSubFilterType.HolyMage,
MorphIdType.Chicken => (byte)BazaarCategorySpecialistSubFilterType.ChickenCostume,
MorphIdType.Jajamaru => (byte)BazaarCategorySpecialistSubFilterType.Jajamaru,
MorphIdType.Crusader => (byte)BazaarCategorySpecialistSubFilterType.Crusader,
MorphIdType.Berserker => (byte)BazaarCategorySpecialistSubFilterType.Berserker,
MorphIdType.Destroyer => (byte)BazaarCategorySpecialistSubFilterType.Destroyer,
MorphIdType.WildKeeper => (byte)BazaarCategorySpecialistSubFilterType.WildKeeper,
MorphIdType.BlueMagician => (byte)BazaarCategorySpecialistSubFilterType.BlueMagician,
MorphIdType.DarkGunner => (byte)BazaarCategorySpecialistSubFilterType.DarkGunner,
MorphIdType.Pirate => (byte)BazaarCategorySpecialistSubFilterType.Pirate,
MorphIdType.Gladiator => (byte)BazaarCategorySpecialistSubFilterType.Gladiator,
MorphIdType.FireCannoneer => (byte)BazaarCategorySpecialistSubFilterType.FireCannoneer,
MorphIdType.Volcano => (byte)BazaarCategorySpecialistSubFilterType.Volcano,
MorphIdType.BattleMonk => (byte)BazaarCategorySpecialistSubFilterType.BattleMonk,
MorphIdType.Scout => (byte)BazaarCategorySpecialistSubFilterType.Scout,
MorphIdType.TideLord => (byte)BazaarCategorySpecialistSubFilterType.TideLord,
MorphIdType.DeathReaper => (byte)BazaarCategorySpecialistSubFilterType.DeathReaper,
MorphIdType.DemonHunter => (byte)BazaarCategorySpecialistSubFilterType.DemonHunter,
MorphIdType.Seer => (byte)BazaarCategorySpecialistSubFilterType.Seer,
MorphIdType.Renegade => (byte)BazaarCategorySpecialistSubFilterType.Renegade,
MorphIdType.AvengingAngel => (byte)BazaarCategorySpecialistSubFilterType.AvengingAngel,
MorphIdType.Archmage => (byte)BazaarCategorySpecialistSubFilterType.Archmage,
MorphIdType.DraconicFist => (byte)BazaarCategorySpecialistSubFilterType.DraconicFist,
MorphIdType.MysticArts => (byte)BazaarCategorySpecialistSubFilterType.MysticArts,
MorphIdType.WeddingCostume => (byte)BazaarCategorySpecialistSubFilterType.WeddingCostume,
MorphIdType.MasterWolf => (byte)BazaarCategorySpecialistSubFilterType.MasterWolf,
MorphIdType.DemonWarrior => (byte)BazaarCategorySpecialistSubFilterType.DemonWarrior,
_ => default
};
}
private BazaarLevelFilterType GetItemLevelFilterByInstance(ItemInstanceDTO itemInstanceDto, ItemInstanceType instanceType)
{
switch (instanceType)
{
case ItemInstanceType.BoxInstance:
case ItemInstanceType.SpecialistInstance:
ItemInstanceDTO specialistInstanceDto = itemInstanceDto;
return GetItemLevelFilterByLevel(specialistInstanceDto.SpLevel, false);
default:
(ItemDTO itemDto, BazaarLevelFilterType bazaarLevelFilterType) = _cachedItems.Get(itemInstanceDto.ItemVNum);
return itemDto.ItemType == ItemType.Shell
? GetItemLevelFilterByLevel(itemInstanceDto.Upgrade, false)
: bazaarLevelFilterType;
}
}
private static BazaarLevelFilterType GetItemLevelFilterByLevel(byte level, bool isHeroic)
{
if (isHeroic)
{
if (level is < 1 or > 60)
{
return BazaarLevelFilterType.ChampionGear;
}
return (BazaarLevelFilterType)(Convert.ToByte(Math.Ceiling(level / 10f)) + (byte)BazaarLevelFilterType.ChampionGear);
}
if (level is < 1 or > 99)
{
return BazaarLevelFilterType.All;
}
return (BazaarLevelFilterType)Convert.ToByte(Math.Ceiling(level / 10f));
}
private static bool ItemLevelFilterChecker(BazaarLevelFilterType demandedFilterType, BazaarLevelFilterType itemFilterType)
{
return demandedFilterType == itemFilterType || demandedFilterType == BazaarLevelFilterType.All || demandedFilterType == BazaarLevelFilterType.ChampionGear && itemFilterType switch
{
BazaarLevelFilterType.ChampionLevelOneToTen => true,
BazaarLevelFilterType.ChampionLevelElevenToTwenty => true,
BazaarLevelFilterType.ChampionLevelTwentyOneToThirty => true,
BazaarLevelFilterType.ChampionLevelThirtyOneToForty => true,
BazaarLevelFilterType.ChampionLevelFortyOneToFifty => true,
BazaarLevelFilterType.ChampionLevelFiftyOneToSixty => true,
_ => false
};
}
private static BazaarUpgradeFilterType GetUpgradeFilterByInstance(ItemInstanceDTO itemInstanceDto)
{
if (15 < itemInstanceDto.Upgrade)
{
return BazaarUpgradeFilterType.All;
}
return (BazaarUpgradeFilterType)(itemInstanceDto.Upgrade + 1);
}
private static BazaarRarityFilterType GetRarityFilterByInstance(ItemInstanceDTO itemInstanceDto)
{
if (itemInstanceDto.Rarity is < 0 or > 8)
{
return BazaarRarityFilterType.All;
}
return (BazaarRarityFilterType)(itemInstanceDto.Rarity + 1);
}
private static BazaarPerfectionFilterType GetPerfectionFilterByInstance(ItemInstanceDTO itemInstanceDto)
{
const int max = 100;
return itemInstanceDto.SpStoneUpgrade switch
{
< 1 or < max => BazaarPerfectionFilterType.All,
max => BazaarPerfectionFilterType.NintyOneToHundred,
_ => (BazaarPerfectionFilterType)Convert.ToByte(Math.Ceiling(itemInstanceDto.SpStoneUpgrade / 10f))
};
}
private static BazaarCategoryPartnerSubFilterType GetPartnerSubCategoryOfAttack(ItemDTO item)
{
return item.PartnerClass switch
{
(byte)AttackType.Melee => BazaarCategoryPartnerSubFilterType.CloseAttack,
(byte)AttackType.Ranged => BazaarCategoryPartnerSubFilterType.RemoteAttack,
(byte)AttackType.Magical => BazaarCategoryPartnerSubFilterType.Magic,
_ => BazaarCategoryPartnerSubFilterType.All
};
}
private IReadOnlyCollection<int> GetItemVNumsByCategoryAndSubCategory(BazaarCategoryFilterType categoryFilterType, byte subTypeFilter)
{
if (categoryFilterType == BazaarCategoryFilterType.All || !_itemVnumByCategoryAndSubCategory.ContainsKey(categoryFilterType) ||
!_itemVnumByCategoryAndSubCategory[categoryFilterType].ContainsKey(subTypeFilter))
{
return null;
}
return _itemVnumByCategoryAndSubCategory[categoryFilterType][subTypeFilter];
}
}
}

View file

@ -0,0 +1,82 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Logging;
using PhoenixLib.ServiceBus.MQTT;
using Plugin.Database.Mapping;
using ProtoBuf.Grpc.Client;
namespace BazaarServer
{
public class Program
{
public static async Task Main(string[] args)
{
PrintHeader();
NonGameMappingRules.InitializeMapping();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
GrpcClientFactory.AllowUnencryptedHttp2 = true;
using var stopService = new DockerGracefulStopService();
using IHost host = CreateHostBuilder(args).Build();
{
await host.StartAsync();
IMessagingService messagingService = host.Services.GetRequiredService<IMessagingService>();
await messagingService.StartAsync();
IServiceProvider services = host.Services;
Log.Info("BazaarServer started");
await host.WaitForShutdownAsync(stopService.CancellationToken);
await messagingService.DisposeAsync();
}
}
private static void PrintHeader()
{
const string text = @"
";
string separator = new('=', Console.WindowWidth);
string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s))
.Aggregate("", (current, i) => current + i);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator);
Console.ForegroundColor = ConsoleColor.White;
}
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
private static IHostBuilder CreateHostBuilder(string[] args)
{
IHostBuilder host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(s =>
{
s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("BAZAAR_SERVER_PORT") ?? "25555"), options => { options.Protocols = HttpProtocols.Http2; });
});
webBuilder.UseStartup<Startup>();
});
return host;
}
}
}

View file

@ -0,0 +1,25 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BazaarServer.Managers;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Logging;
namespace BazaarServer.RecurrentJobs
{
public class BazaarSystem : BackgroundService
{
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(10);
private readonly BazaarManager _bazaarManager;
public BazaarSystem(BazaarManager bazaarManager) => _bazaarManager = bazaarManager;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Log.Info("[BAZAAR_SYSTEM] Caching all items in the database...");
long countCachedItems = await _bazaarManager.CacheAllItemsInDb();
Log.Info($"[BAZAAR_SYSTEM] Cached: {countCachedItems.ToString()} item/s");
Log.Info("[BAZAAR_SYSTEM] Started!");
}
}
}

View file

@ -0,0 +1,243 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using BazaarServer.Managers;
using WingsAPI.Communication;
using WingsAPI.Communication.Bazaar;
using WingsAPI.Data.Bazaar;
using WingsEmu.Health;
namespace BazaarServer.Services
{
public class BazaarService : IBazaarService
{
private readonly BazaarManager _bazaarManager;
private readonly IMaintenanceManager _maintenanceManager;
public BazaarService(BazaarManager bazaarManager, IMaintenanceManager maintenanceManager)
{
_bazaarManager = bazaarManager;
_maintenanceManager = maintenanceManager;
}
private bool MaintenanceMode => _maintenanceManager.IsMaintenanceActive;
public async ValueTask<BazaarItemResponse> GetBazaarItemById(BazaarGetItemByIdRequest request)
{
if (MaintenanceMode)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
BazaarItemDTO bazaarItem = await _bazaarManager.GetBazaarItemById(request.BazaarItemId);
return new BazaarItemResponse
{
ResponseType = bazaarItem == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
BazaarItemDto = bazaarItem
};
}
public async ValueTask<BazaarItemResponse> AddItemToBazaar(BazaarAddItemRequest request)
{
if (MaintenanceMode)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
if (request.MaximumListedItems <= _bazaarManager.GetItemsByCharacterId(request.BazaarItemDto.CharacterId)?.Count)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.GENERIC_SERVER_ERROR
};
}
BazaarItemDTO bazaarItemDto = await _bazaarManager.SaveAsync(request.BazaarItemDto);
return new BazaarItemResponse
{
ResponseType = RpcResponseType.SUCCESS,
BazaarItemDto = bazaarItemDto
};
}
public async ValueTask<BazaarItemResponse> RemoveItemFromBazaar(BazaarRemoveItemRequest request)
{
if (MaintenanceMode)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
BazaarItemDTO deletedItem = await _bazaarManager.DeleteItemWithDto(request.BazaarItemDto, request.RequesterCharacterId);
if (deletedItem == null)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.GENERIC_SERVER_ERROR
};
}
return new BazaarItemResponse
{
ResponseType = RpcResponseType.SUCCESS,
BazaarItemDto = deletedItem
};
}
public async ValueTask<BazaarItemResponse> ChangeItemPriceFromBazaar(BazaarChangeItemPriceRequest request)
{
if (MaintenanceMode)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
BazaarItemDTO updatedItem = await _bazaarManager.ChangeItemPriceWithDto(request.BazaarItemDto, request.ChangerCharacterId, request.NewPrice, request.NewSaleFee);
if (updatedItem == null)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.GENERIC_SERVER_ERROR,
BazaarItemDto = null
};
}
return new BazaarItemResponse
{
ResponseType = RpcResponseType.SUCCESS,
BazaarItemDto = updatedItem
};
}
public async ValueTask<BazaarGetItemsByCharIdResponse> GetItemsByCharacterIdFromBazaar(BazaarGetItemsByCharIdRequest request)
{
if (MaintenanceMode)
{
return new BazaarGetItemsByCharIdResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
return new BazaarGetItemsByCharIdResponse
{
ResponseType = RpcResponseType.SUCCESS,
BazaarItems = _bazaarManager.GetItemsByCharacterId(request.CharacterId)
};
}
public async ValueTask<BazaarRemoveItemsByCharIdResponse> RemoveItemsByCharacterIdFromBazaar(BazaarRemoveItemsByCharIdRequest request)
{
if (MaintenanceMode)
{
return new BazaarRemoveItemsByCharIdResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
ICollection<BazaarItemDTO> items = _bazaarManager.GetItemsByCharacterId(request.CharacterId);
if (items == null)
{
return new BazaarRemoveItemsByCharIdResponse
{
ResponseType = RpcResponseType.SUCCESS
};
}
foreach (BazaarItemDTO item in items)
{
await _bazaarManager.DeleteItemWithDto(item, request.CharacterId);
}
return new BazaarRemoveItemsByCharIdResponse
{
ResponseType = RpcResponseType.SUCCESS
};
}
public async ValueTask<BazaarSearchBazaarItemsResponse> SearchBazaarItems(BazaarSearchBazaarItemsRequest request)
{
if (MaintenanceMode)
{
return new BazaarSearchBazaarItemsResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
return new BazaarSearchBazaarItemsResponse
{
ResponseType = RpcResponseType.SUCCESS,
BazaarItemDtos = _bazaarManager.SearchBazaarItems(request.BazaarSearchContext)
};
}
public async ValueTask<BazaarItemResponse> BuyItemFromBazaar(BazaarBuyItemRequest request)
{
if (MaintenanceMode)
{
return new BazaarItemResponse
{
ResponseType = RpcResponseType.MAINTENANCE_MODE
};
}
BazaarItemDTO cachedItem = await _bazaarManager.BuyItemWithExpectedValues(request.BazaarItemId, request.BuyerCharacterId, request.Amount, request.PricePerItem);
if (cachedItem == null)
{
return new BazaarItemResponse
{
BazaarItemDto = null,
ResponseType = RpcResponseType.GENERIC_SERVER_ERROR
};
}
return new BazaarItemResponse
{
BazaarItemDto = cachedItem,
ResponseType = RpcResponseType.SUCCESS
};
}
public async ValueTask<UnlistItemFromBazaarResponse> UnlistItemsFromBazaarWithVnumAsync(UnlistItemFromBazaarRequest request)
{
IReadOnlyCollection<BazaarItemDTO> itemsToUnlist = _bazaarManager.SearchBazaarItems(new BazaarSearchContext
{
ItemVNumFilter = request.Vnum,
Index = 0,
AmountOfItemsPerIndex = 10000
});
List<BazaarItemDTO> unlistedItems = await _bazaarManager.UnlistItemsWithVnums(itemsToUnlist.ToList());
return new UnlistItemFromBazaarResponse
{
UnlistedItems = unlistedItems.Count
};
}
public async ValueTask<UnlistItemFromBazaarResponse> UnlistCharacterItemsFromBazaarAsync(UnlistCharacterItemsFromBazaarRequest request)
{
ICollection<BazaarItemDTO> itemsToUnlist = _bazaarManager.GetItemsByCharacterId(request.Id);
List<BazaarItemDTO> unlistedItems = await _bazaarManager.UnlistItemsWithVnums(itemsToUnlist.ToList());
return new UnlistItemFromBazaarResponse
{
UnlistedItems = unlistedItems.Count
};
}
}
}

View file

@ -0,0 +1,70 @@
using BazaarServer.Consumers;
using BazaarServer.Managers;
using BazaarServer.RecurrentJobs;
using BazaarServer.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Caching;
using PhoenixLib.Events;
using PhoenixLib.Logging;
using PhoenixLib.ServiceBus.Extensions;
using Plugin.Database;
using Plugin.ResourceLoader;
using ProtoBuf.Grpc.Server;
using WingsAPI.Communication.Services.Messages;
using WingsEmu.Communication.gRPC.Extensions;
using WingsEmu.Health.Extensions;
namespace BazaarServer
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMqttConfigurationFromEnv();
services.AddMaintenanceMode();
services.AddEventPipeline();
services.AddEventHandlersInAssembly<Startup>();
new DatabasePlugin().AddDependencies(services);
new FileResourceLoaderPlugin().AddDependencies(services);
services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>));
services.TryAddSingleton(typeof(IUuidKeyCachedRepository<>), typeof(InMemoryUuidCacheRepository<>));
services.AddGrpcBazaarServiceClient();
services.AddSingleton<BazaarManager>();
services.AddSingleton<BazaarSearchManager>();
services.AddSingleton<BazaarService>();
services.AddSingleton<BazaarSystem>();
services.AddHostedService(s => s.GetRequiredService<BazaarSystem>());
services.AddPhoenixLogging();
services.AddCodeFirstGrpc(config =>
{
config.MaxReceiveMessageSize = null;
config.MaxSendMessageSize = null;
config.EnableDetailedErrors = true;
});
services.AddMessageSubscriber<ServiceMaintenanceNotificationMessage, ServiceMaintenanceNotificationMessageConsumer>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints => { endpoints.MapGrpcService<BazaarService>(); });
}
}
}

View file

@ -0,0 +1,29 @@
using System.Threading;
using System.Threading.Tasks;
using DatabaseServer.Managers;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.Services.Messages;
namespace DatabaseServer.Consumers
{
public class ServiceFlushAllMessageConsumer : IMessageConsumer<ServiceFlushAllMessage>
{
private readonly IAccountWarehouseManager _accountWarehouseManager;
private readonly ICharacterManager _characterManager;
private readonly ITimeSpaceManager _timeSpaceManager;
public ServiceFlushAllMessageConsumer(ICharacterManager characterManager, IAccountWarehouseManager accountWarehouseManager, ITimeSpaceManager timeSpaceManager)
{
_characterManager = characterManager;
_accountWarehouseManager = accountWarehouseManager;
_timeSpaceManager = timeSpaceManager;
}
public async Task HandleAsync(ServiceFlushAllMessage notification, CancellationToken token)
{
await _characterManager.FlushCharacterSaves();
await _accountWarehouseManager.FlushWarehouseSaves();
await _timeSpaceManager.FlushTimeSpaceRecords();
}
}
}

View file

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputPath>..\..\dist\database-server\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.38.0" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.152" />
<PackageReference Include="Z.EntityFramework.Extensions.EFCore" Version="5.13.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PhoenixLib.Caching\PhoenixLib.Caching.csproj" />
<ProjectReference Include="..\PhoenixLib.Logging\PhoenixLib.Logging.csproj" />
<ProjectReference Include="..\PhoenixLib.Messaging\PhoenixLib.Messaging.csproj" />
<ProjectReference Include="..\WingsEmu.Communication.gRPC\WingsEmu.Communication.gRPC.csproj" />
<ProjectReference Include="..\WingsEmu.Health\WingsEmu.Health.csproj" />
<ProjectReference Include="..\_plugins\Plugin.DB.EF\Plugin.DB.EF.csproj" />
<ProjectReference Include="..\_plugins\WingsEmu.Plugins.DistributedGameEvents\WingsEmu.Plugins.DistributedGameEvents.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,53 @@
using System;
using System.Runtime.Loader;
using System.Threading;
using PhoenixLib.Logging;
namespace FamilyServer
{
public class DockerGracefulStopService : IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ManualResetEventSlim _stoppedEvent;
public DockerGracefulStopService()
{
_cancellationTokenSource = new CancellationTokenSource();
_stoppedEvent = new ManualResetEventSlim();
// SIGINT
Console.CancelKeyPress += (sender, eventArgs) =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
// SIGTERM
AssemblyLoadContext.Default.Unloading += context =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Log.Error("UnhandledException", args.ExceptionObject as Exception);
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
}
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
public void Dispose()
{
_stoppedEvent.Set();
}
private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent)
{
Log.Info("DockerGracefulStopService Stopping service");
cancellationTokenSource.Cancel();
stoppedEvent.Wait();
Log.Info("DockerGracefulStopService Stop finished");
}
}
}

View file

@ -0,0 +1,10 @@
namespace DatabaseServer
{
public static class EnvironmentConsts
{
public const string DbServerSaveIntervalMinutes = "DB_SERVER_SAVE_INTERVAL_MINUTES";
public const string DbServerCharTtlMinutes = "DB_SERVER_CHAR_TTL_MINUTES";
public const string DbServerCharSaveIntervalSeconds = "DB_SERVER_CHAR_SAVE_INTERVAL_SECONDS";
public const string TsServerSaveIntervalMinutes = "TS_SERVER_SAVE_INTERVAL_MINUTES";
}
}

View file

@ -0,0 +1,440 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Mapster;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Data.Account;
using WingsAPI.Data.Warehouse;
using WingsEmu.Core.Extensions;
using WingsEmu.DTOs.Items;
namespace DatabaseServer.Managers
{
public class AccountWarehouseManager : BackgroundService, IAccountWarehouseManager
{
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerSaveIntervalMinutes) ?? "5"));
private static readonly TimeSpan LifeTime = Interval * 3;
private readonly IAccountWarehouseItemDao _accountWarehouseItemDao;
private readonly ILongKeyCachedRepository<Dictionary<short, AccountWarehouseItemDto>> _cachedWarehouseItems;
private readonly Dictionary<long, Dictionary<short, (AccountWarehouseItemDto dto, bool remove)>> _itemChanges = new();
private readonly SemaphoreSlim _itemChangesSemaphore = new(1, 1);
private readonly ConcurrentDictionary<long, SemaphoreSlim> _warehouseLocks = new();
public AccountWarehouseManager(IAccountWarehouseItemDao accountWarehouseItemDao, ILongKeyCachedRepository<Dictionary<short, AccountWarehouseItemDto>> cachedWarehouseItems)
{
_accountWarehouseItemDao = accountWarehouseItemDao;
_cachedWarehouseItems = cachedWarehouseItems;
}
public async Task<IEnumerable<AccountWarehouseItemDto>> GetWarehouse(long accountId)
{
SemaphoreSlim accountLock = GetAccountLock(accountId);
await accountLock.WaitAsync();
try
{
return (await GetAccountWarehouse(accountId))?.Values;
}
finally
{
accountLock.Release();
}
}
public async Task<AccountWarehouseItemDto> GetWarehouseItem(long accountId, short slot)
{
SemaphoreSlim accountLock = GetAccountLock(accountId);
await accountLock.WaitAsync();
try
{
return (await GetAccountWarehouse(accountId))?.GetValueOrDefault(slot);
}
finally
{
accountLock.Release();
}
}
public async Task<AddWarehouseItemResult> AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd)
{
long accountId = warehouseItemDtoToAdd.AccountId;
if (warehouseItemDtoToAdd.ItemInstance.Amount < 1 || 999 < warehouseItemDtoToAdd.ItemInstance.Amount)
{
return new AddWarehouseItemResult
{
Success = false
};
}
SemaphoreSlim accountLock = GetAccountLock(accountId);
await accountLock.WaitAsync();
try
{
Dictionary<short, AccountWarehouseItemDto> familyWarehouse = await GetAccountWarehouse(accountId);
if (familyWarehouse == null)
{
return new AddWarehouseItemResult
{
Success = false
};
}
AccountWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToAdd.Slot);
if (alreadyExistentItem == null)
{
familyWarehouse[warehouseItemDtoToAdd.Slot] = warehouseItemDtoToAdd;
await SetItemChangeWithLock(warehouseItemDtoToAdd, false);
return new AddWarehouseItemResult
{
Success = true,
UpdatedItem = warehouseItemDtoToAdd
};
}
if (warehouseItemDtoToAdd.ItemInstance.ItemVNum != alreadyExistentItem.ItemInstance.ItemVNum)
{
return new AddWarehouseItemResult
{
Success = false
};
}
if (warehouseItemDtoToAdd.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || alreadyExistentItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM
|| warehouseItemDtoToAdd.ItemInstance.Amount + alreadyExistentItem.ItemInstance.Amount > 999)
{
return new AddWarehouseItemResult
{
Success = false
};
}
alreadyExistentItem.ItemInstance.Amount += warehouseItemDtoToAdd.ItemInstance.Amount;
await SetItemChangeWithLock(alreadyExistentItem, false);
return new AddWarehouseItemResult
{
Success = true,
UpdatedItem = alreadyExistentItem
};
}
finally
{
accountLock.Release();
}
}
public async Task<WithdrawWarehouseItemResult> WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount)
{
long accountId = warehouseItemDtoToWithdraw.AccountId;
if (amount < 1 || 999 < amount)
{
return new WithdrawWarehouseItemResult
{
Success = false
};
}
SemaphoreSlim accountLock = GetAccountLock(accountId);
await accountLock.WaitAsync();
try
{
Dictionary<short, AccountWarehouseItemDto> familyWarehouse = await GetAccountWarehouse(accountId);
if (familyWarehouse == null)
{
return new WithdrawWarehouseItemResult
{
Success = false
};
}
AccountWarehouseItemDto alreadyExistentItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToWithdraw.Slot);
if (alreadyExistentItem == null || alreadyExistentItem.ItemInstance.Amount < amount)
{
return new WithdrawWarehouseItemResult
{
Success = false
};
}
alreadyExistentItem.ItemInstance.Amount -= amount;
bool toRemove = alreadyExistentItem.ItemInstance.Amount == 0;
if (toRemove)
{
familyWarehouse.Remove(warehouseItemDtoToWithdraw.Slot);
}
await SetItemChangeWithLock(alreadyExistentItem, toRemove);
ItemInstanceDTO newItemInstance = alreadyExistentItem.ItemInstance.Adapt<ItemInstanceDTO>();
newItemInstance.Amount = amount;
return new WithdrawWarehouseItemResult
{
Success = true,
UpdatedItem = alreadyExistentItem.ItemInstance.Amount == 0 ? null : alreadyExistentItem,
WithdrawnItem = newItemInstance
};
}
finally
{
accountLock.Release();
}
}
public async Task<MoveWarehouseItemResult> MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot)
{
long accountId = warehouseItemDtoToMove.AccountId;
if (amount < 1 || 999 < amount)
{
return new MoveWarehouseItemResult
{
Success = false
};
}
SemaphoreSlim accountLock = GetAccountLock(accountId);
await accountLock.WaitAsync();
try
{
Dictionary<short, AccountWarehouseItemDto> familyWarehouse = await GetAccountWarehouse(accountId);
if (familyWarehouse == null)
{
return new MoveWarehouseItemResult
{
Success = false
};
}
AccountWarehouseItemDto toMoveItem = familyWarehouse.GetValueOrDefault(warehouseItemDtoToMove.Slot);
AccountWarehouseItemDto toMergeItem = familyWarehouse.GetValueOrDefault(newSlot);
if (toMoveItem == null || toMoveItem.ItemInstance.Amount < amount)
{
return new MoveWarehouseItemResult
{
Success = false
};
}
if (toMergeItem == null)
{
if (amount == toMoveItem.ItemInstance.Amount)
{
familyWarehouse.Remove(toMoveItem.Slot);
await SetItemChangeWithLock(new AccountWarehouseItemDto
{
AccountId = accountId,
Slot = toMoveItem.Slot
}, true);
toMoveItem.Slot = newSlot;
familyWarehouse[toMoveItem.Slot] = toMoveItem;
await SetItemChangeWithLock(toMoveItem, false);
return new MoveWarehouseItemResult
{
Success = true,
OldItem = null,
NewItem = toMoveItem
};
}
toMoveItem.ItemInstance.Amount -= amount;
await SetItemChangeWithLock(toMoveItem, false);
var newItem = new AccountWarehouseItemDto
{
AccountId = accountId,
ItemInstance = toMoveItem.ItemInstance.Adapt<ItemInstanceDTO>(),
Slot = newSlot
};
newItem.ItemInstance.Amount = amount;
familyWarehouse[newItem.Slot] = newItem;
await SetItemChangeWithLock(newItem, false);
return new MoveWarehouseItemResult
{
Success = true,
OldItem = toMoveItem,
NewItem = newItem
};
}
if (toMoveItem.ItemInstance.ItemVNum != toMergeItem.ItemInstance.ItemVNum)
{
toMergeItem.Slot = toMoveItem.Slot;
toMoveItem.Slot = newSlot;
familyWarehouse[toMoveItem.Slot] = toMoveItem;
await SetItemChangeWithLock(toMoveItem, false);
familyWarehouse[toMergeItem.Slot] = toMergeItem;
await SetItemChangeWithLock(toMergeItem, false);
return new MoveWarehouseItemResult
{
Success = true,
OldItem = toMergeItem,
NewItem = toMoveItem
};
}
if (toMoveItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || toMergeItem.ItemInstance.Type != ItemInstanceType.NORMAL_ITEM || amount + toMergeItem.ItemInstance.Amount > 999)
{
return new MoveWarehouseItemResult
{
Success = false
};
}
toMoveItem.ItemInstance.Amount -= amount;
toMergeItem.ItemInstance.Amount += amount;
bool toRemove = toMoveItem.ItemInstance.Amount == 0;
if (toRemove)
{
familyWarehouse.Remove(toMoveItem.Slot);
}
await SetItemChangeWithLock(toMoveItem, toRemove);
await SetItemChangeWithLock(toMergeItem, false);
return new MoveWarehouseItemResult
{
Success = true,
OldItem = toRemove ? null : toMoveItem,
NewItem = toMergeItem
};
}
finally
{
accountLock.Release();
}
}
public async Task FlushWarehouseSaves()
{
if (_itemChanges.Count < 1)
{
return;
}
await _itemChangesSemaphore.WaitAsync();
try
{
List<(AccountWarehouseItemDto dto, bool remove)> unsavedChanges = new();
var globalWatch = Stopwatch.StartNew();
foreach ((long accountId, Dictionary<short, (AccountWarehouseItemDto dto, bool remove)> warehouseChanges) in _itemChanges)
{
List<AccountWarehouseItemDto> itemsToSave = new();
List<AccountWarehouseItemDto> itemsToRemove = new();
foreach ((short _, (AccountWarehouseItemDto dto, bool remove)) in warehouseChanges)
{
(remove ? itemsToRemove : itemsToSave).Add(dto);
}
if (itemsToSave.Count > 0)
{
try
{
int countSavedItems = await _accountWarehouseItemDao.SaveAsync(itemsToSave);
Log.Warn($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Saved {countSavedItems.ToString()} warehouseItems");
}
catch (Exception e)
{
Log.Error(
$"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Error while trying to save {itemsToSave.Count.ToString()} warehouseItems. Re-queueing. ",
e);
unsavedChanges.AddRange(itemsToSave.Select(x => (x, false)));
}
}
if (itemsToRemove.Count < 1)
{
continue;
}
try
{
await _accountWarehouseItemDao.DeleteAsync(itemsToRemove);
Log.Warn($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Removed (at maximum) {itemsToRemove.Count.ToString()} warehouseItems");
}
catch (Exception e)
{
Log.Error(
$"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES][ACCOUNT_ID: {accountId.ToString()}] Error while trying to remove {itemsToRemove.Count.ToString()} warehouseItems. Re-queueing. ",
e);
unsavedChanges.AddRange(itemsToRemove.Select(x => (x, true)));
}
}
globalWatch.Stop();
Log.Debug($"[ACCOUNT_WAREHOUSE_MANAGER][FLUSH_SAVES] Saving all warehouses took {globalWatch.ElapsedMilliseconds.ToString()}ms");
_itemChanges.Clear();
foreach ((AccountWarehouseItemDto dto, bool remove) in unsavedChanges)
{
SetItemChange(dto, remove);
}
}
finally
{
_itemChangesSemaphore.Release();
}
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await FlushWarehouseSaves();
await Task.Delay(Interval, stoppingToken);
}
}
private SemaphoreSlim GetAccountLock(long accountId) => _warehouseLocks.GetOrAdd(accountId, new SemaphoreSlim(1, 1));
private async Task SetItemChangeWithLock(AccountWarehouseItemDto dto, bool remove)
{
await _itemChangesSemaphore.WaitAsync();
try
{
SetItemChange(dto, remove);
}
finally
{
_itemChangesSemaphore.Release();
}
}
/// <summary>
/// Not to be used outside SemaphoreSlim
/// </summary>
private void SetItemChange(AccountWarehouseItemDto dto, bool remove)
{
_itemChanges.GetOrSetDefault(dto.AccountId, new Dictionary<short, (AccountWarehouseItemDto dto, bool remove)>())[dto.Slot] = (dto, remove);
}
private async Task<Dictionary<short, AccountWarehouseItemDto>> GetAccountWarehouse(long accountId)
{
Dictionary<short, AccountWarehouseItemDto> cachedItems = _cachedWarehouseItems.Get(accountId);
if (cachedItems != null)
{
return cachedItems;
}
cachedItems = (await _accountWarehouseItemDao.GetByAccountIdAsync(accountId))?.ToDictionary(x => x.Slot);
_cachedWarehouseItems.Set(accountId, cachedItems ?? new Dictionary<short, AccountWarehouseItemDto>(), LifeTime);
return cachedItems;
}
}
}

View file

@ -0,0 +1,11 @@
using WingsAPI.Data.Account;
namespace DatabaseServer.Managers
{
public class AddWarehouseItemResult
{
public bool Success { get; init; }
public AccountWarehouseItemDto UpdatedItem { get; init; }
}
}

View file

@ -0,0 +1,317 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Data.Character;
using WingsEmu.DTOs.Enums;
namespace DatabaseServer.Managers
{
public class CharacterManager : BackgroundService, ICharacterManager
{
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerCharSaveIntervalSeconds) ?? "60"));
private static readonly TimeSpan LifeTime = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.DbServerCharTtlMinutes) ?? "30"));
private readonly ILongKeyCachedRepository<CharacterDTO> _characterById;
private readonly ICharacterDAO _characterDao;
private readonly IKeyValueCache<long> _characterIdByKey;
private readonly HashSet<long> _characterIdsToSave = new();
private readonly SemaphoreSlim _createCharacterSemaphore = new(1, 1);
private readonly SemaphoreSlim _semaphoreSlim = new(1, 1);
public CharacterManager(ICharacterDAO characterDao, ILongKeyCachedRepository<CharacterDTO> characterById, IKeyValueCache<long> characterIdByKey)
{
_characterDao = characterDao;
_characterById = characterById;
_characterIdByKey = characterIdByKey;
}
public async Task<IEnumerable<CharacterDTO>> GetCharactersByAccountId(long accountId) => await _characterDao.LoadByAccountAsync(accountId);
public async Task<CharacterDTO> GetCharacterBySlot(long accountId, byte slot)
{
long characterId = _characterIdByKey.Get(GetKeyAccountIdSlot(accountId, slot));
// check if the cache does have the value
if (characterId != default)
{
Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacterBySlot] CharacterId fetched from cache via AccountId and Slot. AccountId: '{accountId.ToString()}' Slot: '{slot.ToString()}'");
return await GetCharacterById(characterId);
}
CharacterDTO characterDto = await _characterDao.LoadBySlotAsync(accountId, slot);
if (characterDto == null)
{
// shouldn't happen normally
Log.Warn(
$"[CHARACTER_SAVE_SYSTEM][GetCharacterBySlot] The given AccountId and Slot does not pertain to any CharacterDTO in the db. AccountId: '{accountId.ToString()}' Slot: '{slot.ToString()}'");
return null;
}
SetCharacter(characterDto);
Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacterBySlot] fetched by AccountId and Slot. AccountId: '{accountId.ToString()}' Slot: '{slot.ToString()}'");
return characterDto;
}
public async Task<CharacterDTO> GetCharacterById(long characterId)
{
CharacterDTO dto = _characterById.Get(characterId);
if (dto != null)
{
Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacter] CharacterDTO fetched from cache via CharacterId: '{characterId.ToString()}'");
return dto;
}
dto = await _characterDao.GetByIdAsync(characterId);
if (dto == null)
{
Log.Warn($"[CHARACTER_SAVE_SYSTEM][GetCharacter] The given CharacterId does not pertain to any CharacterDTO in the db. CharacterId: '{characterId.ToString()}'");
return null;
}
SetCharacter(dto);
Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacter] {characterId.ToString()} fetched from DB cause was not existing in cache");
return dto;
}
public async Task<CharacterDTO> GetCharacterByName(string name)
{
long characterId = _characterIdByKey.Get(GetKey(name));
// check if the cache does have the value
if (characterId != default)
{
Log.Debug($"[CHARACTER_SAVE_SYSTEM][GetCharacter] CharacterId fetched from cache through CharacterName. CharacterId: '{characterId.ToString()}' CharacterName: '{name}'");
return await GetCharacterById(characterId);
}
CharacterDTO characterDto = await _characterDao.LoadByNameAsync(name);
if (characterDto == null)
{
// shouldn't happen normally
Log.Warn($"[CHARACTER_SAVE_SYSTEM][GetCharacter] The given CharacterName does not exist in the db. CharacterName: '{name}'");
return null;
}
SetCharacter(characterDto);
Log.Debug(
$"[CHARACTER_SAVE_SYSTEM][GetCharacter] CharacterDTO fetched from DB through CharacterName cause it was not existing in cache. CharacterId: '{characterId.ToString()}' CharacterName: '{name}'");
return characterDto;
}
public async Task<CharacterDTO> CreateCharacter(CharacterDTO characterDto, bool ignoreSlotCheck)
{
await _createCharacterSemaphore.WaitAsync();
try
{
if (await _characterDao.LoadByNameAsync(characterDto.Name) != null)
{
return null;
}
CharacterDTO character;
if (ignoreSlotCheck == false)
{
character = await GetCharacterBySlot(characterDto.AccountId, characterDto.Slot);
if (character != null)
{
Log.Warn("[CHARACTER_SAVE_SYSTEM][CreateCharacter] Found a character already in the desired slot." +
$"AccountId: '{character.AccountId.ToString()}' CharacterId: '{character.Id.ToString()}' Slot: '{character.Slot.ToString()}'");
return null;
}
}
character = await _characterDao.SaveAsync(characterDto);
SetCharacter(character);
Log.Debug(
$"[CHARACTER_SAVE_SYSTEM][CreateCharacter] Created a new character. AccountId: '{character.AccountId.ToString()}' CharacterId: '{character.Id.ToString()}' Slot: '{character.Slot.ToString()}'");
return character;
}
finally
{
_createCharacterSemaphore.Release();
}
}
public async Task AddCharacterToSavingQueue(CharacterDTO characterDto)
{
SetCharacter(characterDto);
await _semaphoreSlim.WaitAsync();
try
{
_characterIdsToSave.Add(characterDto.Id);
}
finally
{
_semaphoreSlim.Release();
}
Log.Debug($"[CHARACTER_SAVE_SYSTEM][AddCharacterToSavingQueue] Flagged CharacterId for saving. CharacterId: '{characterDto.Id.ToString()}'");
}
public async Task AddCharactersToSavingQueue(IEnumerable<CharacterDTO> characterDtos)
{
int i = 0;
await _semaphoreSlim.WaitAsync();
try
{
foreach (CharacterDTO characterDto in characterDtos)
{
SetCharacter(characterDto);
_characterIdsToSave.Add(characterDto.Id);
i++;
}
}
finally
{
_semaphoreSlim.Release();
}
Log.Debug($"[CHARACTER_SAVE_SYSTEM][AddCharactersToSavingQueue] Flagged {i.ToString()} characterIds for saving.");
}
public async Task<bool> DeleteCharacter(CharacterDTO characterDto)
{
DeleteResult result = await _characterDao.DeleteByPrimaryKey(characterDto.AccountId, characterDto.Slot);
if (result != DeleteResult.Deleted)
{
Log.Warn(
$"[CHARACTER_SAVE_SYSTEM][DeleteCharacter] Tried to delete a character that doesn't exist. AccountId: '{characterDto.AccountId.ToString()}' Slot: '{characterDto.Slot.ToString()}");
return false;
}
RemoveCharacter(characterDto);
Log.Debug(
$"[CHARACTER_SAVE_SYSTEM][DeleteCharacter] Deleted a character. AccountId: '{characterDto.AccountId.ToString()}' CharacterId: '{characterDto.Id.ToString()}' Slot: '{characterDto.Slot.ToString()}'");
return true;
}
public async Task<int> FlushCharacterSaves()
{
if (_characterIdsToSave.Count < 1)
{
return 0;
}
List<long> unsavedCharacterIds = new();
long[] characterIds;
await _semaphoreSlim.WaitAsync();
try
{
characterIds = new long[_characterIdsToSave.Count];
_characterIdsToSave.CopyTo(characterIds);
_characterIdsToSave.Clear();
}
finally
{
_semaphoreSlim.Release();
}
int count = 0;
var tmp = Stopwatch.StartNew();
var individualWatch = new Stopwatch();
foreach (long characterId in characterIds)
{
CharacterDTO toSave = _characterById.Get(characterId);
if (toSave == null)
{
Log.Error($"[CHARACTER_SAVE_SYSTEM] {characterId.ToString()} could not be retrieved from cache", new DataException($"Desynchronised data for character {characterId.ToString()}"));
continue;
}
individualWatch.Restart();
try
{
CharacterDTO savedCharacter = await _characterDao.SaveAsync(toSave);
count++;
Log.Warn($"[CHARACTER_SAVE_SYSTEM] Saved a character successfully in {individualWatch.ElapsedMilliseconds.ToString()}ms. " +
$"CharacterName: '{savedCharacter.Name}' CharacterId: '{savedCharacter.Id.ToString()}' AccountId: '{savedCharacter.AccountId.ToString()}'");
}
catch (Exception e)
{
unsavedCharacterIds.Add(characterId);
Log.Error($"[CHARACTER_SAVE_SYSTEM] Failed to save a character in {individualWatch.ElapsedMilliseconds.ToString()}ms. Re-queueing the save. " +
$"CharacterName: '{toSave.Name}' CharacterId: '{toSave.Id.ToString()}' AccountId: '{toSave.AccountId.ToString()}'", e);
}
individualWatch.Stop();
}
tmp.Stop();
Log.Debug($"[CHARACTER_SAVE_SYSTEM] Saving of saves took in total {tmp.ElapsedMilliseconds.ToString()}ms");
if (unsavedCharacterIds.Count <= 0)
{
return count;
}
await _semaphoreSlim.WaitAsync();
try
{
foreach (long characterId in unsavedCharacterIds)
{
_characterIdsToSave.Add(characterId);
}
}
finally
{
_semaphoreSlim.Release();
}
Log.Warn($"[CHARACTER_SAVE_SYSTEM] Re-queued {unsavedCharacterIds.Count.ToString()} character saves");
return count;
}
public async Task<CharacterDTO> RemoveCachedCharacter(string requestCharacterName)
{
CharacterDTO characterDto = await GetCharacterByName(requestCharacterName);
if (characterDto == null)
{
return null;
}
await _characterDao.SaveAsync(characterDto);
_characterById.Remove(characterDto.Id);
return characterDto;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
await FlushCharacterSaves();
await Task.Delay(Interval, stoppingToken);
}
}
private static string GetKey(string name) => $"char:name:{name}";
private static string GetKeyAccountIdSlot(long accountId, byte slot) => $"char:account-id:{accountId.ToString()}:slot:{slot.ToString()}";
private void SetCharacter(CharacterDTO characterDto)
{
_characterById.Set(characterDto.Id, characterDto, LifeTime);
_characterIdByKey.Set(GetKey(characterDto.Name), characterDto.Id, LifeTime);
_characterIdByKey.Set(GetKeyAccountIdSlot(characterDto.AccountId, characterDto.Slot), characterDto.Id, LifeTime);
}
private void RemoveCharacter(CharacterDTO characterDto)
{
_characterById.Remove(characterDto.Id);
_characterIdByKey.Remove(GetKey(characterDto.Name));
_characterIdByKey.Remove(GetKeyAccountIdSlot(characterDto.AccountId, characterDto.Slot));
_characterIdsToSave.Remove(characterDto.Id);
}
}
}

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using WingsAPI.Data.Account;
namespace DatabaseServer.Managers
{
public interface IAccountWarehouseManager
{
public Task<IEnumerable<AccountWarehouseItemDto>> GetWarehouse(long accountId);
public Task<AccountWarehouseItemDto> GetWarehouseItem(long accountId, short slot);
public Task<AddWarehouseItemResult> AddWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToAdd);
public Task<WithdrawWarehouseItemResult> WithdrawWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToWithdraw, int amount);
public Task<MoveWarehouseItemResult> MoveWarehouseItem(AccountWarehouseItemDto warehouseItemDtoToMove, int amount, short newSlot);
public Task FlushWarehouseSaves();
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using WingsAPI.Data.Character;
namespace DatabaseServer.Managers
{
public interface ICharacterManager
{
public Task<IEnumerable<CharacterDTO>> GetCharactersByAccountId(long accountId);
public Task<CharacterDTO> GetCharacterBySlot(long accountId, byte slot);
public Task<CharacterDTO> GetCharacterById(long characterId);
public Task<CharacterDTO> GetCharacterByName(string name);
public Task<CharacterDTO> CreateCharacter(CharacterDTO characterDto, bool ignoreSlotCheck);
public Task AddCharacterToSavingQueue(CharacterDTO characterDto);
public Task AddCharactersToSavingQueue(IEnumerable<CharacterDTO> characterDtos);
public Task<bool> DeleteCharacter(CharacterDTO characterDto);
public Task<int> FlushCharacterSaves();
public Task<CharacterDTO> RemoveCachedCharacter(string requestCharacterName);
}
}

View file

@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using WingsAPI.Data.Character;
namespace DatabaseServer.Managers
{
public interface IRankingManager
{
/// <summary>
/// Retrieves a list of the top 30 characters in compliments
/// </summary>
/// <returns></returns>
Task<IReadOnlyList<CharacterDTO>> GetTopCompliment();
/// <summary>
/// Retrieves a list of the top 30 characters in points
/// </summary>
/// <returns></returns>
Task<IReadOnlyList<CharacterDTO>> GetTopPoints();
/// <summary>
/// Retrieves a list of the top 43 characters in reputation
/// </summary>
/// <returns></returns>
Task<IReadOnlyList<CharacterDTO>> GetTopReputation();
/// <summary>
/// Tries to refresh the ranking, in case it fails it will return false
/// </summary>
/// <returns></returns>
Task<RefreshResponse> TryRefreshRanking();
}
public class RefreshResponse
{
public bool Success { get; init; }
public IReadOnlyList<CharacterDTO> TopCompliment { get; init; }
public IReadOnlyList<CharacterDTO> TopPoints { get; init; }
public IReadOnlyList<CharacterDTO> TopReputation { get; init; }
}
}

View file

@ -0,0 +1,14 @@
using System.Threading.Tasks;
using WingsAPI.Data.TimeSpace;
namespace DatabaseServer.Managers
{
public interface ITimeSpaceManager
{
Task<TimeSpaceRecordDto> GetRecordByTimeSpaceId(long tsId);
Task FlushTimeSpaceRecords();
Task Initialize();
void TryAddNewRecord(TimeSpaceRecordDto record);
Task<bool> IsNewRecord(long tsId, long points);
}
}

View file

@ -0,0 +1,13 @@
using WingsAPI.Data.Account;
namespace DatabaseServer.Managers
{
public class MoveWarehouseItemResult
{
public bool Success { get; init; }
public AccountWarehouseItemDto OldItem { get; init; }
public AccountWarehouseItemDto NewItem { get; init; }
}
}

View file

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PhoenixLib.Logging;
using WingsAPI.Data.Character;
namespace DatabaseServer.Managers
{
public class RankingManager : IRankingManager
{
private readonly ICharacterDAO _characterDao;
private IReadOnlyList<CharacterDTO> _topCompliment;
private IReadOnlyList<CharacterDTO> _topPoints;
private IReadOnlyList<CharacterDTO> _topReputation;
public RankingManager(ICharacterDAO characterDao) => _characterDao = characterDao;
public async Task<IReadOnlyList<CharacterDTO>> GetTopCompliment()
{
if (_topCompliment != null)
{
return _topCompliment;
}
try
{
_topCompliment = await _characterDao.GetTopCompliment();
}
catch (Exception e)
{
Log.Error("[RANKING_MANAGER][GET_TOP_COMPLIMENT] Unexpected error:", e);
}
return _topCompliment;
}
public async Task<IReadOnlyList<CharacterDTO>> GetTopPoints()
{
if (_topPoints != null)
{
return _topPoints;
}
try
{
_topPoints = await _characterDao.GetTopPoints();
}
catch (Exception e)
{
Log.Error("[RANKING_MANAGER][GET_TOP_POINTS] Unexpected error:", e);
}
return _topPoints;
}
public async Task<IReadOnlyList<CharacterDTO>> GetTopReputation()
{
if (_topReputation != null)
{
return _topReputation;
}
try
{
_topReputation = await _characterDao.GetTopReputation();
}
catch (Exception e)
{
Log.Error("[RANKING_MANAGER][GET_TOP_REPUTATION] Unexpected error:", e);
}
return _topReputation;
}
public async Task<RefreshResponse> TryRefreshRanking()
{
try
{
_topCompliment = await _characterDao.GetTopCompliment();
}
catch (Exception e)
{
Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error:", e);
return new RefreshResponse
{
Success = false
};
}
try
{
_topPoints = await _characterDao.GetTopPoints();
}
catch (Exception e)
{
Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error:", e);
return new RefreshResponse
{
Success = false
};
}
try
{
_topReputation = await _characterDao.GetTopReputation();
}
catch (Exception e)
{
Log.Error("[RANKING_MANAGER][TRY_REFRESH_RANKING] Unexpected error:", e);
return new RefreshResponse
{
Success = false
};
}
return new RefreshResponse
{
Success = true,
TopCompliment = _topCompliment,
TopPoints = _topPoints,
TopReputation = _topReputation
};
}
}
}

View file

@ -0,0 +1,10 @@
using System;
namespace DatabaseServer.Managers
{
internal record SaveRequest
{
public DateTime CreatedAt { get; init; }
public long CharacterId { get; init; }
}
}

View file

@ -0,0 +1,119 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Data.TimeSpace;
namespace DatabaseServer.Managers
{
public class TimeSpaceManager : BackgroundService, ITimeSpaceManager
{
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(Convert.ToUInt32(Environment.GetEnvironmentVariable(EnvironmentConsts.TsServerSaveIntervalMinutes) ?? "5"));
private readonly ConcurrentQueue<TimeSpaceRecordDto> _queue = new();
private readonly ILongKeyCachedRepository<TimeSpaceRecordDto> _records;
private readonly ITimeSpaceRecordDao _timeSpaceRecordDao;
public TimeSpaceManager(ITimeSpaceRecordDao timeSpaceRecordDao, ILongKeyCachedRepository<TimeSpaceRecordDto> records)
{
_timeSpaceRecordDao = timeSpaceRecordDao;
_records = records;
}
public async Task<TimeSpaceRecordDto> GetRecordByTimeSpaceId(long tsId)
{
TimeSpaceRecordDto cached = _records.Get(tsId);
if (cached != null)
{
return cached;
}
TimeSpaceRecordDto record = await _timeSpaceRecordDao.GetRecordById(tsId);
if (record == null)
{
return null;
}
_records.Set(tsId, record);
return record;
}
public async Task FlushTimeSpaceRecords()
{
if (_queue.IsEmpty)
{
return;
}
try
{
while (_queue.TryDequeue(out TimeSpaceRecordDto record))
{
TimeSpaceRecordDto currentRecord = await GetRecordByTimeSpaceId(record.TimeSpaceId);
if (currentRecord == null)
{
_records.Set(record.TimeSpaceId, record);
await _timeSpaceRecordDao.SaveRecord(record);
continue;
}
if (currentRecord.Record >= record.Record)
{
continue;
}
_records.Set(record.TimeSpaceId, record);
await _timeSpaceRecordDao.SaveRecord(record);
}
}
catch (Exception e)
{
Log.Error("FlushTimeSpaceRecords", e);
}
}
public async Task Initialize()
{
IEnumerable<TimeSpaceRecordDto> records = await _timeSpaceRecordDao.GetAllRecords();
int counter = 0;
foreach (TimeSpaceRecordDto recordDto in records)
{
counter++;
_records.Set(recordDto.TimeSpaceId, recordDto);
}
Log.Info($"Initialized {counter} time-space records.");
}
public void TryAddNewRecord(TimeSpaceRecordDto record)
{
_queue.Enqueue(record);
}
public async Task<bool> IsNewRecord(long tsId, long points)
{
TimeSpaceRecordDto currentRecord = await GetRecordByTimeSpaceId(tsId);
if (currentRecord == null)
{
return true;
}
return currentRecord.Record < points;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Initialize();
Log.Info("[TIME_SPACE_MANAGER] Initialized!");
while (!stoppingToken.IsCancellationRequested)
{
await FlushTimeSpaceRecords();
await Task.Delay(Interval, stoppingToken);
}
}
}
}

View file

@ -0,0 +1,14 @@
using WingsAPI.Data.Account;
using WingsEmu.DTOs.Items;
namespace DatabaseServer.Managers
{
public class WithdrawWarehouseItemResult
{
public bool Success { get; init; }
public AccountWarehouseItemDto UpdatedItem { get; init; }
public ItemInstanceDTO WithdrawnItem { get; init; }
}
}

View file

@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using DatabaseServer.Managers;
using FamilyServer;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Npgsql;
using PhoenixLib.Logging;
using PhoenixLib.ServiceBus.MQTT;
using Plugin.Database.DB;
using Plugin.Database.Extensions;
using Plugin.Database.Mapping;
using ProtoBuf.Grpc.Client;
namespace DatabaseServer
{
public class Program
{
public static async Task Main(string[] args)
{
PrintHeader();
NonGameMappingRules.InitializeMapping();
GrpcClientFactory.AllowUnencryptedHttp2 = true;
using var stopService = new DockerGracefulStopService();
using IHost host = CreateHostBuilder(args).Build();
IDbContextFactory<GameContext> dbContextFactory = host.Services.GetRequiredService<IDbContextFactory<GameContext>>();
if (!await dbContextFactory.TryMigrateAsync())
{
throw new PostgresException("Couldn't migrate the database", "ERROR", "ERROR", "None");
}
await host.StartAsync();
IMessagingService messagingService = host.Services.GetService<IMessagingService>();
if (messagingService != null)
{
await messagingService.StartAsync();
}
Log.Info("Database Server started");
ICharacterManager characterManager = host.Services.GetRequiredService<ICharacterManager>();
IAccountWarehouseManager accountWarehouseManager = host.Services.GetRequiredService<IAccountWarehouseManager>();
ITimeSpaceManager timespaceManager = host.Services.GetRequiredService<ITimeSpaceManager>();
await host.WaitForShutdownAsync(stopService.CancellationToken);
if (messagingService != null)
{
await messagingService.DisposeAsync();
}
await characterManager.FlushCharacterSaves();
await accountWarehouseManager.FlushWarehouseSaves();
await timespaceManager.FlushTimeSpaceRecords();
}
private static void PrintHeader()
{
const string text = @"
";
string separator = new('=', Console.WindowWidth);
string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s))
.Aggregate("", (current, i) => current + i);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator);
Console.ForegroundColor = ConsoleColor.White;
}
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
private static IHostBuilder CreateHostBuilder(string[] args)
{
IHostBuilder host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(s =>
{
s.ListenAnyIP(short.Parse(Environment.GetEnvironmentVariable("DATABASE_SERVER_PORT") ?? "29999"), options => { options.Protocols = HttpProtocols.Http2; });
});
webBuilder.UseStartup<Startup>();
});
return host;
}
}
}

View file

@ -0,0 +1,173 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using PhoenixLib.Logging;
using WingsAPI.Communication;
using WingsAPI.Communication.DbServer.AccountService;
using WingsAPI.Data.Account;
namespace DatabaseServer.Services
{
public class AccountService : IAccountService
{
private readonly IAccountBanDao _accountBanDao;
private readonly IAccountDAO _accountDao;
private readonly IAccountPenaltyDao _accountPenaltyDao;
public AccountService(IAccountDAO accountDao, IAccountBanDao accountBanDao, IAccountPenaltyDao accountPenaltyDao)
{
_accountDao = accountDao;
_accountBanDao = accountBanDao;
_accountPenaltyDao = accountPenaltyDao;
}
public async Task<AccountLoadResponse> LoadAccountByName(AccountLoadByNameRequest request)
{
AccountDTO dto = null;
try
{
dto = await _accountDao.GetByNameAsync(request.Name);
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][LOAD_ACCOUNT_BY_NAME] Unexpected error: ", e);
}
return new AccountLoadResponse
{
ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
AccountDto = dto
};
}
public async Task<AccountLoadResponse> LoadAccountById(AccountLoadByIdRequest request)
{
AccountDTO dto = null;
try
{
dto = await _accountDao.GetByIdAsync(request.AccountId);
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][LOAD_ACCOUNT_BY_ID] Unexpected error: ", e);
}
return new AccountLoadResponse
{
ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
AccountDto = dto
};
}
public async Task<AccountSaveResponse> SaveAccount(AccountSaveRequest request)
{
AccountDTO dto = null;
try
{
dto = await _accountDao.SaveAsync(request.AccountDto);
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][SAVE_ACCOUNT] Unexpected error: ", e);
}
return new AccountSaveResponse
{
ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
AccountDto = dto
};
}
public async Task<AccountBanGetResponse> GetAccountBan(AccountBanGetRequest request)
{
try
{
AccountBanDto dto = await _accountBanDao.FindAccountBan(request.AccountId);
return new AccountBanGetResponse
{
ResponseType = RpcResponseType.SUCCESS,
AccountBanDto = dto
};
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][GET_ACCOUNT_BAN] Unexpected error: ", e);
}
return new AccountBanGetResponse
{
ResponseType = RpcResponseType.GENERIC_SERVER_ERROR
};
}
public async Task<AccountBanSaveResponse> SaveAccountBan(AccountBanSaveRequest request)
{
AccountBanDto dto = null;
try
{
dto = await _accountBanDao.SaveAsync(request.AccountBanDto);
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][SAVE_ACCOUNT_BAN] Unexpected error: ", e);
}
return new AccountBanSaveResponse
{
ResponseType = dto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
AccountBanDto = dto
};
}
public async Task<AccountPenaltyGetAllResponse> GetAccountPenalties(AccountPenaltyGetRequest request)
{
try
{
List<AccountPenaltyDto> dtos = await _accountPenaltyDao.GetPenaltiesByAccountId(request.AccountId);
return new AccountPenaltyGetAllResponse
{
ResponseType = RpcResponseType.SUCCESS,
AccountPenaltyDtos = dtos
};
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][GET_ACCOUNT_PENALTIES] Unexpected error: ", e);
}
return new AccountPenaltyGetAllResponse
{
ResponseType = RpcResponseType.GENERIC_SERVER_ERROR
};
}
public async Task<AccountPenaltyMultiSaveResponse> SaveAccountPenalties(AccountPenaltyMultiSaveRequest request)
{
if (request.AccountPenaltyDtos == null)
{
return new AccountPenaltyMultiSaveResponse
{
ResponseType = RpcResponseType.SUCCESS
};
}
IEnumerable<AccountPenaltyDto> dtos = null;
try
{
dtos = await _accountPenaltyDao.SaveAsync(request.AccountPenaltyDtos);
}
catch (Exception e)
{
Log.Error("[ACCOUNT_SERVICE][SAVE_ACCOUNT_PENALTIES] Unexpected error: ", e);
}
return new AccountPenaltyMultiSaveResponse
{
ResponseType = dtos == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
AccountPenaltyDtos = dtos
};
}
}
}

View file

@ -0,0 +1,73 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using DatabaseServer.Managers;
using WingsAPI.Communication;
using WingsAPI.Communication.DbServer.WarehouseService;
using WingsAPI.Data.Account;
namespace DatabaseServer.Services
{
public class AccountWarehouseService : IAccountWarehouseService
{
private readonly IAccountWarehouseManager _accountWarehouseManager;
public AccountWarehouseService(IAccountWarehouseManager accountWarehouseManager) => _accountWarehouseManager = accountWarehouseManager;
public async ValueTask<AccountWarehouseGetItemsResponse> GetItems(AccountWarehouseGetItemsRequest request)
{
IEnumerable<AccountWarehouseItemDto> warehouseItemDtos = await _accountWarehouseManager.GetWarehouse(request.AccountId);
return new AccountWarehouseGetItemsResponse
{
ResponseType = warehouseItemDtos == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Items = warehouseItemDtos
};
}
public async ValueTask<AccountWarehouseGetItemResponse> GetItem(AccountWarehouseGetItemRequest request)
{
AccountWarehouseItemDto warehouseItemDto = await _accountWarehouseManager.GetWarehouseItem(request.AccountId, request.Slot);
return new AccountWarehouseGetItemResponse
{
ResponseType = warehouseItemDto == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Item = warehouseItemDto
};
}
public async ValueTask<AccountWarehouseAddItemResponse> AddItem(AccountWarehouseAddItemRequest request)
{
AddWarehouseItemResult result = await _accountWarehouseManager.AddWarehouseItem(request.Item);
return new AccountWarehouseAddItemResponse
{
ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR,
Item = result.UpdatedItem
};
}
public async ValueTask<AccountWarehouseWithdrawItemResponse> WithdrawItem(AccountWarehouseWithdrawItemRequest request)
{
WithdrawWarehouseItemResult result = await _accountWarehouseManager.WithdrawWarehouseItem(request.ItemToWithdraw, request.Amount);
return new AccountWarehouseWithdrawItemResponse
{
ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR,
UpdatedItem = result.UpdatedItem,
WithdrawnItem = result.WithdrawnItem
};
}
public async ValueTask<AccountWarehouseMoveItemResponse> MoveItem(AccountWarehouseMoveItemRequest request)
{
MoveWarehouseItemResult result = await _accountWarehouseManager.MoveWarehouseItem(request.WarehouseItemDtoToMove, request.Amount, request.NewSlot);
return new AccountWarehouseMoveItemResponse
{
ResponseType = result.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR,
OldItem = result.OldItem,
NewItem = result.NewItem
};
}
}
}

View file

@ -0,0 +1,171 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using DatabaseServer.Managers;
using WingsAPI.Communication;
using WingsAPI.Communication.DbServer.CharacterService;
using WingsAPI.Data.Character;
namespace DatabaseServer.Services
{
public class CharacterService : ICharacterService
{
private readonly ICharacterManager _characterManager;
private readonly IRankingManager _rankingManager;
public CharacterService(ICharacterManager characterManager, IRankingManager rankingManager)
{
_characterManager = characterManager;
_rankingManager = rankingManager;
}
public async Task<DbServerSaveCharactersResponse> SaveCharacters(DbServerSaveCharactersRequest request)
{
await _characterManager.AddCharactersToSavingQueue(request.Characters);
return new DbServerSaveCharactersResponse
{
RpcResponseType = RpcResponseType.SUCCESS
};
}
public async Task<DbServerSaveCharacterResponse> SaveCharacter(DbServerSaveCharacterRequest request)
{
await _characterManager.AddCharacterToSavingQueue(request.Character);
return new DbServerSaveCharacterResponse
{
RpcResponseType = RpcResponseType.SUCCESS
};
}
public async Task<DbServerSaveCharacterResponse> CreateCharacter(DbServerSaveCharacterRequest request)
{
CharacterDTO character = await _characterManager.CreateCharacter(request.Character, request.IgnoreSlotCheck);
return new DbServerSaveCharacterResponse
{
RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Character = character
};
}
public async Task<DbServerGetCharactersResponse> GetCharacters(DbServerGetCharactersRequest request)
{
IEnumerable<CharacterDTO> characters = await _characterManager.GetCharactersByAccountId(request.AccountId);
return new DbServerGetCharactersResponse
{
RpcResponseType = characters == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Characters = characters
};
}
public async Task<DbServerGetCharacterResponse> GetCharacterBySlot(DbServerGetCharacterFromSlotRequest fromSlotRequest)
{
CharacterDTO character = await _characterManager.GetCharacterBySlot(fromSlotRequest.AccountId, fromSlotRequest.Slot);
return new DbServerGetCharacterResponse
{
RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
CharacterDto = character
};
}
public async Task<DbServerGetCharacterResponse> GetCharacterById(DbServerGetCharacterByIdRequest request)
{
CharacterDTO character = await _characterManager.GetCharacterById(request.CharacterId);
return new DbServerGetCharacterResponse
{
RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
CharacterDto = character
};
}
public async Task<DbServerGetCharacterResponse> GetCharacterByName(DbServerGetCharacterRequestByName request)
{
CharacterDTO character = await _characterManager.GetCharacterByName(request.CharacterName);
return new DbServerGetCharacterResponse
{
RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
CharacterDto = character
};
}
public async Task<DbServerFlushCharacterSavesResponse> FlushCharacterSaves(DbServerFlushCharacterSavesRequest request)
{
await _characterManager.FlushCharacterSaves();
return new DbServerFlushCharacterSavesResponse
{
RpcResponseType = RpcResponseType.SUCCESS
};
}
public async Task<DbServerDeleteCharacterResponse> DeleteCharacter(DbServerDeleteCharacterRequest request)
{
bool success = await _characterManager.DeleteCharacter(request.CharacterDto);
return new DbServerDeleteCharacterResponse
{
RpcResponseType = success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR
};
}
public async Task<DbServerGetCharacterResponse> ForceRemoveCharacterFromCache(DbServerGetCharacterRequestByName request)
{
CharacterDTO character = await _characterManager.RemoveCachedCharacter(request.CharacterName);
return new DbServerGetCharacterResponse
{
RpcResponseType = character == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
CharacterDto = character
};
}
public async ValueTask<CharacterGetTopResponse> GetTopCompliment(EmptyRpcRequest request)
{
IReadOnlyList<CharacterDTO> top = await _rankingManager.GetTopCompliment();
return new CharacterGetTopResponse
{
ResponseType = top == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Top = top
};
}
public async ValueTask<CharacterGetTopResponse> GetTopPoints(EmptyRpcRequest request)
{
IReadOnlyList<CharacterDTO> top = await _rankingManager.GetTopPoints();
return new CharacterGetTopResponse
{
ResponseType = top == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Top = top
};
}
public async ValueTask<CharacterGetTopResponse> GetTopReputation(EmptyRpcRequest request)
{
IReadOnlyList<CharacterDTO> top = await _rankingManager.GetTopReputation();
return new CharacterGetTopResponse
{
ResponseType = top == null ? RpcResponseType.GENERIC_SERVER_ERROR : RpcResponseType.SUCCESS,
Top = top
};
}
public async ValueTask<CharacterRefreshRankingResponse> RefreshRanking(EmptyRpcRequest request)
{
RefreshResponse response = await _rankingManager.TryRefreshRanking();
return new CharacterRefreshRankingResponse
{
ResponseType = response.Success ? RpcResponseType.SUCCESS : RpcResponseType.GENERIC_SERVER_ERROR,
TopCompliment = response.TopCompliment,
TopPoints = response.TopPoints,
TopReputation = response.TopReputation
};
}
}
}

View file

@ -0,0 +1,31 @@
using System.Threading.Tasks;
using DatabaseServer.Managers;
using WingsAPI.Communication;
using WingsAPI.Communication.DbServer.TimeSpaceService;
namespace DatabaseServer.Services
{
public class TimeSpaceService : ITimeSpaceService
{
private readonly ITimeSpaceManager _timeSpaceManager;
public TimeSpaceService(ITimeSpaceManager timeSpaceManager) => _timeSpaceManager = timeSpaceManager;
public async ValueTask<TimeSpaceIsNewRecordResponse> IsNewRecord(TimeSpaceIsNewRecordRequest request) => new()
{
IsNewRecord = await _timeSpaceManager.IsNewRecord(request.TimeSpaceId, request.Record)
};
public async ValueTask<EmptyResponse> SetNewRecord(TimeSpaceNewRecordRequest request)
{
_timeSpaceManager.TryAddNewRecord(request.TimeSpaceRecordDto);
return new EmptyResponse();
}
public async ValueTask<TimeSpaceRecordResponse> GetTimeSpaceRecord(TimeSpaceRecordRequest request) =>
new TimeSpaceRecordResponse
{
TimeSpaceRecordDto = await _timeSpaceManager.GetRecordByTimeSpaceId(request.TimeSpaceId)
};
}
}

View file

@ -0,0 +1,85 @@
using DatabaseServer.Consumers;
using DatabaseServer.Managers;
using DatabaseServer.Services;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using PhoenixLib.ServiceBus.Extensions;
using Plugin.Database;
using Plugin.Database.Extensions;
using ProtoBuf.Grpc.Server;
using WingsAPI.Communication.Services.Messages;
using WingsEmu.Communication.gRPC.Extensions;
using WingsEmu.Health.Extensions;
namespace DatabaseServer
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMqttConfigurationFromEnv();
new DatabasePlugin().AddDependencies(services);
services.AddMaintenanceMode();
services.AddSingleton<CharacterService>();
services.AddSingleton<CharacterManager>();
services.AddHostedService(s => s.GetRequiredService<CharacterManager>());
services.AddSingleton<ICharacterManager>(s => s.GetRequiredService<CharacterManager>());
services.AddSingleton<AccountWarehouseService>();
services.AddSingleton<AccountWarehouseManager>();
services.AddHostedService(s => s.GetRequiredService<AccountWarehouseManager>());
services.AddSingleton<IAccountWarehouseManager>(s => s.GetRequiredService<AccountWarehouseManager>());
services.AddSingleton<TimeSpaceService>();
services.AddSingleton<TimeSpaceManager>();
services.AddHostedService(s => s.GetRequiredService<TimeSpaceManager>());
services.AddSingleton<ITimeSpaceManager>(s => s.GetRequiredService<TimeSpaceManager>());
services.AddSingleton<IRankingManager, RankingManager>();
services.AddPhoenixLogging();
services.AddSingleton<AccountService>();
services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>));
services.TryAddSingleton(typeof(IKeyValueCache<>), typeof(InMemoryKeyValueCache<>));
services.AddGrpcDbServerServiceClient();
services.AddCodeFirstGrpc(config =>
{
config.MaxReceiveMessageSize = null;
config.MaxSendMessageSize = null;
config.EnableDetailedErrors = true;
});
services.AddMessageSubscriber<ServiceFlushAllMessage, ServiceFlushAllMessageConsumer>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseZEfCoreExtensions();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<CharacterService>();
endpoints.MapGrpcService<AccountWarehouseService>();
endpoints.MapGrpcService<TimeSpaceService>();
endpoints.MapGrpcService<AccountService>();
});
}
}
}

View file

@ -0,0 +1,40 @@
// WingsEmu
//
// Developed by NosWings Team
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Player;
using WingsEmu.Game._playerActionLogs;
namespace DiscordNotifier.Consumers.Chat
{
public class LogChatMessageMessageFormatter : IDiscordLogFormatter<LogPlayerChatMessage>
{
public LogType LogType { get; private set; }
public bool TryFormat(LogPlayerChatMessage message, out string formattedString)
{
LogType = message.ChatType switch
{
ChatType.General => LogType.CHAT_GENERAL,
ChatType.HeroChat => LogType.CHAT_GENERAL,
ChatType.SpeechBubble => LogType.CHAT_GENERAL,
ChatType.Whisper => LogType.CHAT_WHISPERS,
ChatType.FriendChat => LogType.CHAT_FRIENDS,
ChatType.Shout => LogType.CHAT_SPEAKERS,
ChatType.FamilyChat => LogType.CHAT_FAMILIES,
ChatType.GroupChat => LogType.CHAT_GROUPS
};
formattedString = $"[ChannelId: '{message.ChannelId.ToString()}']" +
$"[ChatType: '{message.ChatType.ToString()}']" +
$"[CharacterId: '{message.CharacterId.ToString()}'{(message.TargetCharacterId.HasValue ? $" -> TargetId: '{message.TargetCharacterId.Value.ToString()}'" : string.Empty)}]" +
$" {message.Message}";
return true;
}
}
}

View file

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using Discord;
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyCreatedEmbedMessageFormatter : IDiscordEmbedLogFormatter<LogFamilyCreatedMessage>
{
public LogType LogType => LogType.PLAYERS_EVENTS_CHANNEL;
public bool TryFormat(LogFamilyCreatedMessage message, out List<EmbedBuilder> embeds)
{
embeds = new List<EmbedBuilder>
{
new()
{
Author = new EmbedAuthorBuilder { IconUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200" },
Title = "[NosWings] Family created",
Description = $"Family {message.FamilyName} has been created",
Color = Color.Orange,
Footer = new EmbedFooterBuilder().WithIconUrl("https://avatars0.githubusercontent.com/u/40839221?s=200").WithText("discord-notifier microservice"),
//ImageUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200",
// ThumbnailUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200",
Timestamp = DateTimeOffset.Now
}
};
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyCreatedMessageFormatter : IDiscordLogFormatter<LogFamilyCreatedMessage>
{
public LogType LogType => LogType.FAMILY_CREATED;
public bool TryFormat(LogFamilyCreatedMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | HEAD: {message.CharacterName} | DEPUTIES: {string.Join(", ", message.DeputiesIds)} | "
+ $"FAM_ID: {message.FamilyId} | FAM_NAME: {message.FamilyName}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyDisbandedMessageFormatter : IDiscordLogFormatter<LogFamilyDisbandedMessage>
{
public LogType LogType => LogType.FAMILY_DISBANDED;
public bool TryFormat(LogFamilyDisbandedMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | "
+ $"PLAYER: {message.CharacterName} | FAM_ID: {message.FamilyId}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyJoinedMessageFormatter : IDiscordLogFormatter<LogFamilyJoinedMessage>
{
public LogType LogType => LogType.FAMILY_JOINED;
public bool TryFormat(LogFamilyJoinedMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | "
+ $"PLAYER: {message.CharacterName} | INVITER_ID: {message.InviterId} | FAM_ID: {message.FamilyId}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyKickedMessageFormatter : IDiscordLogFormatter<LogFamilyKickedMessage>
{
public LogType LogType => LogType.FAMILY_KICK;
public bool TryFormat(LogFamilyKickedMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | "
+ $"PLAYER: {message.CharacterName} | FAM_ID: {message.FamilyId} | KICKED_NAME: {message.KickedMemberName} | KICKED_ID: {message.KickedMemberId}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyLeftMessageFormatter : IDiscordLogFormatter<LogFamilyLeftMessage>
{
public LogType LogType => LogType.FAMILY_LEFT;
public bool TryFormat(LogFamilyLeftMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | "
+ $"PLAYER: {message.CharacterName} | FAM_ID: {message.FamilyId}";
return true;
}
}
}

View file

@ -0,0 +1,19 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Family;
namespace DiscordNotifier.Consumers.Family
{
public class LogFamilyMessageMessageFormatter : IDiscordLogFormatter<LogFamilyMessageMessage>
{
public LogType LogType => LogType.FAMILY_MESSAGES;
public bool TryFormat(LogFamilyMessageMessage message, out string formattedString)
{
formattedString = $"[{message.CreatedAt:yyyy-MM-dd HH:mm:ss}] [CHANNEL {message.ChannelId}] [FAM_ID: {message.FamilyId}]\n"
+ $"**Player**: {message.CharacterName}\n"
+ $"**MessageType**: {message.FamilyMessageType}\n"
+ $"**Message**: {message.Message}\n";
return true;
}
}
}

View file

@ -0,0 +1,68 @@
// WingsEmu
//
// Developed by NosWings Team
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using DiscordNotifier.Discord;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.InstantBattle;
namespace DiscordNotifier.Consumers.GameEvents
{
public class LogInstantBattleStartDiscordConsumer : IMessageConsumer<InstantBattleStartMessage>
{
private readonly IDiscordWebhookLogsService _discordWebhook;
public LogInstantBattleStartDiscordConsumer(IDiscordWebhookLogsService discordWebhook) => _discordWebhook = discordWebhook;
public async Task HandleAsync(InstantBattleStartMessage notification, CancellationToken token)
{
if (notification.HasNoDelay)
{
EmbedFooterBuilder embedFooterBuilder = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Instant Combat");
var embedAuthorBuilder = new EmbedAuthorBuilder { IconUrl = StaticHardcodedCode.AvatarUrl };
var embedBuilders = new List<EmbedBuilder>
{
new()
{
Author = embedAuthorBuilder,
Title = "[INSTANT-COMBAT] An instant combat has started!",
Description = "May fate be in your favor",
Color = Color.Orange,
Footer = embedFooterBuilder,
// todo ThumbnailUrl = $"https://friends111.nostale.club/list/ip/iconId here.png",
Timestamp = DateTimeOffset.UtcNow
}
};
await _discordWebhook.PublishLogsEmbedded(LogType.PLAYERS_EVENTS_CHANNEL, embedBuilders);
}
else
{
EmbedFooterBuilder embedFooterBuilder = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Instant Combat");
var embedAuthorBuilder = new EmbedAuthorBuilder { IconUrl = StaticHardcodedCode.AvatarUrl };
var embedBuilders = new List<EmbedBuilder>
{
new()
{
Author = embedAuthorBuilder,
Title = "[INSTANT-COMBAT] An instant combat will start in 5 minutes!",
Description = "Ready to fight against waves of monsters...?",
Color = Color.Orange,
Footer = embedFooterBuilder,
// todo ThumbnailUrl = $"https://friends111.nostale.club/list/ip/iconId here.png",
Timestamp = DateTimeOffset.UtcNow
}
};
await _discordWebhook.PublishLogsEmbedded(LogType.PLAYERS_EVENTS_CHANNEL, embedBuilders);
}
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Upgrade;
namespace DiscordNotifier.Consumers.Item
{
public class LogItemGambledMessageFormatter : IDiscordLogFormatter<LogItemGambledMessage>
{
public LogType LogType => LogType.ITEM_GAMBLED;
public bool TryFormat(LogItemGambledMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | PLAYER: {message.CharacterName} | ITEM: {message.ItemVnum} | MODE: {message.Mode} | "
+ $"PROTECTION: {message.Protection}{(message.Amulet != null ? " | AMULET: " + message.Amulet : "")} | SUCCEED: {message.Succeed} | RARITY: {message.OriginalRarity}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Upgrade;
namespace DiscordNotifier.Consumers.Item
{
public class LogItemUpgradedMessageFormatter : IDiscordLogFormatter<LogItemUpgradedMessage>
{
public LogType LogType => LogType.ITEM_UPGRADED;
public bool TryFormat(LogItemUpgradedMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | PLAYER: {message.CharacterName} | ITEM: {message.Item.ItemVNum} | MODE: {message.Mode} | "
+ $"PROTECTION: {message.Protection} | HAS_AMULET: {message.HasAmulet} | ORIGINAL_UPGRADE: {message.OriginalUpgrade} | RESULT: {message.Result}";
return true;
}
}
}

View file

@ -0,0 +1,29 @@
// WingsEmu
//
// Developed by NosWings Team
using System;
using System.Threading;
using System.Threading.Tasks;
using Discord.Webhook;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.Services.Messages;
namespace DiscordNotifier.Consumers.Maintenance
{
public class ServiceDownMessageConsumer : IMessageConsumer<ServiceDownMessage>
{
private static readonly string _webhookUrl = Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_HEALTHCHECK_URL");
public async Task HandleAsync(ServiceDownMessage notification, CancellationToken token)
{
if (string.IsNullOrEmpty(_webhookUrl))
{
return;
}
var client = new DiscordWebhookClient(_webhookUrl);
await client.SendMessageAsync($"```\n[{notification.LastUpdate:yyyy-MM-dd HH:mm:ss}][HEALTHCHECK] {notification.ServiceName} IS OFFLINE!```");
}
}
}

View file

@ -0,0 +1,89 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using DiscordNotifier.Discord;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.Services.Messages;
namespace DiscordNotifier.Consumers.Maintenance
{
public class ServiceMaintenanceNotificationMessageConsumer : IMessageConsumer<ServiceMaintenanceNotificationMessage>
{
private const string ThumbnailUrl = "https://friends111.nostale.club/list/ip/1117.png";
private static readonly EmbedAuthorBuilder Author = new() { IconUrl = StaticHardcodedCode.AvatarUrl };
private static readonly EmbedFooterBuilder DefaultFooter = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Timestamp:");
private static readonly EmbedFooterBuilder FooterForSchedule = new EmbedFooterBuilder().WithIconUrl(StaticHardcodedCode.AvatarUrl).WithText("Maintenance scheduled for:");
private readonly IDiscordWebhookLogsService _discordWebhook;
public ServiceMaintenanceNotificationMessageConsumer(IDiscordWebhookLogsService discordWebhook) => _discordWebhook = discordWebhook;
public async Task HandleAsync(ServiceMaintenanceNotificationMessage notification, CancellationToken token)
{
DateTime scheduledMaintenanceDateTime = DateTime.UtcNow + notification.TimeLeft;
EmbedBuilder embedBuilder = notification.NotificationType switch
{
ServiceMaintenanceNotificationType.Rescheduled => new EmbedBuilder
{
Author = Author,
Title = "[NosWings Maintenance] The maintenance has been re-scheduled!",
Description = GenerateScheduleDescription(scheduledMaintenanceDateTime, notification.Reason),
Color = Color.Gold,
ThumbnailUrl = ThumbnailUrl,
Footer = FooterForSchedule,
Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero)
},
ServiceMaintenanceNotificationType.ScheduleStopped => new EmbedBuilder
{
Author = Author,
Title = "[NosWings Maintenance] Maintenance canceled!",
Description = string.IsNullOrEmpty(notification.Reason) ? string.Empty : $"Reason: {notification.Reason}",
Color = Color.Red,
ThumbnailUrl = ThumbnailUrl,
Footer = DefaultFooter,
Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero)
},
ServiceMaintenanceNotificationType.Executed => new EmbedBuilder
{
Author = Author,
Title = "[NosWings Maintenance] The Server is now in Maintenance mode!",
Color = Color.DarkBlue,
ThumbnailUrl = ThumbnailUrl,
Footer = DefaultFooter,
Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero)
},
ServiceMaintenanceNotificationType.EmergencyExecuted => new EmbedBuilder
{
Author = Author,
Title = "[NosWings Maintenance] An emergency maintenance has been called!",
Description = $"An unexpected maintenance has been executed, sorry for any inconvenience it may have cause.\n**Reason:** {notification.Reason}",
Color = Color.Purple,
ThumbnailUrl = ThumbnailUrl,
Footer = DefaultFooter,
Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero)
},
ServiceMaintenanceNotificationType.Lifted => new EmbedBuilder
{
Author = Author,
Title = "[NosWings Maintenance] The Server's maintenance has been lifted!",
Color = Color.Green,
ThumbnailUrl = ThumbnailUrl,
Footer = DefaultFooter,
Timestamp = new DateTimeOffset(scheduledMaintenanceDateTime, TimeSpan.Zero)
},
_ => null
};
if (embedBuilder == null)
{
return;
}
await _discordWebhook.PublishLogEmbedded(LogType.PLAYERS_EVENTS_CHANNEL, embedBuilder);
}
private static string GenerateScheduleDescription(DateTime scheduledMaintenanceDateTime, string reason) => $"**Date and time:** {scheduledMaintenanceDateTime:U} (UTC)\n**Reason:** {reason}";
}
}

View file

@ -0,0 +1,25 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Miniland;
using WingsEmu.Game.Configurations.Miniland;
namespace DiscordNotifier.Consumers.Minigame
{
public class LogMinigameRewardClaimedMessageFormatter : IDiscordLogFormatter<LogMinigameRewardClaimedMessage>
{
public LogType LogType => LogType.MINIGAME_REWARD_CLAIMED;
public bool TryFormat(LogMinigameRewardClaimedMessage log, out string formattedString)
{
// For now we only want to log on DC the Lv.4 and Lv.5 rewards
if (log.RewardLevel != RewardLevel.FourthReward && log.RewardLevel != RewardLevel.FifthReward)
{
formattedString = string.Empty;
return false;
}
formattedString =
$"{log.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {log.ChannelId} | {log.CharacterName} | MINIGAME {log.MinigameType} | OWNER_ID {log.OwnerId} | Lv.{(short)log.RewardLevel + 1} | ITEM {log.ItemVnum} x{log.Amount}";
return true;
}
}
}

View file

@ -0,0 +1,18 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Miniland;
namespace DiscordNotifier.Consumers.Minigame
{
public class LogMinigameScoreMessageFormatter : IDiscordLogFormatter<LogMinigameScoreMessage>
{
public LogType LogType => LogType.MINIGAME_SCORE;
public bool TryFormat(LogMinigameScoreMessage message, out string formattedString)
{
formattedString =
$"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} | MINIGAME {message.MinigameType} | OWNER_ID {message.OwnerId} | SCORE1 {message.Score1} |"
+ $"SCORE2 {message.Score2} | VALIDITY {message.ScoreValidity} | COMPLETION_TIME {message.CompletionTime.ToString()}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages;
namespace DiscordNotifier.Consumers.Player
{
public class LogGmCommandExecutedMessageFormatter : IDiscordLogFormatter<LogGmCommandExecutedMessage>
{
public LogType LogType => LogType.COMMANDS_GM_COMMAND_EXECUTED;
public bool TryFormat(LogGmCommandExecutedMessage message, out string formattedString)
{
formattedString =
$"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | P.Authority: {message.PlayerAuthority} | C.Authority: [{message.CommandAuthority}] {message.CharacterName} | {message.Command}";
return true;
}
}
}

View file

@ -0,0 +1,16 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.Player;
namespace DiscordNotifier.Consumers.Player
{
public class LogPlayerCommandExecutedMessageFormatter : IDiscordLogFormatter<LogPlayerCommandExecutedMessage>
{
public LogType LogType => LogType.COMMANDS_PLAYER_COMMAND_EXECUTED;
public bool TryFormat(LogPlayerCommandExecutedMessage message, out string formattedString)
{
formattedString = $"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} | {message.Command}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages.LevelUp;
namespace DiscordNotifier.Consumers.Player
{
public class LogPlayerLevelUpMessageFormatter : IDiscordLogFormatter<LogLevelUpCharacterMessage>
{
public LogType LogType => LogType.FARMING_LEVEL_UP;
public bool TryFormat(LogLevelUpCharacterMessage message, out string formattedString)
{
formattedString =
$"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} {message.LevelType.ToUpperInvariant()} {message.Level}";
return true;
}
}
}

View file

@ -0,0 +1,17 @@
using DiscordNotifier.Formatting;
using Plugin.PlayerLogs.Messages;
namespace DiscordNotifier.Consumers.Player
{
public class LogStrangeBehaviorMessageFormatter : IDiscordLogFormatter<LogStrangeBehaviorMessage>
{
public LogType LogType => LogType.STRANGE_BEHAVIORS;
public bool TryFormat(LogStrangeBehaviorMessage message, out string formattedString)
{
formattedString =
$"{message.CreatedAt:yyyy-MM-dd HH:mm:ss} | CHANNEL {message.ChannelId} | {message.CharacterName} | [{message.SeverityType}] -> {message.Message}";
return true;
}
}
}

View file

@ -0,0 +1,8 @@
using System.Collections.Generic;
namespace DiscordNotifier.Discord
{
public class DiscordWebhookConfiguration : Dictionary<LogType, string>
{
}
}

View file

@ -0,0 +1,80 @@
// WingsEmu
//
// Developed by NosWings Team
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
using Discord.Webhook;
using PhoenixLib.Logging;
namespace DiscordNotifier.Discord
{
public class DiscordWebhookLogsService : IDiscordWebhookLogsService
{
private readonly DiscordWebhookConfiguration _configs;
public DiscordWebhookLogsService(DiscordWebhookConfiguration configs) => _configs = configs;
public async Task PublishLogMessage(LogType logType, string message)
{
try
{
if (!_configs.TryGetValue(logType, out string webhookUrl) || string.IsNullOrEmpty(webhookUrl))
{
return;
}
using var webhookClient = new DiscordWebhookClient(webhookUrl);
await webhookClient.SendMessageAsync($"```\n{message}\n```");
}
catch (Exception e)
{
Log.Error($"[LogsServer] PublishLogEmbedded {logType}", e);
}
}
public async Task PublishLogEmbedded(LogType logType, EmbedBuilder embed)
{
try
{
if (!_configs.TryGetValue(logType, out string webhookUrl) || string.IsNullOrEmpty(webhookUrl))
{
return;
}
using var webhookClient = new DiscordWebhookClient(webhookUrl);
await webhookClient.SendMessageAsync(embeds: new[] { embed.Build() });
}
catch (Exception e)
{
Log.Error($"[LogsServer] PublishLogEmbedded {logType}", e);
}
}
public async Task PublishLogsEmbedded(LogType logType, List<EmbedBuilder> embeds)
{
var builtEmbeds = new List<Embed>();
foreach (EmbedBuilder embed in embeds)
{
builtEmbeds.Add(embed.Build());
}
try
{
if (!_configs.TryGetValue(logType, out string webhookUrl) || string.IsNullOrEmpty(webhookUrl))
{
return;
}
using var webhookClient = new DiscordWebhookClient(webhookUrl);
await webhookClient.SendMessageAsync(embeds: builtEmbeds);
}
catch (Exception e)
{
Log.Error($"[LogsServer] PublishLogsEmbedded {logType}", e);
}
}
}
}

View file

@ -0,0 +1,13 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Discord;
namespace DiscordNotifier.Discord
{
public interface IDiscordWebhookLogsService
{
Task PublishLogMessage(LogType logType, string message);
Task PublishLogEmbedded(LogType logType, EmbedBuilder embed);
Task PublishLogsEmbedded(LogType logType, List<EmbedBuilder> embeds);
}
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputPath>..\..\dist\discord-notifier\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PhoenixLib.Caching\PhoenixLib.Caching.csproj" />
<ProjectReference Include="..\PhoenixLib.Configuration\PhoenixLib.Configuration.csproj" />
<ProjectReference Include="..\PhoenixLib.Events\PhoenixLib.Events.csproj" />
<ProjectReference Include="..\PhoenixLib.Logging\PhoenixLib.Logging.csproj" />
<ProjectReference Include="..\PhoenixLib.Messaging\PhoenixLib.Messaging.csproj" />
<ProjectReference Include="..\WingsEmu.Health\WingsEmu.Health.csproj" />
<ProjectReference Include="..\_plugins\Plugin.FamilyImpl\Plugin.FamilyImpl.csproj" />
<ProjectReference Include="..\_plugins\Plugin.PlayerLogs\Plugin.PlayerLogs.csproj" />
<ProjectReference Include="..\_plugins\Plugin.DB.EF\Plugin.DB.EF.csproj" />
<ProjectReference Include="..\_plugins\Plugin.ResourceLoader\Plugin.ResourceLoader.csproj" />
<ProjectReference Include="..\_plugins\WingsEmu.Plugins.DistributedGameEvents\WingsEmu.Plugins.DistributedGameEvents.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Discord.Net.Webhook" Version="2.4.0" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,54 @@
using System;
using System.Runtime.Loader;
using System.Threading;
using PhoenixLib.Logging;
namespace DiscordNotifier
{
public class DockerGracefulStopService : IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ManualResetEventSlim _stoppedEvent;
public DockerGracefulStopService()
{
_cancellationTokenSource = new CancellationTokenSource();
_stoppedEvent = new ManualResetEventSlim();
// SIGINT
Console.CancelKeyPress += (sender, eventArgs) =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
// SIGTERM
AssemblyLoadContext.Default.Unloading += context =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Log.Error("UnhandledException", args.ExceptionObject as Exception);
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
}
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
public void Dispose()
{
_stoppedEvent.Set();
}
private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent)
{
Log.Info("DockerGracefulStopService Stopping service");
cancellationTokenSource.Cancel();
stoppedEvent.Wait();
Log.Info("DockerGracefulStopService Stop finished");
}
}
}

View file

@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection;
using PhoenixLib.ServiceBus.Extensions;
using Plugin.PlayerLogs;
namespace DiscordNotifier.Formatting
{
public static class DiscordLogExtensions
{
public static void AddDiscordFormattedLog<TMessage, TFormatter>(this IServiceCollection services)
where TMessage : class, IPlayerActionLogMessage
where TFormatter : class, IDiscordLogFormatter<TMessage>
{
services.AddMessageSubscriber<TMessage, GenericDiscordLogConsumer<TMessage>>();
services.AddSingleton<IDiscordLogFormatter<TMessage>, TFormatter>();
}
public static void AddDiscordEmbedFormattedLog<TMessage, TFormatter>(this IServiceCollection services)
where TMessage : class, IPlayerActionLogMessage
where TFormatter : class, IDiscordEmbedLogFormatter<TMessage>
{
services.AddMessageSubscriber<TMessage, GenericDiscordEmbedLogConsumer<TMessage>>();
services.AddSingleton<IDiscordEmbedLogFormatter<TMessage>, TFormatter>();
}
}
}

View file

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Discord;
using DiscordNotifier.Discord;
using PhoenixLib.ServiceBus;
using Plugin.PlayerLogs;
namespace DiscordNotifier.Formatting
{
public class GenericDiscordEmbedLogConsumer<T> : IMessageConsumer<T> where T : IPlayerActionLogMessage
{
private readonly IDiscordWebhookLogsService _discordWebhook;
private readonly IDiscordEmbedLogFormatter<T> _formatter;
public GenericDiscordEmbedLogConsumer(IDiscordWebhookLogsService discordWebhook, IDiscordEmbedLogFormatter<T> formatter)
{
_discordWebhook = discordWebhook;
_formatter = formatter;
}
public async Task HandleAsync(T notification, CancellationToken token)
{
if (!_formatter.TryFormat(notification, out List<EmbedBuilder> embeds))
{
return;
}
await _discordWebhook.PublishLogsEmbedded(_formatter.LogType, embeds);
}
}
}

View file

@ -0,0 +1,30 @@
using System.Threading;
using System.Threading.Tasks;
using DiscordNotifier.Discord;
using PhoenixLib.ServiceBus;
using Plugin.PlayerLogs;
namespace DiscordNotifier.Formatting
{
public class GenericDiscordLogConsumer<T> : IMessageConsumer<T> where T : IPlayerActionLogMessage
{
private readonly IDiscordWebhookLogsService _discordWebhook;
private readonly IDiscordLogFormatter<T> _formatter;
public GenericDiscordLogConsumer(IDiscordLogFormatter<T> formatter, IDiscordWebhookLogsService discordWebhook)
{
_formatter = formatter;
_discordWebhook = discordWebhook;
}
public async Task HandleAsync(T notification, CancellationToken token)
{
if (!_formatter.TryFormat(notification, out string formattedString))
{
return;
}
await _discordWebhook.PublishLogMessage(_formatter.LogType, formattedString);
}
}
}

View file

@ -0,0 +1,12 @@
using System.Collections.Generic;
using Discord;
using Plugin.PlayerLogs;
namespace DiscordNotifier.Formatting
{
public interface IDiscordEmbedLogFormatter<TMessage> where TMessage : IPlayerActionLogMessage
{
LogType LogType { get; }
bool TryFormat(TMessage message, out List<EmbedBuilder> embeds);
}
}

View file

@ -0,0 +1,8 @@
namespace DiscordNotifier.Formatting
{
public interface IDiscordLogFormatter<TMessage>
{
LogType LogType { get; }
bool TryFormat(TMessage message, out string formattedString);
}
}

View file

@ -0,0 +1,45 @@
// WingsEmu
//
// Developed by NosWings Team
namespace DiscordNotifier
{
public enum LogType
{
PLAYERS_EVENTS_CHANNEL,
CHAT_GENERAL, // channel, characterName, message
CHAT_WHISPERS, // channel, characterName, message
CHAT_FRIENDS, // channel, characterName, message
CHAT_FAMILIES, // channel, characterName, message
CHAT_SPEAKERS, // channel, characterName, message
CHAT_GROUPS, // channel, characterName, groupId, message
FARMING_LEVEL_UP, // channel, characterName, levelType, level
FAMILY_CREATED, // channel, characterName, familyId, familyName, deputies
FAMILY_DISBANDED, // channel, characterName, familyId
FAMILY_JOINED, // channel, characterName, familyId, inviterId
FAMILY_LEFT, // channel, characterName, familyId
FAMILY_KICK, // channel, characterName, familyId, kickedId, kickedName
FAMILY_MESSAGES, // channel, characterName, familyId, messageType, message
MINIGAME_REWARD_CLAIMED,
MINIGAME_SCORE,
ITEM_GAMBLED,
ITEM_UPGRADED,
GENERAL_ITEM_USAGE, // channel, characterName, itemVnum
GENERAL_EXCHANGE, // channel, characterName, tradeCharacterName, tradeInfos (items, gold...)
EXPLOITS_WRONG_PACKET, // channel, characterName, sent packet
EXPLOITS_TRIED_TO_DUPE, // channel, characterName, sent packet
EXPLOITS_WARNINGS, // channel, characterName, WarningType, warning arguments
STRANGE_BEHAVIORS, // channel, behaviorType, message
COMMANDS_PLAYER_COMMAND_EXECUTED, // channel, characterName
COMMANDS_GM_COMMAND_EXECUTED // channel, characterName, authority
}
}

View file

@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using PhoenixLib.Caching;
using PhoenixLib.Logging;
using WingsAPI.Data.GameData;
using WingsEmu.DTOs.Items;
namespace DiscordNotifier.Managers
{
public class ItemManager
{
private readonly ILongKeyCachedRepository<ItemDTO> _cachedItems;
private readonly IResourceLoader<ItemDTO> _itemDao;
public ItemManager(IResourceLoader<ItemDTO> itemDao, ILongKeyCachedRepository<ItemDTO> cachedItems)
{
_itemDao = itemDao;
_cachedItems = cachedItems;
}
public async Task CacheClientItems()
{
Log.Info("[ITEM_MANAGER] Caching items from DB");
IEnumerable<ItemDTO> items = await _itemDao.LoadAsync();
int count = 0;
foreach (ItemDTO item in items)
{
_cachedItems.Set(item.Id, item);
count++;
}
Log.Info($"[ITEM_MANAGER] Cached: {count.ToString()} items");
}
public int GetItemIconIdByItemId(int itemId)
{
ItemDTO cachedItem = _cachedItems.Get(itemId);
return cachedItem?.IconId ?? default;
}
public ItemDTO GetItemDtoByItemId(int itemId) => _cachedItems.Get(itemId);
}
}

View file

@ -0,0 +1,75 @@
using System;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using DiscordNotifier.Managers;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PhoenixLib.ServiceBus.MQTT;
namespace DiscordNotifier
{
public class Program
{
public static async Task Main(string[] args)
{
PrintHeader();
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
using var stopService = new DockerGracefulStopService();
using IHost host = CreateHostBuilder(args).Build();
{
IServiceProvider services = host.Services;
await host.StartAsync();
IMessagingService messagingService = services.GetRequiredService<IMessagingService>();
await messagingService.StartAsync();
ItemManager itemManager = services.GetRequiredService<ItemManager>();
await itemManager.CacheClientItems();
await host.WaitForShutdownAsync(stopService.CancellationToken);
await messagingService.DisposeAsync();
}
}
private static void PrintHeader()
{
const string text = @"
";
string separator = new('=', Console.WindowWidth);
string logo = text.Split('\n').Select(s => string.Format("{0," + (Console.WindowWidth / 2 + s.Length / 2) + "}\n", s))
.Aggregate("", (current, i) => current + i);
Console.ForegroundColor = ConsoleColor.Cyan;
Console.WriteLine(separator + logo + $"Version: {Assembly.GetExecutingAssembly().GetName().Version}\n" + separator);
Console.ForegroundColor = ConsoleColor.White;
}
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
private static IHostBuilder CreateHostBuilder(string[] args)
{
IHostBuilder host = Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(s => { s.ListenAnyIP(28000); });
webBuilder.UseStartup<Startup>();
});
return host;
}
}
}

View file

@ -0,0 +1,127 @@
using System;
using DiscordNotifier.Consumers.Chat;
using DiscordNotifier.Consumers.Family;
using DiscordNotifier.Consumers.GameEvents;
using DiscordNotifier.Consumers.Item;
using DiscordNotifier.Consumers.Maintenance;
using DiscordNotifier.Consumers.Minigame;
using DiscordNotifier.Consumers.Player;
using DiscordNotifier.Discord;
using DiscordNotifier.Formatting;
using DiscordNotifier.Managers;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using PhoenixLib.Caching;
using PhoenixLib.Configuration;
using PhoenixLib.Events;
using PhoenixLib.Logging;
using PhoenixLib.ServiceBus.Extensions;
using Plugin.Database;
using Plugin.PlayerLogs.Messages;
using Plugin.PlayerLogs.Messages.Family;
using Plugin.PlayerLogs.Messages.LevelUp;
using Plugin.PlayerLogs.Messages.Miniland;
using Plugin.PlayerLogs.Messages.Player;
using Plugin.PlayerLogs.Messages.Upgrade;
using Plugin.ResourceLoader;
using WingsAPI.Communication.InstantBattle;
using WingsAPI.Communication.Services.Messages;
using WingsEmu.Health.Extensions;
namespace DiscordNotifier
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMqttConfigurationFromEnv();
services.AddEventPipeline();
services.AddEventHandlersInAssembly<Startup>();
services.AddMaintenanceMode();
services.AddPhoenixLogging();
services.TryAddSingleton(typeof(ILongKeyCachedRepository<>), typeof(InMemoryCacheRepository<>));
new FileResourceLoaderPlugin().AddDependencies(services);
// discord
services.AddYamlConfigurationHelper();
services.AddSingleton(new DiscordWebhookConfiguration
{
{ LogType.PLAYERS_EVENTS_CHANNEL, Environment.GetEnvironmentVariable("WINGSEMU_DISCORD_WEBHOOK_URL") },
{ LogType.CHAT_FAMILIES, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_FAMILIES") },
{ LogType.CHAT_FRIENDS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_FRIENDS") },
{ LogType.CHAT_GENERAL, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_GENERAL") },
{ LogType.CHAT_GROUPS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_GROUPS") },
{ LogType.CHAT_SPEAKERS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_SPEAKERS") },
{ LogType.CHAT_WHISPERS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_CHAT_WHISPERS") },
{ LogType.FARMING_LEVEL_UP, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FARMING_LEVEL_UP") },
{ LogType.COMMANDS_PLAYER_COMMAND_EXECUTED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_PLAYER_COMMAND_EXECUTED") },
{ LogType.COMMANDS_GM_COMMAND_EXECUTED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_GM_COMMAND_EXECUTED") },
{ LogType.FAMILY_CREATED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_CREATED") },
{ LogType.FAMILY_DISBANDED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_DISBANDED") },
{ LogType.FAMILY_JOINED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_JOINED") },
{ LogType.FAMILY_LEFT, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_LEFT") },
{ LogType.FAMILY_KICK, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_KICK") },
{ LogType.FAMILY_MESSAGES, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_FAMILY_MESSAGES") },
{ LogType.MINIGAME_REWARD_CLAIMED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_MINIGAME_REWARDS_CLAIMED") },
{ LogType.MINIGAME_SCORE, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_MINIGAME_SCORE") },
{ LogType.ITEM_GAMBLED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_ITEM_GAMBLED") },
{ LogType.ITEM_UPGRADED, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_ITEM_UPGRADED") },
{ LogType.STRANGE_BEHAVIORS, Environment.GetEnvironmentVariable("DISCORD_WEBHOOK_STRANGE_BEHAVIORS") }
});
services.AddSingleton<IDiscordWebhookLogsService, DiscordWebhookLogsService>();
services.AddSingleton<ItemManager>();
new DatabasePlugin().AddDependencies(services);
services.AddDiscordFormattedLog<LogPlayerChatMessage, LogChatMessageMessageFormatter>();
services.AddDiscordFormattedLog<LogLevelUpCharacterMessage, LogPlayerLevelUpMessageFormatter>();
services.AddDiscordFormattedLog<LogPlayerCommandExecutedMessage, LogPlayerCommandExecutedMessageFormatter>();
services.AddDiscordFormattedLog<LogGmCommandExecutedMessage, LogGmCommandExecutedMessageFormatter>();
// Family discord
services.AddDiscordFormattedLog<LogFamilyCreatedMessage, LogFamilyCreatedMessageFormatter>();
services.AddDiscordFormattedLog<LogFamilyDisbandedMessage, LogFamilyDisbandedMessageFormatter>();
services.AddDiscordFormattedLog<LogFamilyJoinedMessage, LogFamilyJoinedMessageFormatter>();
services.AddDiscordFormattedLog<LogFamilyKickedMessage, LogFamilyKickedMessageFormatter>();
services.AddDiscordFormattedLog<LogFamilyLeftMessage, LogFamilyLeftMessageFormatter>();
services.AddDiscordFormattedLog<LogFamilyMessageMessage, LogFamilyMessageMessageFormatter>();
services.AddDiscordEmbedFormattedLog<LogFamilyCreatedMessage, LogFamilyCreatedEmbedMessageFormatter>();
// Minigames discord
services.AddDiscordFormattedLog<LogMinigameRewardClaimedMessage, LogMinigameRewardClaimedMessageFormatter>();
services.AddDiscordFormattedLog<LogMinigameScoreMessage, LogMinigameScoreMessageFormatter>();
// Items discord
services.AddDiscordFormattedLog<LogItemGambledMessage, LogItemGambledMessageFormatter>();
services.AddDiscordFormattedLog<LogItemUpgradedMessage, LogItemUpgradedMessageFormatter>();
services.AddDiscordFormattedLog<LogStrangeBehaviorMessage, LogStrangeBehaviorMessageFormatter>();
services.AddMessageSubscriber<InstantBattleStartMessage, LogInstantBattleStartDiscordConsumer>();
// healthcheck
services.AddMessageSubscriber<ServiceDownMessage, ServiceDownMessageConsumer>();
// maintenance
services.AddMessageSubscriber<ServiceMaintenanceNotificationMessage, ServiceMaintenanceNotificationMessageConsumer>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
}
}
}

View file

@ -0,0 +1,7 @@
namespace DiscordNotifier
{
public static class StaticHardcodedCode
{
public const string AvatarUrl = "https://avatars0.githubusercontent.com/u/40839221?s=200";
}
}

View file

@ -0,0 +1,11 @@
using PhoenixLib.Events;
namespace FamilyServer.Achievements
{
public class FamilyAchievementIncrement : IAsyncEvent
{
public long FamilyId { get; set; }
public int AchievementId { get; set; }
public int ValueToAdd { get; set; }
}
}

View file

@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.Events;
namespace FamilyServer.Achievements
{
public class FamilyAchievementIncrementEventHandler : IAsyncEventProcessor<FamilyAchievementIncrement>
{
private readonly FamilyAchievementManager _familyAchievementManager;
public FamilyAchievementIncrementEventHandler(FamilyAchievementManager familyAchievementManager) => _familyAchievementManager = familyAchievementManager;
public Task HandleAsync(FamilyAchievementIncrement e, CancellationToken cancellation)
{
_familyAchievementManager.AddIncrementToQueue(e);
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,25 @@
// WingsEmu
//
// Developed by NosWings Team
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Achievements;
namespace FamilyServer.Achievements
{
public class FamilyAchievementIncrementMessageConsumer : IMessageConsumer<FamilyAchievementIncrementMessage>
{
private readonly FamilyAchievementManager _familyAchievementManager;
public FamilyAchievementIncrementMessageConsumer(FamilyAchievementManager familyAchievementManager) => _familyAchievementManager = familyAchievementManager;
public Task HandleAsync(FamilyAchievementIncrementMessage notification, CancellationToken token)
{
var incrementRequest = new FamilyAchievementIncrement { AchievementId = notification.AchievementId, FamilyId = notification.FamilyId, ValueToAdd = notification.ValueToAdd };
_familyAchievementManager.AddIncrementToQueue(incrementRequest);
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,361 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Logs;
using FamilyServer.Managers;
using Microsoft.Extensions.Hosting;
using PhoenixLib.DAL.Redis.Locks;
using PhoenixLib.Logging;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Achievements;
using Plugin.FamilyImpl.Messages;
using WingsAPI.Communication.Families;
using WingsAPI.Data.Families;
using WingsEmu.Packets.Enums.Families;
namespace FamilyServer.Achievements
{
public class FamilyAchievementManager : BackgroundService
{
private readonly Dictionary<int, FamilyAchievementSpecificConfiguration> _achievementsConfiguration;
private readonly ConcurrentQueue<FamilyAchievementIncrement> _achievementsQueue = new();
private readonly FamilyAchievementsConfiguration _configuration;
private readonly IExpirableLockService _expirableLockService;
private readonly FamilyExperienceManager _familyExperienceManager;
private readonly FamilyLogManager _familyLogManager;
private readonly FamilyManager _familyManager;
private readonly IFamilyService _familyService;
private readonly IMessagePublisher<FamilyUpdateMessage> _familyUpdatePublisher;
private readonly Dictionary<int, FamilyMissionSpecificConfiguration> _missionSpecificConfigurations;
private readonly ConcurrentQueue<FamilyMissionIncrement> _missionsQueue = new();
private readonly IMessagePublisher<FamilyAchievementUnlockedMessage> _unlockedMessagePublisher;
public FamilyAchievementManager(FamilyManager familyManager, FamilyAchievementsConfiguration configuration, IMessagePublisher<FamilyUpdateMessage> familyUpdatePublisher,
IMessagePublisher<FamilyAchievementUnlockedMessage> unlockedMessagePublisher, IFamilyService familyService, FamilyMissionsConfiguration missionsConfiguration,
IExpirableLockService expirableLockService, FamilyExperienceManager familyExperienceManager, FamilyLogManager familyLogManager)
{
_familyManager = familyManager;
_configuration = configuration;
_familyUpdatePublisher = familyUpdatePublisher;
_unlockedMessagePublisher = unlockedMessagePublisher;
_familyService = familyService;
_expirableLockService = expirableLockService;
_familyExperienceManager = familyExperienceManager;
_familyLogManager = familyLogManager;
_missionSpecificConfigurations = missionsConfiguration.ToDictionary(s => s.MissionId);
_achievementsConfiguration = configuration.Counters.ToDictionary(s => s.Id);
}
private static TimeSpan RefreshTime => TimeSpan.FromSeconds(Convert.ToInt32(Environment.GetEnvironmentVariable("FAMILY_ACHIEVEMENT_REFRESH_IN_SECONDS") ?? "5"));
public void AddIncrementToQueue(FamilyAchievementIncrement message)
{
_achievementsQueue.Enqueue(message);
}
public void AddIncrementToQueue(FamilyMissionIncrement message)
{
_missionsQueue.Enqueue(message);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
Dictionary<long, FamilyDTO> toUpdate = new();
await FlushPendingAchievements(toUpdate);
await FlushPendingMissions(toUpdate);
if (toUpdate.Count > 0)
{
foreach (FamilyDTO family in toUpdate.Values)
{
await _familyManager.SaveFamilyAsync(family);
}
await _familyUpdatePublisher.PublishAsync(new FamilyUpdateMessage
{
Families = toUpdate.Values.ToList(),
ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.AchievementsAndMissions
});
}
await Task.Delay(RefreshTime, stoppingToken);
}
}
private async Task FlushPendingMissions(Dictionary<long, FamilyDTO> toUpdate)
{
try
{
if (_missionsQueue.IsEmpty)
{
return;
}
HashSet<(long, int)> unlockedMissions = new();
while (_missionsQueue.TryDequeue(out FamilyMissionIncrement message))
{
FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(message.FamilyId);
if (family == null)
{
Log.Debug("Family not found");
continue;
}
int missionId = message.MissionId;
int value = message.ValueToAdd;
long? characterId = message.CharacterId;
Log.Debug($"[FAMILY_MISSIONS] Family {family.Id} adding {value} to mission: {missionId}");
family.Missions ??= new FamilyMissionsDto();
family.Missions.Missions ??= new Dictionary<int, FamilyMissionDto>();
if (!family.Missions.Missions.TryGetValue(missionId, out FamilyMissionDto progress))
{
family.Missions.Missions[missionId] = progress = new FamilyMissionDto
{
Id = missionId
};
}
if (progress.CompletionDate.HasValue && progress.CompletionDate < DateTime.UtcNow)
{
Log.Debug($"[FAMILY_MISSIONS] {progress.CompletionDate.Value} need to wait reset");
continue;
}
if (!_missionSpecificConfigurations.TryGetValue(missionId, out FamilyMissionSpecificConfiguration config))
{
Log.Error($"Family achievement config {missionId} is not configured", new Exception());
continue;
}
if (config.OncePerPlayerPerDay && !characterId.HasValue)
{
Log.Debug($"[FAMILY_MISSION] could not increment mission: {missionId} for family: {family.Id} because characterId is missing");
continue;
}
if (config.OncePerPlayerPerDay &&
!await _expirableLockService.TryAddTemporaryLockAsync($"game:locks:family:{family.Id}:missions:{missionId}:character:{characterId}", DateTime.UtcNow.Date.AddDays(1)))
{
Log.Debug($"[FAMILY_MISSION] {characterId} could not increment mission: {missionId} for family: {family.Id}, already done for today");
continue;
}
progress.Count += value;
toUpdate.TryAdd(family.Id, family);
if (progress.Count < config.Value)
{
Log.Debug($"[FAMILY_MISSIONS] Family {family.Id} tried to increment mission: {missionId} progress.Count < config.Value");
continue;
}
// mission done
DateTime now = DateTime.UtcNow;
progress.CompletionDate = now;
progress.CompletionCount++;
unlockedMissions.Add((family.Id, missionId));
if (config.Rewards != null)
{
foreach (FamilyMissionReward reward in config.Rewards)
{
if (reward.FamilyXp.HasValue)
{
_familyExperienceManager.AddExperienceIncrementRequest(new ExperienceIncrementRequest
{
FamilyId = family.Id,
Experience = reward.FamilyXp.Value
});
}
}
}
if (progress.Count > config.Value)
{
progress.Count = config.Value;
continue;
}
Log.Debug($"[FAMILY_MISSIONS] Family {family.Id} tried to increment mission: {missionId} progress.Count <= config.Value");
}
foreach ((long familyId, int missionId) in unlockedMissions)
{
_familyLogManager.SaveFamilyLogs(new[]
{
new FamilyLogDto
{
FamilyLogType = FamilyLogType.FamilyMission,
FamilyId = familyId,
Actor = missionId.ToString(),
Timestamp = DateTime.UtcNow
}
});
}
}
catch (Exception e)
{
Log.Error("[FAMILY_ACHIEVEMENT_MANAGER]", e);
}
}
private async Task FlushPendingAchievements(Dictionary<long, FamilyDTO> toUpdate)
{
try
{
if (_achievementsQueue.IsEmpty)
{
return;
}
HashSet<(long, int)> unlockedAchievements = new();
while (_achievementsQueue.TryDequeue(out FamilyAchievementIncrement message))
{
FamilyDTO family = await _familyManager.GetFamilyByFamilyIdAsync(message.FamilyId);
if (family == null)
{
Log.Debug("Family not found");
continue;
}
int achievementId = message.AchievementId;
int value = message.ValueToAdd;
Log.Debug($"[FAMILY_ACHIEVEMENT] Family {family.Id} adding {value} to achievement: {achievementId}");
family.Achievements ??= new FamilyAchievementsDto();
family.Achievements.Achievements ??= new Dictionary<int, FamilyAchievementCompletionDto>();
family.Achievements.Progress ??= new Dictionary<int, FamilyAchievementProgressDto>();
if (family.Achievements?.Achievements?.ContainsKey(achievementId) == true)
{
AddNextAchievement(achievementId, family, value);
continue;
}
if (!family.Achievements.Progress.TryGetValue(achievementId, out FamilyAchievementProgressDto progress))
{
family.Achievements.Progress[achievementId] = progress = new FamilyAchievementProgressDto
{
Id = achievementId
};
}
if (!_achievementsConfiguration.TryGetValue(achievementId, out FamilyAchievementSpecificConfiguration config))
{
Log.Error($"Family achievement config {achievementId} is not configured", new Exception($"[FAMILY_ACHIEVEMENT_CONFIG] {progress.Id} not configured"));
continue;
}
if (config.RequiredId.HasValue && !family.Achievements.Achievements.ContainsKey(config.RequiredId.Value))
{
Log.Debug($"[FAMILY_ACHIEVEMENT] Family {family.Id} tried to increment achievement: {achievementId} but didn't have {config.RequiredId.Value}");
continue;
}
progress.Count += value;
toUpdate.TryAdd(family.Id, family);
if (progress.Count < config.Value)
{
Log.Debug($"[FAMILY_ACHIEVEMENT] Family {family.Id} tried to increment achievement: {achievementId} progress.Count < config.Value");
continue;
}
// achievement unlocked
family.Achievements.Achievements[achievementId] = new FamilyAchievementCompletionDto
{
Id = progress.Id,
CompletionDate = DateTime.UtcNow
};
(long Id, int achievementId) key = (family.Id, achievementId);
if (!unlockedAchievements.Contains(key))
{
unlockedAchievements.Add(key);
}
if (config.Rewards != null)
{
foreach (FamilyAchievementReward reward in config.Rewards)
{
if (reward.FamilyXp.HasValue)
{
_familyExperienceManager.AddExperienceIncrementRequest(new ExperienceIncrementRequest
{
FamilyId = family.Id,
Experience = reward.FamilyXp.Value
});
}
if (reward.FamilyUpgradeCategory.HasValue && reward.UpgradeValue.HasValue && reward.UpgradeId.HasValue)
{
await _familyService.TryAddFamilyUpgrade(new FamilyUpgradeRequest
{
FamilyId = family.Id,
FamilyUpgradeType = reward.FamilyUpgradeCategory.Value,
UpgradeId = reward.UpgradeId.Value,
Value = reward.UpgradeValue.Value
});
}
}
}
family.Achievements.Progress.Remove(achievementId);
int progressCount = achievementId is >= 9018 and <= 9036 ? 0 : progress.Count;
AddNextAchievement(achievementId, family, progressCount);
}
foreach ((long familyId, int achievementId) in unlockedAchievements)
{
await _unlockedMessagePublisher.PublishAsync(new FamilyAchievementUnlockedMessage
{
FamilyId = familyId,
AchievementId = achievementId
});
_familyLogManager.SaveFamilyLogs(new[]
{
new FamilyLogDto
{
FamilyLogType = FamilyLogType.FamilyAchievement,
FamilyId = familyId,
Actor = achievementId.ToString(),
Timestamp = DateTime.UtcNow
}
});
}
}
catch (Exception e)
{
Log.Error("[FAMILY_ACHIEVEMENT_MANAGER]", e);
}
}
private void AddNextAchievement(int achievementId, FamilyDTO family, int value)
{
FamilyAchievementSpecificConfiguration tmp = _configuration.Counters.FirstOrDefault(s => s.RequiredId == achievementId);
if (tmp == null)
{
Log.Error($"[FAMILY_ACHIEVEMENT_MANAGER] Couldn't find next achievement for {achievementId} - familyId: {family.Id}", new Exception());
return;
}
_achievementsQueue.Enqueue(new FamilyAchievementIncrement
{
FamilyId = family.Id,
AchievementId = tmp.Id,
ValueToAdd = value
});
}
}
}

View file

@ -0,0 +1,10 @@
namespace FamilyServer.Achievements
{
public class FamilyMissionIncrement
{
public long FamilyId { get; set; }
public long? CharacterId { get; set; }
public int MissionId { get; set; }
public int ValueToAdd { get; set; }
}
}

View file

@ -0,0 +1,27 @@
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Achievements;
namespace FamilyServer.Achievements
{
public class FamilyMissionIncrementMessageConsumer : IMessageConsumer<FamilyMissionIncrementMessage>
{
private readonly FamilyAchievementManager _familyAchievementManager;
public FamilyMissionIncrementMessageConsumer(FamilyAchievementManager familyAchievementManager) => _familyAchievementManager = familyAchievementManager;
public Task HandleAsync(FamilyMissionIncrementMessage notification, CancellationToken token)
{
var incrementRequest = new FamilyMissionIncrement
{
MissionId = notification.MissionId,
CharacterId = notification.CharacterId,
FamilyId = notification.FamilyId,
ValueToAdd = notification.ValueToAdd
};
_familyAchievementManager.AddIncrementToQueue(incrementRequest);
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents;
namespace FamilyServer.Consumers
{
public class FamilyCharacterConnectMessageConsumer : IMessageConsumer<PlayerConnectedOnChannelMessage>
{
private readonly IMessagePublisher<FamilyCharacterJoinMessage> _messagePublisher;
public FamilyCharacterConnectMessageConsumer(IMessagePublisher<FamilyCharacterJoinMessage> messagePublisher) => _messagePublisher = messagePublisher;
public async Task HandleAsync(PlayerConnectedOnChannelMessage notification, CancellationToken token)
{
await _messagePublisher.PublishAsync(new FamilyCharacterJoinMessage
{
CharacterId = notification.CharacterId,
FamilyId = notification.FamilyId
});
}
}
}

View file

@ -0,0 +1,41 @@
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
using WingsAPI.Communication.Families;
using WingsEmu.Plugins.DistributedGameEvents.PlayerEvents;
namespace FamilyServer.Consumers
{
public class FamilyCharacterDisconnectMessageConsumer : IMessageConsumer<PlayerDisconnectedChannelMessage>
{
private readonly IFamilyService _familyService;
private readonly IMessagePublisher<FamilyCharacterLeaveMessage> _messagePublisher;
public FamilyCharacterDisconnectMessageConsumer(IMessagePublisher<FamilyCharacterLeaveMessage> messagePublisher, IFamilyService familyService)
{
_messagePublisher = messagePublisher;
_familyService = familyService;
}
public async Task HandleAsync(PlayerDisconnectedChannelMessage e, CancellationToken cancellation)
{
if (!e.FamilyId.HasValue)
{
return;
}
await _familyService.MemberDisconnectedAsync(new FamilyMemberDisconnectedRequest
{
CharacterId = e.CharacterId,
DisconnectionTime = e.DisconnectionTime
});
;
await _messagePublisher.PublishAsync(new FamilyCharacterLeaveMessage
{
CharacterId = e.CharacterId,
FamilyId = e.FamilyId.Value
}, cancellation);
}
}
}

View file

@ -0,0 +1,48 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Managers;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
using WingsEmu.Game.Families;
namespace FamilyServer.Consumers
{
public class FamilyDeclareExperienceGainedMessageConsumer : IMessageConsumer<FamilyDeclareExperienceGainedMessage>
{
private readonly FamilyExperienceManager _familyExperienceManager;
public FamilyDeclareExperienceGainedMessageConsumer(FamilyExperienceManager familyExperienceManager) => _familyExperienceManager = familyExperienceManager;
public async Task HandleAsync(FamilyDeclareExperienceGainedMessage e, CancellationToken cancellation)
{
Dictionary<long, long> dictionary = FuseDifferentXpValues(e.Experiences);
foreach ((long characterId, long experienceToAdd) in dictionary)
{
_familyExperienceManager.AddExperienceIncrementRequest(new ExperienceIncrementRequest
{
CharacterId = characterId,
Experience = experienceToAdd
});
}
}
private static Dictionary<long, long> FuseDifferentXpValues(IEnumerable<ExperienceGainedSubMessage> experienceGainedSubMessages)
{
var dictionary = new Dictionary<long, long>();
foreach (ExperienceGainedSubMessage exp in experienceGainedSubMessages)
{
if (dictionary.ContainsKey(exp.CharacterId))
{
dictionary[exp.CharacterId] += exp.FamXpGained;
continue;
}
dictionary.Add(exp.CharacterId, exp.FamXpGained);
}
return dictionary;
}
}
}

View file

@ -0,0 +1,25 @@
// WingsEmu
//
// Developed by NosWings Team
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Logs;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
namespace FamilyServer.Consumers
{
public class FamilyDeclareLogsMessageConsumer : IMessageConsumer<FamilyDeclareLogsMessage>
{
private readonly FamilyLogManager _familyLogManager;
public FamilyDeclareLogsMessageConsumer(FamilyLogManager familyLogManager) => _familyLogManager = familyLogManager;
public Task HandleAsync(FamilyDeclareLogsMessage e, CancellationToken cancellation)
{
_familyLogManager.SaveFamilyLogs(e.Logs);
return Task.CompletedTask;
}
}
}

View file

@ -0,0 +1,48 @@
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Managers;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
using WingsAPI.Data.Families;
using WingsEmu.Packets.Enums.Character;
namespace FamilyServer.Consumers
{
public class FamilyHeadSexMessageConsumer : IMessageConsumer<FamilyHeadSexMessage>
{
private readonly FamilyManager _familyManager;
private readonly IMessagePublisher<FamilyUpdateMessage> _messagePublisher;
public FamilyHeadSexMessageConsumer(FamilyManager familyManager, IMessagePublisher<FamilyUpdateMessage> messagePublisher)
{
_familyManager = familyManager;
_messagePublisher = messagePublisher;
}
public async Task HandleAsync(FamilyHeadSexMessage notification, CancellationToken token)
{
long familyId = notification.FamilyId;
GenderType genderType = notification.NewGenderType;
FamilyDTO familyDto = await _familyManager.GetFamilyByFamilyIdAsync(familyId);
if (familyDto == null)
{
return;
}
if (familyDto.HeadGender == genderType)
{
return;
}
familyDto.HeadGender = genderType;
await _familyManager.SaveFamilyAsync(familyDto);
await _messagePublisher.PublishAsync(new FamilyUpdateMessage
{
ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.HeadSex,
Families = new[] { familyDto }
});
}
}
}

View file

@ -0,0 +1,43 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Managers;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
using WingsAPI.Data.Families;
namespace FamilyServer.Consumers
{
public class FamilyMemberTodayMessageConsumer : IMessageConsumer<FamilyMemberTodayMessage>
{
private readonly FamilyMembershipManager _familyMembershipManager;
private readonly IMessagePublisher<FamilyMemberUpdateMessage> _messagePublisher;
public FamilyMemberTodayMessageConsumer(FamilyMembershipManager familyMembershipManager, IMessagePublisher<FamilyMemberUpdateMessage> messagePublisher)
{
_familyMembershipManager = familyMembershipManager;
_messagePublisher = messagePublisher;
}
public async Task HandleAsync(FamilyMemberTodayMessage notification, CancellationToken token)
{
FamilyMembershipDto familyMember = await _familyMembershipManager.GetFamilyMembershipByCharacterIdAsync(notification.CharacterId);
if (familyMember == null)
{
return;
}
familyMember.DailyMessage = notification.Message;
await _familyMembershipManager.SaveFamilyMembershipAsync(familyMember);
await _messagePublisher.PublishAsync(new FamilyMemberUpdateMessage
{
ChangedInfoMemberUpdate = ChangedInfoMemberUpdate.DailyMessage,
UpdatedMembers = new List<FamilyMembershipDto>
{
familyMember
}
});
}
}
}

View file

@ -0,0 +1,20 @@
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Services;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.Families;
namespace FamilyServer.Consumers
{
public class FamilyMissionsResetMessageConsumer : IMessageConsumer<FamilyMissionsResetMessage>
{
private readonly FamilyService _familyService;
public FamilyMissionsResetMessageConsumer(FamilyService familyService) => _familyService = familyService;
public async Task HandleAsync(FamilyMissionsResetMessage notification, CancellationToken token)
{
await _familyService.ResetFamilyMissions();
}
}
}

View file

@ -0,0 +1,42 @@
using System.Threading;
using System.Threading.Tasks;
using FamilyServer.Managers;
using PhoenixLib.ServiceBus;
using Plugin.FamilyImpl.Messages;
using WingsAPI.Data.Families;
namespace FamilyServer.Consumers
{
public class FamilyNoticeMessageConsumer : IMessageConsumer<FamilyNoticeMessage>
{
private readonly FamilyManager _familyManager;
private readonly IMessagePublisher<FamilyUpdateMessage> _messagePublisher;
public FamilyNoticeMessageConsumer(FamilyManager familyManager, IMessagePublisher<FamilyUpdateMessage> messagePublisher)
{
_familyManager = familyManager;
_messagePublisher = messagePublisher;
}
public async Task HandleAsync(FamilyNoticeMessage notification, CancellationToken token)
{
long familyId = notification.FamilyId;
string message = notification.Message;
FamilyDTO familyDto = await _familyManager.GetFamilyByFamilyIdAsync(familyId);
if (familyDto == null)
{
return;
}
familyDto.Message = message;
await _familyManager.SaveFamilyAsync(familyDto);
await _messagePublisher.PublishAsync(new FamilyUpdateMessage
{
ChangedInfoFamilyUpdate = ChangedInfoFamilyUpdate.Notice,
Families = new[] { familyDto }
});
}
}
}

View file

@ -0,0 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using PhoenixLib.ServiceBus;
using WingsAPI.Communication.Services.Messages;
namespace FamilyServer.Consumers
{
public class ServiceFlushAllMessageConsumer : IMessageConsumer<ServiceFlushAllMessage>
{
private readonly FamilySystem _familySystem;
public ServiceFlushAllMessageConsumer(FamilySystem familySystem) => _familySystem = familySystem;
public async Task HandleAsync(ServiceFlushAllMessage notification, CancellationToken token)
{
await _familySystem.ProcessMain();
}
}
}

View file

@ -0,0 +1,54 @@
using System;
using System.Runtime.Loader;
using System.Threading;
using PhoenixLib.Logging;
namespace FamilyServer
{
public class DockerGracefulStopService : IDisposable
{
private readonly CancellationTokenSource _cancellationTokenSource;
private readonly ManualResetEventSlim _stoppedEvent;
public DockerGracefulStopService()
{
_cancellationTokenSource = new CancellationTokenSource();
_stoppedEvent = new ManualResetEventSlim();
// SIGINT
Console.CancelKeyPress += (sender, eventArgs) =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGINT received", new Exception("[GRACEFUL_SHUTDOWN] SIGINT received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
// SIGTERM
AssemblyLoadContext.Default.Unloading += context =>
{
Log.Error("[GRACEFUL_SHUTDOWN] SIGTERM received", new Exception("[GRACEFUL_SHUTDOWN] SIGTERM received"));
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Log.Error("UnhandledException", args.ExceptionObject as Exception);
GracefulStop(_cancellationTokenSource, _stoppedEvent);
};
}
public CancellationToken CancellationToken => _cancellationTokenSource.Token;
public void Dispose()
{
_stoppedEvent.Set();
}
private static void GracefulStop(CancellationTokenSource cancellationTokenSource, ManualResetEventSlim stoppedEvent)
{
Log.Info("DockerGracefulStopService Stopping service");
cancellationTokenSource.Cancel();
stoppedEvent.Wait();
Log.Info("DockerGracefulStopService Stop finished");
}
}
}

View file

@ -0,0 +1,7 @@
namespace FamilyServer
{
public static class EnvironmentConsts
{
public const string FamilyServerSaveIntervalMinutes = "FAMILY_SERVER_SAVE_INTERVAL_MINUTES";
}
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<OutputPath>..\..\dist\family-server\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.38.0" />
<PackageReference Include="protobuf-net.Grpc.AspNetCore" Version="1.0.152" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PhoenixLib.Caching\PhoenixLib.Caching.csproj" />
<ProjectReference Include="..\PhoenixLib.Configuration\PhoenixLib.Configuration.csproj" />
<ProjectReference Include="..\PhoenixLib.DAL.Redis\PhoenixLib.DAL.Redis.csproj" />
<ProjectReference Include="..\PhoenixLib.Events\PhoenixLib.Events.csproj" />
<ProjectReference Include="..\PhoenixLib.Logging\PhoenixLib.Logging.csproj" />
<ProjectReference Include="..\PhoenixLib.Messaging\PhoenixLib.Messaging.csproj" />
<ProjectReference Include="..\WingsEmu.Communication.gRPC\WingsEmu.Communication.gRPC.csproj" />
<ProjectReference Include="..\WingsEmu.Health\WingsEmu.Health.csproj" />
<ProjectReference Include="..\_plugins\Plugin.FamilyImpl\Plugin.FamilyImpl.csproj" />
<ProjectReference Include="..\_plugins\Plugin.DB.EF\Plugin.DB.EF.csproj" />
<ProjectReference Include="..\_plugins\WingsEmu.Plugins.DistributedGameEvents\WingsEmu.Plugins.DistributedGameEvents.csproj" />
</ItemGroup>
</Project>

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