A => .gitignore +679 -0
@@ 1,679 @@
+
+# Created by https://www.toptal.com/developers/gitignore/api/csharp,c++,visualstudio,rider
+# Edit at https://www.toptal.com/developers/gitignore?templates=csharp,c++,visualstudio,rider
+
+### C++ ###
+# Prerequisites
+*.d
+
+# Compiled Object files
+*.slo
+*.lo
+*.o
+*.obj
+
+# Precompiled Headers
+*.gch
+*.pch
+
+# Compiled Dynamic libraries
+*.so
+*.dylib
+*.dll
+
+# Fortran module files
+*.mod
+*.smod
+
+# Compiled Static libraries
+*.lai
+*.la
+*.a
+*.lib
+
+# Executables
+*.exe
+*.out
+*.app
+
+### Csharp ###
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.iobj
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.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
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# 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
+# Note: 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
+# NuGet Symbol Packages
+*.snupkg
+# 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
+*.nuget.props
+*.nuget.targets
+
+# Nuget personal access tokens and Credentials
+# nuget.config
+
+# 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
+*.appx
+*.appxbundle
+*.appxupload
+
+# 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
+orleans.codegen.cs
+
+# 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)
+#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
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# 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/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# 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/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+### Rider ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# AWS User-specific
+.idea/**/aws.xml
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### 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
+
+# ASP.NET Scaffolding
+
+# 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
+
+# TeamCity is a build add-in
+
+# DotCover is a Code Coverage Tool
+
+# AxoCover is a Code Coverage Tool
+
+# Coverlet is a free, cross platform 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
+# NuGet Symbol 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
+
+# Nuget personal access tokens and Credentials
+# nuget.config
+
+# 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
+
+# Ionide (cross platform F# VS Code tools) working folder
+
+# Fody - auto-generated XML schema
+
+# VS Code files for those working on multiple tools
+
+# Local History for Visual Studio Code
+
+# Windows Installer files from build outputs
+
+# JetBrains Rider
+
+### VisualStudio Patch ###
+# Additional files built by Visual Studio
+
+# End of https://www.toptal.com/developers/gitignore/api/csharp,c++,visualstudio,rider
A => Directory.Build.props +22 -0
@@ 1,22 @@
+<Project>
+ <PropertyGroup>
+ <StyleCopRuleset>$(MSBuildThisFileDirectory)stylecop.ruleset</StyleCopRuleset>
+ <StyleCopConfiguration>$(MSBuildThisFileDirectory)stylecop.json</StyleCopConfiguration>
+
+ <Nullable>enable</Nullable>
+ <WarningsAsErrors>nullable</WarningsAsErrors>
+
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+
+ <CodeAnalysisRuleSet>$(StyleCopRuleset)</CodeAnalysisRuleSet>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.261">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+
+ <AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" Link="stylecop.json"/>
+ </ItemGroup>
+</Project>
A => LICENSE +21 -0
@@ 1,21 @@
+MIT License
+
+Copyright (c) 2021 František Boháček
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
A => NosSmooth.Local.sln +98 -0
@@ 1,98 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{004AF2D8-8D02-4BDF-BD18-752C54D65F1C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{1D0E2BB9-4DDD-4461-A7F4-1B7946FD01CC}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{987E4CD9-DBFE-40FF-90F7-AAD060CFC781}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{B5ED7EE8-22B3-4119-9F6C-F798C59ADD98}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Inject", "Inject", "{2D16F101-E8AB-4121-9C61-55249EAA2AB2}"
+EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "NosSmooth.Inject", "src\Inject\NosSmooth.Inject\NosSmooth.Inject.vcxproj", "{CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Injector", "src\Inject\NosSmooth.Injector\NosSmooth.Injector.csproj", "{CAD71BA8-1AA6-4063-AEA7-6EEC36E1F3CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Injector.CLI", "src\Inject\NosSmooth.Injector.CLI\NosSmooth.Injector.CLI.csproj", "{49E3C82D-030F-4AB2-B967-FF9B5969E5CE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.ChatCommands", "src\Extensions\NosSmooth.ChatCommands\NosSmooth.ChatCommands.csproj", "{5595FD94-F361-4B19-832F-963122099683}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.LocalBinding", "src\Core\NosSmooth.LocalBinding\NosSmooth.LocalBinding.csproj", "{F3AF1412-596A-4A98-8FEF-AE238177ED5E}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.LocalClient", "src\Core\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj", "{DC16F0E2-0905-4FA3-9068-966FDE740F94}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LowLevel", "LowLevel", "{3FEDC05A-980C-4C96-923D-B2CD7D96CD7B}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{D19F45EC-8E59-4F7C-A4C4-D0BD11C8C32B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterceptNameChanger", "src\Samples\LowLevel\InterceptNameChanger\InterceptNameChanger.csproj", "{BC5A89B8-1191-4BC5-BBF0-B675DF442D5F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalkCommands", "src\Samples\LowLevel\WalkCommands\WalkCommands.csproj", "{CADFC50F-8CEC-4254-99FF-E766DA0B9194}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleChat", "src\Samples\LowLevel\SimpleChat\SimpleChat.csproj", "{CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExternalBrowser", "src\Samples\External\ExternalBrowser\ExternalBrowser.csproj", "{7190312B-CFEE-49D3-8DAB-542C31071E87}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {CA2873D8-BD0B-4583-818D-B94A3C2ABBA3} = {2D16F101-E8AB-4121-9C61-55249EAA2AB2}
+ {CAD71BA8-1AA6-4063-AEA7-6EEC36E1F3CC} = {2D16F101-E8AB-4121-9C61-55249EAA2AB2}
+ {49E3C82D-030F-4AB2-B967-FF9B5969E5CE} = {2D16F101-E8AB-4121-9C61-55249EAA2AB2}
+ {5595FD94-F361-4B19-832F-963122099683} = {987E4CD9-DBFE-40FF-90F7-AAD060CFC781}
+ {F3AF1412-596A-4A98-8FEF-AE238177ED5E} = {B5ED7EE8-22B3-4119-9F6C-F798C59ADD98}
+ {DC16F0E2-0905-4FA3-9068-966FDE740F94} = {B5ED7EE8-22B3-4119-9F6C-F798C59ADD98}
+ {3FEDC05A-980C-4C96-923D-B2CD7D96CD7B} = {004AF2D8-8D02-4BDF-BD18-752C54D65F1C}
+ {D19F45EC-8E59-4F7C-A4C4-D0BD11C8C32B} = {004AF2D8-8D02-4BDF-BD18-752C54D65F1C}
+ {BC5A89B8-1191-4BC5-BBF0-B675DF442D5F} = {3FEDC05A-980C-4C96-923D-B2CD7D96CD7B}
+ {CADFC50F-8CEC-4254-99FF-E766DA0B9194} = {3FEDC05A-980C-4C96-923D-B2CD7D96CD7B}
+ {CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4} = {3FEDC05A-980C-4C96-923D-B2CD7D96CD7B}
+ {7190312B-CFEE-49D3-8DAB-542C31071E87} = {D19F45EC-8E59-4F7C-A4C4-D0BD11C8C32B}
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|Any CPU.ActiveCfg = Debug|Win32
+ {CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Debug|Any CPU.Build.0 = Debug|Win32
+ {CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|Any CPU.ActiveCfg = Release|Win32
+ {CA2873D8-BD0B-4583-818D-B94A3C2ABBA3}.Release|Any CPU.Build.0 = Release|Win32
+ {CAD71BA8-1AA6-4063-AEA7-6EEC36E1F3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CAD71BA8-1AA6-4063-AEA7-6EEC36E1F3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CAD71BA8-1AA6-4063-AEA7-6EEC36E1F3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CAD71BA8-1AA6-4063-AEA7-6EEC36E1F3CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {49E3C82D-030F-4AB2-B967-FF9B5969E5CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {49E3C82D-030F-4AB2-B967-FF9B5969E5CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {49E3C82D-030F-4AB2-B967-FF9B5969E5CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {49E3C82D-030F-4AB2-B967-FF9B5969E5CE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5595FD94-F361-4B19-832F-963122099683}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5595FD94-F361-4B19-832F-963122099683}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5595FD94-F361-4B19-832F-963122099683}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5595FD94-F361-4B19-832F-963122099683}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F3AF1412-596A-4A98-8FEF-AE238177ED5E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F3AF1412-596A-4A98-8FEF-AE238177ED5E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F3AF1412-596A-4A98-8FEF-AE238177ED5E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F3AF1412-596A-4A98-8FEF-AE238177ED5E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DC16F0E2-0905-4FA3-9068-966FDE740F94}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DC16F0E2-0905-4FA3-9068-966FDE740F94}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DC16F0E2-0905-4FA3-9068-966FDE740F94}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DC16F0E2-0905-4FA3-9068-966FDE740F94}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC5A89B8-1191-4BC5-BBF0-B675DF442D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC5A89B8-1191-4BC5-BBF0-B675DF442D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC5A89B8-1191-4BC5-BBF0-B675DF442D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC5A89B8-1191-4BC5-BBF0-B675DF442D5F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CADFC50F-8CEC-4254-99FF-E766DA0B9194}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CADFC50F-8CEC-4254-99FF-E766DA0B9194}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CADFC50F-8CEC-4254-99FF-E766DA0B9194}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CADFC50F-8CEC-4254-99FF-E766DA0B9194}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CEC274A0-7FB7-4C21-B52E-DD79AB93DDB4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7190312B-CFEE-49D3-8DAB-542C31071E87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7190312B-CFEE-49D3-8DAB-542C31071E87}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7190312B-CFEE-49D3-8DAB-542C31071E87}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7190312B-CFEE-49D3-8DAB-542C31071E87}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
A => src/Core/NosSmooth.LocalBinding/Errors/BindingNotFoundError.cs +17 -0
@@ 1,17 @@
+//
+// BindingNotFoundError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Errors;
+
+/// <summary>
+/// The memory pattern was not found in the memory.
+/// </summary>
+/// <param name="Pattern">The pattern that could not be found.</param>
+/// <param name="Path">The entity the pattern should represent.</param>
+public record BindingNotFoundError(string Pattern, string Path)
+ : ResultError($"Could not find pattern ({Pattern}) in the memory while searching for {Path}.");
A => src/Core/NosSmooth.LocalBinding/Errors/CouldNotInitializeModuleError.cs +17 -0
@@ 1,17 @@
+//
+// CouldNotInitializeModuleError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Errors;
+
+/// <summary>
+/// Could not initialize the given NosTale module.
+/// </summary>
+/// <param name="Module">The module type that could not be initialized.</param>
+/// <param name="UnderlyingError">The error why the module could not be initialized.</param>
+public record CouldNotInitializeModuleError
+ (Type Module, IResultError UnderlyingError) : ResultError($"Could initialize a nostale module {Module.FullName}.");<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Errors/NotNostaleProcessError.cs +17 -0
@@ 1,17 @@
+//
+// NotNostaleProcessError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Errors;
+
+/// <summary>
+/// The process you tried to browse is not a nostale process.
+/// </summary>
+/// <param name="process"></param>
+public record NotNostaleProcessError(Process process) : ResultError
+ ($"The process {process.ProcessName} ({process.Id} is not a NosTale process.");<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Extensions/MemoryExtensions.cs +33 -0
@@ 1,33 @@
+//
+// MemoryExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="IMemory"/>.
+/// </summary>
+public static class MemoryExtensions
+{
+ /// <summary>
+ /// Follows the offsets to a 32-bit pointer.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="staticAddress">The static address to follow offsets from.</param>
+ /// <param name="offsets">The offsets, first offset is the 0-th element.</param>
+ /// <returns>A final address.</returns>
+ public static IntPtr FollowStaticAddressOffsets(this IMemory memory, int staticAddress, int[] offsets)
+ {
+ int address = staticAddress;
+ foreach (var offset in offsets)
+ {
+ memory.SafeRead((IntPtr)(address + offset), out address);
+ }
+
+ return (IntPtr)address;
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs +44 -0
@@ 1,44 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.LocalBinding.Objects;
+using NosSmooth.LocalBinding.Structs;
+
+namespace NosSmooth.LocalBinding.Extensions;
+
+/// <summary>
+/// Contains extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds bindings to Nostale objects along with <see cref="NosBindingManager"/> to initialize those.
+ /// </summary>
+ /// <remarks>
+ /// Adds <see cref="PlayerManagerBinding"/> and <see cref="NetworkBinding"/>.
+ /// You have to initialize these using <see cref="NosBindingManager"/>
+ /// prior to requesting them from the provider, otherwise an exception
+ /// will be thrown.
+ /// </remarks>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddNostaleBindings(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddSingleton<NosBindingManager>()
+ .AddSingleton<NosBrowserManager>()
+ .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PlayerManager)
+ .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().SceneManager)
+ .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PetManagerList)
+ .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().SceneManager)
+ .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PetManagerList)
+ .AddSingleton(p => p.GetRequiredService<NosBindingManager>().PlayerManager)
+ .AddSingleton(p => p.GetRequiredService<NosBindingManager>().PetManager)
+ .AddSingleton(p => p.GetRequiredService<NosBindingManager>().UnitManager)
+ .AddSingleton(p => p.GetRequiredService<NosBindingManager>().Network);
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/IsExternalInit.cs +15 -0
@@ 1,15 @@
+//
+// IsExternalInit.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace System.Runtime.CompilerServices
+{
+ /// <summary>
+ /// Dummy.
+ /// </summary>
+ public class IsExternalInit
+ {
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/NosBindingManager.cs +375 -0
@@ 1,375 @@
+//
+// NosBindingManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using Microsoft.Extensions.Options;
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Objects;
+using NosSmooth.LocalBinding.Options;
+using Reloaded.Hooks;
+using Reloaded.Hooks.Definitions;
+using Reloaded.Memory.Sigscan;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding;
+
+/// <summary>
+/// Nostale entity binding manager.
+/// </summary>
+public class NosBindingManager : IDisposable
+{
+ private readonly NosBrowserManager _browserManager;
+ private readonly PetManagerBindingOptions _petManagerBindingOptions;
+ private readonly CharacterBindingOptions _characterBindingOptions;
+ private readonly NetworkBindingOptions _networkBindingOptions;
+ private UnitManagerBindingOptions _unitManagerBindingOptions;
+
+ private NetworkBinding? _networkBinding;
+ private PlayerManagerBinding? _characterBinding;
+ private UnitManagerBinding? _unitManagerBinding;
+ private PetManagerBinding? _petManagerBinding;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NosBindingManager"/> class.
+ /// </summary>
+ /// <param name="browserManager">The NosTale browser manager.</param>
+ /// <param name="characterBindingOptions">The character binding options.</param>
+ /// <param name="networkBindingOptions">The network binding options.</param>
+ /// <param name="sceneManagerBindingOptions">The scene manager binding options.</param>
+ /// <param name="petManagerBindingOptions">The pet manager binding options.</param>
+ public NosBindingManager
+ (
+ NosBrowserManager browserManager,
+ IOptions<CharacterBindingOptions> characterBindingOptions,
+ IOptions<NetworkBindingOptions> networkBindingOptions,
+ IOptions<UnitManagerBindingOptions> sceneManagerBindingOptions,
+ IOptions<PetManagerBindingOptions> petManagerBindingOptions
+ )
+ {
+ _browserManager = browserManager;
+ Hooks = new ReloadedHooks();
+ Memory = new Memory();
+ Scanner = new Scanner(Process.GetCurrentProcess(), Process.GetCurrentProcess().MainModule);
+ _characterBindingOptions = characterBindingOptions.Value;
+ _networkBindingOptions = networkBindingOptions.Value;
+ _unitManagerBindingOptions = sceneManagerBindingOptions.Value;
+ _petManagerBindingOptions = petManagerBindingOptions.Value;
+ }
+
+ /// <summary>
+ /// Gets the memory scanner.
+ /// </summary>
+ internal Scanner Scanner { get; }
+
+ /// <summary>
+ /// Gets the reloaded hooks.
+ /// </summary>
+ internal IReloadedHooks Hooks { get; }
+
+ /// <summary>
+ /// Gets the current process memory.
+ /// </summary>
+ internal IMemory Memory { get; }
+
+ /// <summary>
+ /// Gets the network binding.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the manager is not initialized yet.</exception>
+ public NetworkBinding Network
+ {
+ get
+ {
+ if (_networkBinding is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get network. The binding manager is not initialized. Did you forget to call NosBindingManager.Initialize?"
+ );
+ }
+
+ return _networkBinding;
+ }
+ }
+
+ /// <summary>
+ /// Gets the character binding.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the manager is not initialized yet.</exception>
+ public PlayerManagerBinding PlayerManager
+ {
+ get
+ {
+ if (_characterBinding is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get character. The binding manager is not initialized. Did you forget to call NosBindingManager.Initialize?"
+ );
+ }
+
+ return _characterBinding;
+ }
+ }
+
+ /// <summary>
+ /// Gets the character binding.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the manager is not initialized yet.</exception>
+ public UnitManagerBinding UnitManager
+ {
+ get
+ {
+ if (_unitManagerBinding is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get scene manager. The binding manager is not initialized. Did you forget to call NosBindingManager.Initialize?"
+ );
+ }
+
+ return _unitManagerBinding;
+ }
+ }
+
+ /// <summary>
+ /// Gets the pet manager binding.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the manager is not initialized yet.</exception>
+ public PetManagerBinding PetManager
+ {
+ get
+ {
+ if (_petManagerBinding is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get pet manager. The binding manager is not initialized. Did you forget to call NosBindingManager.Initialize?"
+ );
+ }
+
+ return _petManagerBinding;
+ }
+ }
+
+ /// <summary>
+ /// Initialize the existing bindings and hook NosTale functions.
+ /// </summary>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public IResult Initialize()
+ {
+ List<IResult> errorResults = new List<IResult>();
+ var browserInitializationResult = _browserManager.Initialize();
+ if (!browserInitializationResult.IsSuccess)
+ {
+ if (browserInitializationResult.Error is NotNostaleProcessError)
+ {
+ return browserInitializationResult;
+ }
+
+ errorResults.Add(browserInitializationResult);
+ }
+
+ try
+ {
+ var network = NetworkBinding.Create(this, _networkBindingOptions);
+ if (!network.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(NetworkBinding), network.Error),
+ network
+ )
+ );
+ }
+
+ _networkBinding = network.Entity;
+ }
+ catch (Exception e)
+ {
+ errorResults.Add(
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(NetworkBinding), new ExceptionError(e)),
+ (Result)new ExceptionError(e)
+ ));
+ }
+
+ try
+ {
+ var playerManagerBinding = PlayerManagerBinding.Create
+ (
+ this,
+ _browserManager.PlayerManager,
+ _characterBindingOptions
+ );
+ if (!playerManagerBinding.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(PlayerManagerBinding), playerManagerBinding.Error),
+ playerManagerBinding
+ )
+ );
+ }
+ _characterBinding = playerManagerBinding.Entity;
+ }
+ catch (Exception e)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(PlayerManagerBinding), new ExceptionError(e)),
+ (Result)new ExceptionError(e)
+ )
+ );
+ }
+
+ try
+ {
+ var unitManagerBinding = UnitManagerBinding.Create
+ (
+ this,
+ _unitManagerBindingOptions
+ );
+ if (!unitManagerBinding.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(UnitManagerBinding), unitManagerBinding.Error),
+ unitManagerBinding
+ )
+ );
+ }
+ _unitManagerBinding = unitManagerBinding.Entity;
+ }
+ catch (Exception e)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(UnitManagerBinding), new ExceptionError(e)),
+ (Result)new ExceptionError(e)
+ )
+ );
+ }
+
+ try
+ {
+ var petManagerBinding = PetManagerBinding.Create
+ (
+ this,
+ _browserManager.PetManagerList,
+ _petManagerBindingOptions
+ );
+ if (!petManagerBinding.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(PetManagerBinding), petManagerBinding.Error),
+ petManagerBinding
+ )
+ );
+ }
+ _petManagerBinding = petManagerBinding.Entity;
+ }
+ catch (Exception e)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(UnitManagerBinding), new ExceptionError(e)),
+ (Result)new ExceptionError(e)
+ )
+ );
+ }
+
+ return errorResults.Count switch
+ {
+ 0 => Result.FromSuccess(),
+ 1 => errorResults[0],
+ _ => (Result)new AggregateError(errorResults)
+ };
+ }
+
+ /// <summary>
+ /// Disable the currently enabled nostale hooks.
+ /// </summary>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result DisableHooks()
+ {
+ if (_networkBinding is not null)
+ {
+ var result = _networkBinding.DisableHooks();
+ if (!result.IsSuccess)
+ {
+ return result;
+ }
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Scanner.Dispose();
+ DisableHooks();
+ }
+
+ /// <summary>
+ /// Create a hook object for the given pattern.
+ /// </summary>
+ /// <param name="name">The name of the binding.</param>
+ /// <param name="callbackFunction">The callback function to call instead of the original one.</param>
+ /// <param name="pattern">The pattern.</param>
+ /// <param name="offset">The offset from the pattern.</param>
+ /// <param name="hook">Whether to activate the hook.</param>
+ /// <typeparam name="TFunction">The type of the function.</typeparam>
+ /// <returns>The hook object or an error.</returns>
+ internal Result<IHook<TFunction>> CreateHookFromPattern<TFunction>
+ (
+ string name,
+ TFunction callbackFunction,
+ string pattern,
+ int offset = 0,
+ bool hook = true
+ )
+ {
+ var walkFunctionAddress = Scanner.CompiledFindPattern(pattern);
+ if (!walkFunctionAddress.Found)
+ {
+ return new BindingNotFoundError(pattern, "PetManagerBinding.PetWalk");
+ }
+
+ try
+ {
+ return Result<IHook<TFunction>>.FromSuccess
+ (
+ Hooks.CreateHook
+ (
+ callbackFunction,
+ walkFunctionAddress.Offset + (int)_browserManager.Process.MainModule!.BaseAddress
+ )
+ );
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/NosBrowserManager.cs +279 -0
@@ 1,279 @@
+//
+// NosBrowserManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using Microsoft.Extensions.Options;
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Options;
+using NosSmooth.LocalBinding.Structs;
+using Reloaded.Memory.Sigscan;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding;
+
+/// <summary>
+/// Used for browsing a nostale process data.
+/// </summary>
+public class NosBrowserManager
+{
+ /// <summary>
+ /// Checks whether the given process is a NosTale client process.
+ /// </summary>
+ /// <remarks>
+ /// This is just a guess based on presence of "NostaleData" directory.
+ /// </remarks>
+ /// <param name="process">The process to check.</param>
+ /// <returns>Whether the process is a NosTale client.</returns>
+ public static bool IsProcessNostaleProcess(Process process)
+ {
+ if (process.MainModule is null)
+ {
+ return false;
+ }
+
+ var processDirectory = Path.GetDirectoryName(process.MainModule.FileName);
+ if (processDirectory is null)
+ {
+ return false;
+ }
+
+ return Directory.Exists(Path.Combine(processDirectory, "NostaleData"));
+ }
+
+ /// <summary>
+ /// Get all running nostale processes.
+ /// </summary>
+ /// <returns>The nostale processes.</returns>
+ public static IEnumerable<Process> GetAllNostaleProcesses()
+ => Process
+ .GetProcesses()
+ .Where(IsProcessNostaleProcess);
+
+ private readonly PlayerManagerOptions _playerManagerOptions;
+ private readonly SceneManagerOptions _sceneManagerOptions;
+ private readonly PetManagerOptions _petManagerOptions;
+ private PlayerManager? _playerManager;
+ private SceneManager? _sceneManager;
+ private PetManagerList? _petManagerList;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NosBrowserManager"/> class.
+ /// </summary>
+ /// <param name="playerManagerOptions">The options for obtaining player manager.</param>
+ /// <param name="sceneManagerOptions">The scene manager options.</param>
+ /// <param name="petManagerOptions">The pet manager options.</param>
+ public NosBrowserManager
+ (
+ IOptions<PlayerManagerOptions> playerManagerOptions,
+ IOptions<SceneManagerOptions> sceneManagerOptions,
+ IOptions<PetManagerOptions> petManagerOptions
+ )
+ : this
+ (
+ Process.GetCurrentProcess(),
+ playerManagerOptions.Value,
+ sceneManagerOptions.Value,
+ petManagerOptions.Value
+ )
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NosBrowserManager"/> class.
+ /// </summary>
+ /// <param name="process">The process to browse.</param>
+ /// <param name="playerManagerOptions">The options for obtaining player manager.</param>
+ /// <param name="sceneManagerOptions">The scene manager options.</param>
+ /// <param name="petManagerOptions">The pet manager options.</param>
+ public NosBrowserManager
+ (
+ Process process,
+ PlayerManagerOptions playerManagerOptions,
+ SceneManagerOptions sceneManagerOptions,
+ PetManagerOptions petManagerOptions
+ )
+ {
+ _playerManagerOptions = playerManagerOptions;
+ _sceneManagerOptions = sceneManagerOptions;
+ _petManagerOptions = petManagerOptions;
+ Process = process;
+ Memory = new ExternalMemory(process);
+ Scanner = new Scanner(process, process.MainModule);
+ }
+
+ /// <summary>
+ /// The NosTale process.
+ /// </summary>
+ public Process Process { get; }
+
+ /// <summary>
+ /// Gets the memory scanner.
+ /// </summary>
+ internal Scanner Scanner { get; }
+
+ /// <summary>
+ /// Gets the current process memory.
+ /// </summary>
+ internal IMemory Memory { get; }
+
+ /// <summary>
+ /// Gets whether this is a NosTale process or not.
+ /// </summary>
+ public bool IsNostaleProcess => NosBrowserManager.IsProcessNostaleProcess(Process);
+
+ /// <summary>
+ /// Gets whether the player is currently in game.
+ /// </summary>
+ /// <remarks>
+ /// It may be unsafe to access some data if the player is not in game.
+ /// </remarks>
+ public bool IsInGame
+ {
+ get
+ {
+ var player = PlayerManager.Player;
+ return player.Address != IntPtr.Zero;
+ }
+ }
+
+ /// <summary>
+ /// Gets the player manager.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the browser is not initialized or there was an error with initialization of player manager.</exception>
+ public PlayerManager PlayerManager
+ {
+ get
+ {
+ if (_playerManager is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get player manager. The browser manager is not initialized. Did you forget to call NosBrowserManager.Initialize?"
+ );
+ }
+
+ return _playerManager;
+ }
+ }
+
+ /// <summary>
+ /// Gets the scene manager.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the browser is not initialized or there was an error with initialization of scene manager.</exception>
+ public SceneManager SceneManager
+ {
+ get
+ {
+ if (_sceneManager is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get scene manager. The browser manager is not initialized. Did you forget to call NosBrowserManager.Initialize?"
+ );
+ }
+
+ return _sceneManager;
+ }
+ }
+
+ /// <summary>
+ /// Gets the pet manager list.
+ /// </summary>
+ /// <exception cref="InvalidOperationException">Thrown if the browser is not initialized or there was an error with initialization of pet manager list.</exception>
+ public PetManagerList PetManagerList
+ {
+ get
+ {
+ if (_petManagerList is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Could not get pet manager list. The browser manager is not initialized. Did you forget to call NosBrowserManager.Initialize?"
+ );
+ }
+
+ return _petManagerList;
+ }
+ }
+
+ /// <summary>
+ /// Initialize the nos browser modules.
+ /// </summary>
+ /// <remarks>
+ /// Needed to use all of the classes from NosTale.
+ /// </remarks>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public IResult Initialize()
+ {
+ if (!IsNostaleProcess)
+ {
+ return (Result)new NotNostaleProcessError(Process);
+ }
+
+ List<IResult> errorResults = new List<IResult>();
+ if (_playerManager is null)
+ {
+ var playerManagerResult = PlayerManager.Create(this, _playerManagerOptions);
+ if (!playerManagerResult.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(PlayerManager), playerManagerResult.Error),
+ playerManagerResult
+ )
+ );
+ }
+
+ _playerManager = playerManagerResult.Entity;
+ }
+
+ if (_sceneManager is null)
+ {
+ var sceneManagerResult = SceneManager.Create(this, _sceneManagerOptions);
+ if (!sceneManagerResult.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(SceneManager), sceneManagerResult.Error),
+ sceneManagerResult
+ )
+ );
+ }
+
+ _sceneManager = sceneManagerResult.Entity;
+ }
+
+ if (_petManagerList is null)
+ {
+ var petManagerResult = PetManagerList.Create(this, _petManagerOptions);
+ if (!petManagerResult.IsSuccess)
+ {
+ errorResults.Add
+ (
+ Result.FromError
+ (
+ new CouldNotInitializeModuleError(typeof(PetManagerList), petManagerResult.Error),
+ petManagerResult
+ )
+ );
+ }
+
+ _petManagerList = petManagerResult.Entity;
+ }
+
+ return errorResults.Count switch
+ {
+ 0 => Result.FromSuccess(),
+ 1 => errorResults[0],
+ _ => (Result)new AggregateError(errorResults)
+ };
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/NosSmooth.LocalBinding.csproj +18 -0
@@ 1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <LangVersion>10</LangVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
+ <PackageReference Include="Reloaded.Hooks" Version="3.5.0" />
+ <PackageReference Include="Reloaded.Memory.Sigscan" Version="1.2.1" />
+ <PackageReference Include="Remora.Results" Version="7.1.0" />
+ </ItemGroup>
+
+</Project>
A => src/Core/NosSmooth.LocalBinding/Objects/NetworkBinding.cs +236 -0
@@ 1,236 @@
+//
+// NetworkBinding.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Options;
+using Reloaded.Hooks.Definitions;
+using Reloaded.Hooks.Definitions.X86;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Objects;
+
+/// <summary>
+/// The binding to nostale network object.
+/// </summary>
+public class NetworkBinding
+{
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate void PacketSendDelegate(IntPtr packetObject, IntPtr packetString);
+
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate void PacketReceiveDelegate(IntPtr packetObject, IntPtr packetString);
+
+ /// <summary>
+ /// Create the network binding with finding the network object and functions.
+ /// </summary>
+ /// <param name="bindingManager">The binding manager.</param>
+ /// <param name="options">The options for the binding.</param>
+ /// <returns>A network binding or an error.</returns>
+ public static Result<NetworkBinding> Create(NosBindingManager bindingManager, NetworkBindingOptions options)
+ {
+ var process = Process.GetCurrentProcess();
+ var networkObjectAddress = bindingManager.Scanner.CompiledFindPattern(options.NetworkObjectPattern);
+ if (!networkObjectAddress.Found)
+ {
+ return new BindingNotFoundError(options.NetworkObjectPattern, "NetworkBinding");
+ }
+
+ var packetSendAddress = bindingManager.Scanner.CompiledFindPattern(options.SendFunctionPattern);
+ if (!packetSendAddress.Found)
+ {
+ return new BindingNotFoundError(options.SendFunctionPattern, "NetworkBinding.SendPacket");
+ }
+
+ var packetReceiveAddress = bindingManager.Scanner.CompiledFindPattern(options.ReceiveFunctionPattern);
+ if (!packetReceiveAddress.Found)
+ {
+ return new BindingNotFoundError(options.ReceiveFunctionPattern, "NetworkBinding.ReceivePacket");
+ }
+
+ var sendFunction = bindingManager.Hooks.CreateFunction<PacketSendDelegate>
+ (packetSendAddress.Offset + (int)process.MainModule!.BaseAddress);
+ var sendWrapper = sendFunction.GetWrapper();
+
+ var receiveFunction = bindingManager.Hooks.CreateFunction<PacketReceiveDelegate>
+ (packetReceiveAddress.Offset + (int)process.MainModule!.BaseAddress);
+ var receiveWrapper = receiveFunction.GetWrapper();
+
+ var binding = new NetworkBinding
+ (
+ bindingManager,
+ (IntPtr)(networkObjectAddress.Offset + (int)process.MainModule!.BaseAddress + 0x01),
+ sendWrapper,
+ receiveWrapper
+ );
+
+ if (options.HookSend)
+ {
+ binding._sendHook = sendFunction
+ .Hook(binding.SendPacketDetour);
+ binding._originalSend = binding._sendHook.OriginalFunction;
+ }
+
+ if (options.HookReceive)
+ {
+ binding._receiveHook = receiveFunction
+ .Hook(binding.ReceivePacketDetour);
+ binding._originalReceive = binding._receiveHook.OriginalFunction;
+ }
+
+ binding._sendHook?.Activate();
+ binding._receiveHook?.Activate();
+ return binding;
+ }
+
+ private readonly NosBindingManager _bindingManager;
+ private readonly IntPtr _networkManagerAddress;
+ private IHook<PacketSendDelegate>? _sendHook;
+ private IHook<PacketReceiveDelegate>? _receiveHook;
+ private PacketSendDelegate _originalSend;
+ private PacketReceiveDelegate _originalReceive;
+
+ private NetworkBinding
+ (
+ NosBindingManager bindingManager,
+ IntPtr networkManagerAddress,
+ PacketSendDelegate originalSend,
+ PacketReceiveDelegate originalReceive
+ )
+ {
+ _bindingManager = bindingManager;
+ _networkManagerAddress = networkManagerAddress;
+ _originalSend = originalSend;
+ _originalReceive = originalReceive;
+ }
+
+ /// <summary>
+ /// Event that is called when packet send was called by NosTale.
+ /// </summary>
+ /// <remarks>
+ /// The send must be hooked for this event to be called.
+ /// </remarks>
+ public event Func<string, bool>? PacketSend;
+
+ /// <summary>
+ /// Event that is called when packet receive was called by NosTale.
+ /// </summary>
+ /// <remarks>
+ /// The receive must be hooked for this event to be called.
+ /// </remarks>
+ public event Func<string, bool>? PacketReceive;
+
+ /// <summary>
+ /// Send the given packet.
+ /// </summary>
+ /// <param name="packet">The packet to send.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result SendPacket(string packet)
+ {
+ try
+ {
+ using var nostaleString = NostaleStringA.Create(_bindingManager.Memory, packet);
+ _originalSend(GetManagerAddress(false), nostaleString.Get());
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <summary>
+ /// Receive the given packet.
+ /// </summary>
+ /// <param name="packet">The packet to receive.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result ReceivePacket(string packet)
+ {
+ try
+ {
+ using var nostaleString = NostaleStringA.Create(_bindingManager.Memory, packet);
+ _originalReceive(GetManagerAddress(true), nostaleString.Get());
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <summary>
+ /// Disable all the hooks that are currently enabled.
+ /// </summary>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result DisableHooks()
+ {
+ _receiveHook?.Disable();
+ _sendHook?.Disable();
+ return Result.FromSuccess();
+ }
+
+ private IntPtr GetManagerAddress(bool third)
+ {
+ IntPtr networkManager = _networkManagerAddress;
+ _bindingManager.Memory.Read(networkManager, out networkManager);
+ _bindingManager.Memory.Read(networkManager, out networkManager);
+ _bindingManager.Memory.Read(networkManager, out networkManager);
+
+ if (third)
+ {
+ _bindingManager.Memory.Read(networkManager + 0x34, out networkManager);
+ }
+
+ return networkManager;
+ }
+
+ private void SendPacketDetour(IntPtr packetObject, IntPtr packetString)
+ {
+ var packet = Marshal.PtrToStringAnsi(packetString);
+ if (packet is null)
+ { // ?
+ _originalSend(packetObject, packetString);
+ }
+ else
+ {
+ var result = PacketSend?.Invoke(packet);
+ if (result ?? true)
+ {
+ _originalSend(packetObject, packetString);
+ }
+ }
+ }
+
+ private void ReceivePacketDetour(IntPtr packetObject, IntPtr packetString)
+ {
+ var packet = Marshal.PtrToStringAnsi(packetString);
+ if (packet is null)
+ { // ?
+ _originalReceive(packetObject, packetString);
+ }
+ else
+ {
+ var result = PacketReceive?.Invoke(packet);
+ if (result ?? true)
+ {
+ _originalReceive(packetObject, packetString);
+ }
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Objects/NostaleStringA.cs +84 -0
@@ 1,84 @@
+//
+// NostaleStringA.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Text;
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Objects;
+
+/// <summary>
+/// Represents nostale string object.
+/// </summary>
+public class NostaleStringA : IDisposable
+{
+ private readonly IMemory _memory;
+ private IntPtr _pointer;
+
+ /// <summary>
+ /// Create an instance of <see cref="NostaleStringA"/>.
+ /// </summary>
+ /// <param name="memory">The memory to allocate the string on.</param>
+ /// <param name="data">The string contents.</param>
+ /// <returns>A nostale string.</returns>
+ public static NostaleStringA Create(IMemory memory, string data)
+ {
+ var bytes = Encoding.ASCII.GetBytes(data);
+ var allocated = memory.Allocate(bytes.Length + 1 + 8);
+ memory.SafeWrite(allocated, 1);
+ memory.SafeWrite(allocated + 4, data.Length);
+ memory.SafeWriteRaw(allocated + 8, bytes);
+ memory.SafeWrite(allocated + 8 + data.Length, 0);
+
+ return new NostaleStringA(memory, allocated);
+ }
+
+ private NostaleStringA(IMemory memory, IntPtr pointer)
+ {
+ _memory = memory;
+ _pointer = pointer;
+
+ }
+
+ /// <summary>
+ /// Finalizes an instance of the <see cref="NostaleStringA"/> class.
+ /// </summary>
+ ~NostaleStringA()
+ {
+ Free();
+ }
+
+ /// <summary>
+ /// Gets whether the string is still allocated.
+ /// </summary>
+ public bool Allocated => _pointer != IntPtr.Zero;
+
+ /// <summary>
+ /// Get the pointer to the string.
+ /// </summary>
+ /// <returns>A pointer to the string to pass to NosTale.</returns>
+ public IntPtr Get()
+ {
+ return _pointer + 0x08;
+ }
+
+ /// <summary>
+ /// Free the memory allocated by the string.
+ /// </summary>
+ public void Free()
+ {
+ if (Allocated)
+ {
+ _memory.Free(_pointer);
+ _pointer = IntPtr.Zero;
+ }
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ Free();
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Objects/PetManagerBinding.cs +129 -0
@@ 1,129 @@
+//
+// PetManagerBinding.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Options;
+using NosSmooth.LocalBinding.Structs;
+using Reloaded.Hooks.Definitions;
+using Reloaded.Hooks.Definitions.X86;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Objects;
+
+/// <summary>
+/// The binding to NosTale pet manager.
+/// </summary>
+public class PetManagerBinding
+{
+ /// <summary>
+ /// Create nostale pet manager binding.
+ /// </summary>
+ /// <param name="bindingManager">The binding manager.</param>
+ /// <param name="petManagerList">The list of the pet managers.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>A pet manager binding or and error.</returns>
+ public static Result<PetManagerBinding> Create
+ (NosBindingManager bindingManager, PetManagerList petManagerList, PetManagerBindingOptions options)
+ {
+ var petManager = new PetManagerBinding(petManagerList);
+ var hookResult = bindingManager.CreateHookFromPattern<PetWalkDelegate>
+ (
+ "PetManagerBinding.PetWalk",
+ petManager.PetWalkDetour,
+ options.PetWalkPattern,
+ hook: options.HookPetWalk
+ );
+
+ if (!hookResult.IsSuccess)
+ {
+ return Result<PetManagerBinding>.FromError(hookResult);
+ }
+
+ petManager._petWalkHook = hookResult.Entity;
+ return petManager;
+ }
+
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate bool PetWalkDelegate
+ (
+ IntPtr petManagerPtr,
+ uint position,
+ short unknown0 = 0,
+ int unknown1 = 1,
+ int unknown2 = 1
+ );
+
+ private IHook<PetWalkDelegate> _petWalkHook = null!;
+
+ private PetManagerBinding(PetManagerList petManagerList)
+ {
+ PetManagerList = petManagerList;
+ }
+
+ /// <summary>
+ /// Gets the hook of the pet walk function.
+ /// </summary>
+ public IHook PetWalkHook => _petWalkHook;
+
+ /// <summary>
+ /// Gets pet manager list.
+ /// </summary>
+ public PetManagerList PetManagerList { get; }
+
+ /// <summary>
+ /// Walk the given pet to the given location.
+ /// </summary>
+ /// <param name="selector">Index of the pet to walk. -1 for every pet currently available.</param>
+ /// <param name="x">The x coordinate.</param>
+ /// <param name="y">The y coordinate.</param>
+ /// <returns>A result returned from NosTale or an error.</returns>
+ public Result<bool> PetWalk(int selector, ushort x, ushort y)
+ {
+ uint position = ((uint)y << 16) | x;
+ if (PetManagerList.Length < selector + 1)
+ {
+ return new NotFoundError("Could not find the pet using the given selector.");
+ }
+
+ if (selector == -1)
+ {
+ bool lastResult = true;
+ for (int i = 0; i < PetManagerList.Length; i++)
+ {
+ lastResult = _petWalkHook.OriginalFunction(PetManagerList[i].Address, position);
+ }
+
+ return lastResult;
+ }
+ else
+ {
+ return _petWalkHook.OriginalFunction(PetManagerList[selector].Address, position);
+ }
+ }
+
+ private bool PetWalkDetour
+ (
+ IntPtr petManagerPtr,
+ uint position,
+ short unknown0 = 0,
+ int unknown1 = 1,
+ int unknown2 = 1
+ )
+ {
+ return _petWalkHook.OriginalFunction
+ (
+ petManagerPtr,
+ position,
+ unknown0,
+ unknown1,
+ unknown2
+ );
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Objects/PlayerManagerBinding.cs +284 -0
@@ 1,284 @@
+//
+// PlayerManagerBinding.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Options;
+using NosSmooth.LocalBinding.Structs;
+using Reloaded.Hooks.Definitions;
+using Reloaded.Hooks.Definitions.X86;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Objects;
+
+/// <summary>
+/// The nostale binding of a character.
+/// </summary>
+public class PlayerManagerBinding
+{
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate bool WalkDelegate(IntPtr playerManagerPtr, int position, short unknown0 = 0, int unknown1 = 1);
+
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate bool FollowEntityDelegate
+ (
+ IntPtr playerManagerPtr,
+ IntPtr entityPtr,
+ int unknown1 = 0,
+ int unknown2 = 1
+ );
+
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate void UnfollowEntityDelegate(IntPtr playerManagerPtr, int unknown = 0);
+
+ /// <summary>
+ /// Create the network binding with finding the network object and functions.
+ /// </summary>
+ /// <param name="bindingManager">The binding manager.</param>
+ /// <param name="playerManager">The player manager.</param>
+ /// <param name="options">The options for the binding.</param>
+ /// <returns>A network binding or an error.</returns>
+ public static Result<PlayerManagerBinding> Create(NosBindingManager bindingManager, PlayerManager playerManager, CharacterBindingOptions options)
+ {
+ var process = Process.GetCurrentProcess();
+
+ var walkFunctionAddress = bindingManager.Scanner.CompiledFindPattern(options.WalkFunctionPattern);
+ if (!walkFunctionAddress.Found)
+ {
+ return new BindingNotFoundError(options.WalkFunctionPattern, "CharacterBinding.Walk");
+ }
+
+ var followEntityAddress = bindingManager.Scanner.CompiledFindPattern(options.FollowEntityPattern);
+ if (!followEntityAddress.Found)
+ {
+ return new BindingNotFoundError(options.FollowEntityPattern, "CharacterBinding.FollowEntity");
+ }
+
+ var unfollowEntityAddress = bindingManager.Scanner.CompiledFindPattern(options.UnfollowEntityPattern);
+ if (!unfollowEntityAddress.Found)
+ {
+ return new BindingNotFoundError(options.UnfollowEntityPattern, "CharacterBinding.UnfollowEntity");
+ }
+
+ var walkFunction = bindingManager.Hooks.CreateFunction<WalkDelegate>
+ (walkFunctionAddress.Offset + (int)process.MainModule!.BaseAddress);
+ var walkWrapper = walkFunction.GetWrapper();
+
+ var followEntityFunction = bindingManager.Hooks.CreateFunction<FollowEntityDelegate>
+ (followEntityAddress.Offset + (int)process.MainModule!.BaseAddress);
+ var followEntityWrapper = followEntityFunction.GetWrapper();
+
+ var unfollowEntityFunction = bindingManager.Hooks.CreateFunction<UnfollowEntityDelegate>
+ (unfollowEntityAddress.Offset + (int)process.MainModule!.BaseAddress);
+ var unfollowEntityWrapper = unfollowEntityFunction.GetWrapper();
+
+ var binding = new PlayerManagerBinding
+ (
+ bindingManager,
+ playerManager,
+ walkWrapper,
+ followEntityWrapper,
+ unfollowEntityWrapper
+ );
+
+ if (options.HookWalk)
+ {
+ binding._walkHook = walkFunction
+ .Hook(binding.WalkDetour);
+ binding._originalWalk = binding._walkHook.OriginalFunction;
+ }
+
+ if (options.HookFollowEntity)
+ {
+ binding._followHook = followEntityFunction.Hook(binding.FollowEntityDetour);
+ binding._originalFollowEntity = binding._followHook.OriginalFunction;
+ }
+
+ if (options.HookUnfollowEntity)
+ {
+ binding._unfollowHook = unfollowEntityFunction.Hook(binding.UnfollowEntityDetour);
+ binding._originalUnfollowEntity = binding._unfollowHook.OriginalFunction;
+ }
+
+ binding._walkHook?.Activate();
+ binding._followHook?.Activate();
+ binding._unfollowHook?.Activate();
+ return binding;
+ }
+
+ private readonly NosBindingManager _bindingManager;
+
+ private IHook<WalkDelegate>? _walkHook;
+ private IHook<FollowEntityDelegate>? _followHook;
+ private IHook<UnfollowEntityDelegate>? _unfollowHook;
+
+ private FollowEntityDelegate _originalFollowEntity;
+ private UnfollowEntityDelegate _originalUnfollowEntity;
+ private WalkDelegate _originalWalk;
+
+ private PlayerManagerBinding
+ (
+ NosBindingManager bindingManager,
+ PlayerManager playerManager,
+ WalkDelegate originalWalk,
+ FollowEntityDelegate originalFollowEntity,
+ UnfollowEntityDelegate originalUnfollowEntity
+ )
+ {
+ PlayerManager = playerManager;
+ _bindingManager = bindingManager;
+ _originalWalk = originalWalk;
+ _originalFollowEntity = originalFollowEntity;
+ _originalUnfollowEntity = originalUnfollowEntity;
+ }
+
+ /// <summary>
+ /// Gets the player manager.
+ /// </summary>
+ public PlayerManager PlayerManager { get; }
+
+ /// <summary>
+ /// Event that is called when walk was called by NosTale.
+ /// </summary>
+ /// <remarks>
+ /// The walk must be hooked for this event to be called.
+ /// </remarks>
+ public event Func<ushort, ushort, bool>? WalkCall;
+
+ /// <summary>
+ /// Event that is called when entity follow or unfollow was called.
+ /// </summary>
+ /// <remarks>
+ /// The follow/unfollow entity must be hooked for this event to be called.
+ /// </remarks>
+ public event Func<MapBaseObj?, bool>? FollowEntityCall;
+
+ /// <summary>
+ /// Disable all the hooks that are currently enabled.
+ /// </summary>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result DisableHooks()
+ {
+ _walkHook?.Disable();
+ return Result.FromSuccess();
+ }
+
+ /// <summary>
+ /// Walk to the given position.
+ /// </summary>
+ /// <param name="x">The x coordinate.</param>
+ /// <param name="y">The y coordinate.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result<bool> Walk(ushort x, ushort y)
+ {
+ int param = (y << 16) | x;
+ try
+ {
+ return _originalWalk(PlayerManager.Address, param);
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ }
+
+ private bool WalkDetour(IntPtr characterObject, int position, short unknown0, int unknown1)
+ {
+ var result = WalkCall?.Invoke((ushort)(position & 0xFFFF), (ushort)((position >> 16) & 0xFFFF));
+ if (result ?? true)
+ {
+ return _originalWalk(characterObject, position, unknown0, unknown1);
+ }
+
+ return false;
+ }
+
+ /// <summary>
+ /// Follow the entity.
+ /// </summary>
+ /// <param name="entity">The entity.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result FollowEntity(MapBaseObj? entity)
+ => FollowEntity(entity?.Address ?? IntPtr.Zero);
+
+ /// <summary>
+ /// Follow the entity.
+ /// </summary>
+ /// <param name="entityAddress">The entity address.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result FollowEntity(IntPtr entityAddress)
+ {
+ try
+ {
+ _originalFollowEntity(PlayerManager.Address, entityAddress);
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ /// <summary>
+ /// Stop following entity.
+ /// </summary>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result UnfollowEntity()
+ {
+ try
+ {
+ _originalUnfollowEntity(PlayerManager.Address);
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ private bool FollowEntityDetour
+ (
+ IntPtr playerManagerPtr,
+ IntPtr entityPtr,
+ int unknown1,
+ int unknown2
+ )
+ {
+ var result = FollowEntityCall?.Invoke(new MapBaseObj(_bindingManager.Memory, entityPtr));
+ if (result ?? true)
+ {
+ return _originalFollowEntity(playerManagerPtr, entityPtr, unknown1, unknown2);
+ }
+
+ return false;
+ }
+
+ private void UnfollowEntityDetour(IntPtr playerManagerPtr, int unknown)
+ {
+ var result = FollowEntityCall?.Invoke(null);
+ if (result ?? true)
+ {
+ _originalUnfollowEntity(playerManagerPtr, unknown);
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Objects/UnitManagerBinding.cs +164 -0
@@ 1,164 @@
+//
+// UnitManagerBinding.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Extensions;
+using NosSmooth.LocalBinding.Options;
+using NosSmooth.LocalBinding.Structs;
+using Reloaded.Hooks;
+using Reloaded.Hooks.Definitions;
+using Reloaded.Hooks.Definitions.X86;
+using Reloaded.Memory.Buffers.Internal.Kernel32;
+using Remora.Results;
+using SharpDisasm.Udis86;
+
+namespace NosSmooth.LocalBinding.Objects;
+
+/// <summary>
+/// The nostale binding of a scene manager.
+/// </summary>
+/// <remarks>
+/// The scene manager holds addresses to entities, mouse position, ....
+/// </remarks>
+public class UnitManagerBinding
+{
+ [Function
+ (
+ new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
+ FunctionAttribute.Register.eax,
+ FunctionAttribute.StackCleanup.Callee
+ )]
+ private delegate int FocusEntityDelegate(IntPtr unitManagerPtr, IntPtr entityPtr);
+
+ /// <summary>
+ /// Create the scene manager binding.
+ /// </summary>
+ /// <param name="bindingManager">The binding manager.</param>
+ /// <param name="bindingOptions">The options for the binding.</param>
+ /// <returns>A network binding or an error.</returns>
+ public static Result<UnitManagerBinding> Create
+ (NosBindingManager bindingManager, UnitManagerBindingOptions bindingOptions)
+ {
+ var process = Process.GetCurrentProcess();
+
+ var unitManagerStaticAddress = bindingManager.Scanner.CompiledFindPattern(bindingOptions.UnitManagerPattern);
+ if (!unitManagerStaticAddress.Found)
+ {
+ return new BindingNotFoundError(bindingOptions.UnitManagerPattern, "UnitManagerBinding.UnitManager");
+ }
+
+ var focusEntityAddress = bindingManager.Scanner.CompiledFindPattern(bindingOptions.FocusEntityPattern);
+ if (!focusEntityAddress.Found)
+ {
+ return new BindingNotFoundError(bindingOptions.FocusEntityPattern, "UnitManagerBinding.FocusEntity");
+ }
+
+ var focusEntityFunction = bindingManager.Hooks.CreateFunction<FocusEntityDelegate>
+ (focusEntityAddress.Offset + (int)process.MainModule!.BaseAddress + 0x04);
+ var focusEntityWrapper = focusEntityFunction.GetWrapper();
+
+ var binding = new UnitManagerBinding
+ (
+ bindingManager,
+ (int)process.MainModule!.BaseAddress + unitManagerStaticAddress.Offset,
+ bindingOptions.UnitManagerOffsets,
+ focusEntityWrapper
+ );
+
+ if (bindingOptions.HookFocusEntity)
+ {
+ binding._focusHook = focusEntityFunction.Hook(binding.FocusEntityDetour);
+ binding._originalFocusEntity = binding._focusHook.OriginalFunction;
+ }
+
+ binding._focusHook?.Activate();
+ return binding;
+ }
+
+ private readonly int _staticUnitManagerAddress;
+ private readonly int[] _unitManagerOffsets;
+
+ private readonly NosBindingManager _bindingManager;
+ private FocusEntityDelegate _originalFocusEntity;
+
+ private IHook<FocusEntityDelegate>? _focusHook;
+
+ private UnitManagerBinding
+ (
+ NosBindingManager bindingManager,
+ int staticUnitManagerAddress,
+ int[] unitManagerOffsets,
+ FocusEntityDelegate originalFocusEntity
+ )
+ {
+ _originalFocusEntity = originalFocusEntity;
+ _bindingManager = bindingManager;
+ _staticUnitManagerAddress = staticUnitManagerAddress;
+ _unitManagerOffsets = unitManagerOffsets;
+ }
+
+ /// <summary>
+ /// Gets the address of unit manager.
+ /// </summary>
+ public IntPtr Address => _bindingManager.Memory.FollowStaticAddressOffsets
+ (_staticUnitManagerAddress, _unitManagerOffsets);
+
+ /// <summary>
+ /// Event that is called when entity focus was called by NosTale.
+ /// </summary>
+ /// <remarks>
+ /// The focus entity must be hooked for this event to be called.
+ /// </remarks>
+ public event Func<MapBaseObj?, bool>? EntityFocus;
+
+ /// <summary>
+ /// Focus the entity.
+ /// </summary>
+ /// <param name="entity">The entity.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result FocusEntity(MapBaseObj? entity)
+ => FocusEntity(entity?.Address ?? IntPtr.Zero);
+
+ /// <summary>
+ /// Focus the entity.
+ /// </summary>
+ /// <param name="entityAddress">The entity address.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result FocusEntity(IntPtr entityAddress)
+ {
+ try
+ {
+ var res = _originalFocusEntity(Address, entityAddress);
+ Console.WriteLine(res);
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+
+ return Result.FromSuccess();
+ }
+
+ private int FocusEntityDetour(IntPtr unitManagerPtr, IntPtr entityId)
+ {
+ MapBaseObj? obj = null;
+ if (entityId != IntPtr.Zero)
+ {
+ obj = new MapBaseObj(_bindingManager.Memory, entityId);
+ }
+
+ var result = EntityFocus?.Invoke(obj);
+ if (result ?? true)
+ {
+ var res = _originalFocusEntity(unitManagerPtr, entityId);
+ Console.WriteLine(res);
+ return res;
+ }
+
+ return 0;
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/CharacterBindingOptions.cs +47 -0
@@ 1,47 @@
+//
+// CharacterBindingOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Objects;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="PlayerManagerBinding"/>.
+/// </summary>
+public class CharacterBindingOptions
+{
+ /// <summary>
+ /// Gets or sets whether to hook the walk function.
+ /// </summary>
+ public bool HookWalk { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the pattern to find the walk function at.
+ /// </summary>
+ public string WalkFunctionPattern { get; set; } = "55 8B EC 83 C4 EC 53 56 57 66 89 4D FA";
+
+ /// <summary>
+ /// Gets or sets the pattern to find the follow entity method at.
+ /// </summary>
+ public string FollowEntityPattern { get; set; }
+ = "55 8B EC 51 53 56 57 88 4D FF 8B F2 8B F8";
+
+ /// <summary>
+ /// Gets or sets the pattern to find the unfollow entity method at.
+ /// </summary>
+ public string UnfollowEntityPattern { get; set; }
+ = "80 78 14 00 74 1A";
+
+ /// <summary>
+ /// Gets or sets whether to hook the follow entity function.
+ /// </summary>
+ public bool HookFollowEntity { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets whether to hook the unfollow entity function.
+ /// </summary>
+ public bool HookUnfollowEntity { get; set; } = true;
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/NetworkBindingOptions.cs +44 -0
@@ 1,44 @@
+//
+// NetworkBindingOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Objects;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="NetworkBinding"/>.
+/// </summary>
+public class NetworkBindingOptions
+{
+ /// <summary>
+ /// Gets or sets whether to hook the send packet function.
+ /// </summary>
+ public bool HookSend { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets whether to hook the receive packet function.
+ /// </summary>
+ public bool HookReceive { get; set; } = true;
+
+ /// <summary>
+ /// Gets or sets the pattern to find the network object at.
+ /// </summary>
+ /// <remarks>
+ /// The address of the object is "three pointers down" from address found on this pattern.
+ /// </remarks>
+ public string NetworkObjectPattern { get; set; }
+ = "A1 ?? ?? ?? ?? 8B 00 BA ?? ?? ?? ?? E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? A1 ?? ?? ?? ?? 8B 00 8B 40 40";
+
+ /// <summary>
+ /// Gets or sets the pattern to find the send packet function at.
+ /// </summary>
+ public string SendFunctionPattern { get; set; } = "53 56 8B F2 8B D8 EB 04";
+
+ /// <summary>
+ /// Gets or sets the pattern to find the receive function at.
+ /// </summary>
+ public string ReceiveFunctionPattern { get; set; } = "55 8B EC 83 C4 F4 53 56 57 33 C9 89 4D F4 89 55 FC 8B D8 8B 45 FC";
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/PetManagerBindingOptions.cs +26 -0
@@ 1,26 @@
+//
+// PetManagerBindingOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Objects;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="PetManagerBinding"/>.
+/// </summary>
+public class PetManagerBindingOptions
+{
+ /// <summary>
+ /// Gets or sets the pattern of a pet walk function.
+ /// </summary>
+ public string PetWalkPattern { get; set; }
+ = "55 8b ec 83 c4 e4 53 56 57 8b f9 89 55 fc 8b d8 c6 45 fb 00";
+
+ /// <summary>
+ /// Gets or sets whether to hook the pet walk function.
+ /// </summary>
+ public bool HookPetWalk { get; set; } = true;
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/PetManagerOptions.cs +28 -0
@@ 1,28 @@
+//
+// PetManagerOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Objects;
+using NosSmooth.LocalBinding.Structs;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="PetManagerList"/>.
+/// </summary>
+public class PetManagerOptions
+{
+ /// <summary>
+ /// Gets or sets the pattern to find static pet manager list address at.
+ /// </summary>
+ public string PetManagerListPattern { get; set; }
+ = "8B F8 8B D3 A1 ?? ?? ?? ?? E8 ?? ?? ?? ?? 8B D0";
+
+ /// <summary>
+ /// Gets or sets the offsets to find the pet manager list at from the static address.
+ /// </summary>
+ public int[] PetManagerListOffsets { get; set; }
+ = { 5, 0 };
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/PlayerManagerOptions.cs +27 -0
@@ 1,27 @@
+//
+// PlayerManagerOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Structs;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="PlayerManager"/>.
+/// </summary>
+public class PlayerManagerOptions
+{
+ /// <summary>
+ /// Gets or sets the pattern to find the character object at.
+ /// </summary>
+ public string PlayerManagerPattern { get; set; }
+ = "33 C9 8B 55 FC A1 ?? ?? ?? ?? E8 ?? ?? ?? ??";
+
+ /// <summary>
+ /// Gets or sets the offsets to find the player manager at from the static address.
+ /// </summary>
+ public int[] PlayerManagerOffsets { get; set; }
+ = { 6, 0 };
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/SceneManagerOptions.cs +30 -0
@@ 1,30 @@
+//
+// SceneManagerOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Structs;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="SceneManager"/>.
+/// </summary>
+public class SceneManagerOptions
+{
+ /// <summary>
+ /// Gets or sets the pattern to find the scene manager object at.
+ /// </summary>
+ /// <remarks>
+ /// The address of the object is direct pointer to the scene manager.
+ /// </remarks>
+ public string SceneManagerObjectPattern { get; set; }
+ = "FF ?? ?? ?? ?? ?? FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 FF FF FF FF";
+
+ /// <summary>
+ /// Gets or sets the offsets to find the scene manager at from the static address.
+ /// </summary>
+ public int[] SceneManagerOffsets { get; set; }
+ = { 1 };
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Options/UnitManagerBindingOptions.cs +38 -0
@@ 1,38 @@
+//
+// UnitManagerBindingOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Objects;
+
+namespace NosSmooth.LocalBinding.Options;
+
+/// <summary>
+/// Options for <see cref="UnitManagerBinding"/>.
+/// </summary>
+public class UnitManagerBindingOptions
+{
+ /// <summary>
+ /// Gets or sets the pattern to static address of unit manager.
+ /// </summary>
+ public string UnitManagerPattern { get; set; }
+ = "TODO";
+
+ /// <summary>
+ /// Gets or sets the pointer offsets from the unit manager static address.
+ /// </summary>
+ public int[] UnitManagerOffsets { get; set; }
+ = { 15 };
+
+ /// <summary>
+ /// Gets or sets the pattern to find the focus entity method at.
+ /// </summary>
+ public string FocusEntityPattern { get; set; }
+ = "73 00 00 00 55 8b ec b9 05 00 00 00";
+
+ /// <summary>
+ /// Gets or sets whether to hook the Focus entity function.
+ /// </summary>
+ public bool HookFocusEntity { get; set; } = true;
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/ControlManager.cs +78 -0
@@ 1,78 @@
+//
+// ControlManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// Base for player and pet managers.
+/// </summary>
+public abstract class ControlManager : NostaleObject
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ControlManager"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="address">The address of the manager.</param>
+ protected ControlManager(IMemory memory, IntPtr address)
+ : base(memory, address)
+ {
+ }
+
+ /// <summary>
+ /// Gets the entity this control manager is for.
+ /// </summary>
+ public abstract MapBaseObj Entity { get; }
+
+ /// <summary>
+ /// Gets the current player position x coordinate.
+ /// </summary>
+ public int X
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x4, out short x);
+ return x;
+ }
+ }
+
+ /// <summary>
+ /// Gets the current player position x coordinate.
+ /// </summary>
+ public int Y
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x6, out short y);
+ return y;
+ }
+ }
+
+ /// <summary>
+ /// Gets the target x coordinate the player is moving to.
+ /// </summary>
+ public int TargetX
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x8, out short targetX);
+ return targetX;
+ }
+ }
+
+ /// <summary>
+ /// Gets the target y coordinate the player is moving to.
+ /// </summary>
+ public int TargetY
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0xA, out short targetX);
+ return targetX;
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/MapBaseObj.cs +68 -0
@@ 1,68 @@
+//
+// MapBaseObj.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// Base map object. Common for players, monsters, npcs.
+/// </summary>
+public class MapBaseObj : NostaleObject
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MapBaseObj"/> class.
+ /// </summary>
+ public MapBaseObj()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MapBaseObj"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="mapObjPointer">The map object pointer.</param>
+ public MapBaseObj(IMemory memory, IntPtr mapObjPointer)
+ : base(memory, mapObjPointer)
+ {
+ }
+
+ /// <summary>
+ /// Gets the id of the entity.
+ /// </summary>
+ public long Id
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x08, out int id);
+ return id;
+ }
+ }
+
+ /// <summary>
+ /// Gets the x coordinate of the entity.
+ /// </summary>
+ public ushort X
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x0C, out ushort x);
+ return x;
+ }
+ }
+
+ /// <summary>
+ /// Gets the y coordinate of the entity.
+ /// </summary>
+ public ushort Y
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x0E, out ushort y);
+ return y;
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/MapNpcObj.cs +25 -0
@@ 1,25 @@
+//
+// MapNpcObj.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// Npc NosTale object.
+/// </summary>
+public class MapNpcObj : MapBaseObj
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MapNpcObj"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="mapObjPointer">The pointer to the object.</param>
+ public MapNpcObj(IMemory memory, IntPtr mapObjPointer)
+ : base(memory, mapObjPointer)
+ {
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/MapPlayerObj.cs +45 -0
@@ 1,45 @@
+//
+// MapPlayerObj.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+using System.Text;
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// NosTale Player object.
+/// </summary>
+public class MapPlayerObj : MapBaseObj
+{
+ private readonly IMemory _memory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MapPlayerObj"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="mapObjPointer">The player object pointer.</param>
+ public MapPlayerObj(IMemory memory, IntPtr mapObjPointer)
+ : base(memory, mapObjPointer)
+ {
+ _memory = memory;
+ }
+
+ /// <summary>
+ /// Gets the name of the player.
+ /// </summary>
+ public string? Name
+ {
+ get
+ {
+ _memory.SafeRead(Address + 0x1EC, out int nameAddress);
+ _memory.SafeRead((IntPtr)nameAddress - 4, out int nameLength);
+ byte[] data = new byte[nameLength];
+ _memory.SafeReadRaw((IntPtr)nameAddress, out data, nameLength);
+ return Encoding.ASCII.GetString(data);
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/NostaleList.cs +122 -0
@@ 1,122 @@
+//
+// NostaleList.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections;
+using Reloaded.Memory.Pointers;
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// A class representing a list from nostale.
+/// </summary>
+/// <typeparam name="T">The type.</typeparam>
+public class NostaleList<T> : IEnumerable<T>
+ where T : NostaleObject, new()
+{
+ private readonly IMemory _memory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleList{T}"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="objListPointer">The object list pointer.</param>
+ public NostaleList(IMemory memory, IntPtr objListPointer)
+ {
+ _memory = memory;
+ Address = objListPointer;
+ }
+
+ /// <summary>
+ /// Gets the address.
+ /// </summary>
+ protected IntPtr Address { get; }
+
+ /// <summary>
+ /// Gets the element at the given index.
+ /// </summary>
+ /// <param name="index">The index of the element.</param>
+ /// <exception cref="IndexOutOfRangeException">Thrown if the index is not in the bounds of the array.</exception>
+ public T this[int index]
+ {
+ get
+ {
+ if (index >= Length || index < 0)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ _memory.SafeRead(Address + 0x04, out int arrayAddress);
+ _memory.SafeRead((IntPtr)arrayAddress + (0x04 * index), out int objectAddress);
+
+ return new T
+ {
+ Memory = _memory,
+ Address = (IntPtr)objectAddress
+ };
+ }
+ }
+
+ /// <summary>
+ /// Gets the length of the array.
+ /// </summary>
+ public int Length
+ {
+ get
+ {
+ _memory.SafeRead(Address + 0x08, out int length);
+ return length;
+ }
+ }
+
+ /// <inheritdoc/>
+ public IEnumerator<T> GetEnumerator()
+ {
+ return new NostaleListEnumerator(this);
+ }
+
+ /// <inheritdoc/>
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+
+ private class NostaleListEnumerator : IEnumerator<T>
+ {
+ private readonly NostaleList<T> _list;
+ private int _index;
+
+ public NostaleListEnumerator(NostaleList<T> list)
+ {
+ _index = -1;
+ _list = list;
+ }
+
+ public bool MoveNext()
+ {
+ if (_list.Length > _index + 1)
+ {
+ _index++;
+ return true;
+ }
+
+ return false;
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ }
+
+ public T Current => _list[_index];
+
+ object IEnumerator.Current => Current;
+
+ public void Dispose()
+ {
+ }
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/NostaleObject.cs +43 -0
@@ 1,43 @@
+//
+// NostaleObject.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// A NosTale object base.
+/// </summary>
+public abstract class NostaleObject
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleObject"/> class.
+ /// </summary>
+ internal NostaleObject()
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleObject"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="address">The address in the memory.</param>
+ protected NostaleObject(IMemory memory, IntPtr address)
+ {
+ Memory = memory;
+ Address = address;
+ }
+
+ /// <summary>
+ /// Gets the memory the object is stored in.
+ /// </summary>
+ internal virtual IMemory Memory { get; set; } = null!;
+
+ /// <summary>
+ /// Gets the address of the object.
+ /// </summary>
+ public virtual IntPtr Address { get; internal set; } = IntPtr.Zero;
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/PetManager.cs +51 -0
@@ 1,51 @@
+//
+// PetManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Options;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// NosTale pet manager.
+/// </summary>
+public class PetManager : ControlManager
+{
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PetManager"/> class.
+ /// </summary>
+ public PetManager()
+ : base(null!, IntPtr.Zero)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PetManager"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="petManagerAddress">The pet manager address.</param>
+ public PetManager(IMemory memory, IntPtr petManagerAddress)
+ : base(memory, petManagerAddress)
+ {
+ }
+
+ /// <summary>
+ /// Gets the player object.
+ /// </summary>
+ public MapNpcObj Pet
+ {
+ get
+ {
+ Memory.SafeRead(Address + 0x7C, out int playerAddress);
+ return new MapNpcObj(Memory, (IntPtr)playerAddress);
+ }
+ }
+
+ /// <inheritdoc />
+ public override MapBaseObj Entity => Pet;
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/PetManagerList.cs +54 -0
@@ 1,54 @@
+//
+// PetManagerList.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Extensions;
+using NosSmooth.LocalBinding.Options;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+using SharpDisasm.Udis86;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// NosTale list of <see cref="PetManager"/>.
+/// </summary>
+public class PetManagerList : NostaleList<PetManager>
+{
+ /// <summary>
+ /// Create <see cref="PlayerManager"/> instance.
+ /// </summary>
+ /// <param name="nosBrowserManager">The NosTale process browser.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>The player manager or an error.</returns>
+ public static Result<PetManagerList> Create(NosBrowserManager nosBrowserManager, PetManagerOptions options)
+ {
+ var characterObjectAddress = nosBrowserManager.Scanner.CompiledFindPattern(options.PetManagerListPattern);
+ if (!characterObjectAddress.Found)
+ {
+ return new BindingNotFoundError(options.PetManagerListPattern, "PetManagerList");
+ }
+
+ if (nosBrowserManager.Process.MainModule is null)
+ {
+ return new NotFoundError("Cannot find the main module of the target process.");
+ }
+
+ int staticManagerAddress = (int)nosBrowserManager.Process.MainModule.BaseAddress + characterObjectAddress.Offset;
+ return new PetManagerList(nosBrowserManager.Memory, staticManagerAddress, options.PetManagerListOffsets);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PetManagerList"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="staticPetManagerListAddress">The static pet manager address.</param>
+ /// <param name="staticPetManagerOffsets">The offsets to follow to the pet manager list address.</param>
+ public PetManagerList(IMemory memory, int staticPetManagerListAddress, int[] staticPetManagerOffsets)
+ : base(memory, memory.FollowStaticAddressOffsets(staticPetManagerListAddress, staticPetManagerOffsets))
+ {
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/PlayerManager.cs +94 -0
@@ 1,94 @@
+//
+// PlayerManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Options;
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Extensions;
+using NosSmooth.LocalBinding.Options;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// NosTale player manager.
+/// </summary>
+public class PlayerManager : ControlManager
+{
+ /// <summary>
+ /// Create <see cref="PlayerManager"/> instance.
+ /// </summary>
+ /// <param name="nosBrowserManager">The NosTale process browser.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>The player manager or an error.</returns>
+ public static Result<PlayerManager> Create(NosBrowserManager nosBrowserManager, PlayerManagerOptions options)
+ {
+ var characterObjectAddress = nosBrowserManager.Scanner.CompiledFindPattern(options.PlayerManagerPattern);
+ if (!characterObjectAddress.Found)
+ {
+ return new BindingNotFoundError(options.PlayerManagerPattern, "PlayerManager");
+ }
+
+ if (nosBrowserManager.Process.MainModule is null)
+ {
+ return new NotFoundError("Cannot find the main module of the target process.");
+ }
+
+ var staticAddress = (int)nosBrowserManager.Process.MainModule.BaseAddress + characterObjectAddress.Offset;
+ return new PlayerManager(nosBrowserManager.Memory, staticAddress, options.PlayerManagerOffsets);
+ }
+
+ private readonly IMemory _memory;
+ private readonly int _staticPlayerManagerAddress;
+ private readonly int[] _playerManagerOffsets;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlayerManager"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="staticPlayerManagerAddress">The pointer to the beginning of the player manager structure.</param>
+ /// <param name="playerManagerOffsets">The offsets to get the player manager address from the static one.</param>
+ public PlayerManager(IMemory memory, int staticPlayerManagerAddress, int[] playerManagerOffsets)
+ : base(memory, IntPtr.Zero)
+ {
+ _memory = memory;
+ _staticPlayerManagerAddress = staticPlayerManagerAddress;
+ _playerManagerOffsets = playerManagerOffsets;
+ }
+
+ /// <summary>
+ /// Gets the address to the player manager.
+ /// </summary>
+ public override IntPtr Address => _memory.FollowStaticAddressOffsets
+ (_staticPlayerManagerAddress, _playerManagerOffsets);
+
+ /// <summary>
+ /// Gets the player object.
+ /// </summary>
+ public MapPlayerObj Player
+ {
+ get
+ {
+ _memory.SafeRead(Address + 0x20, out int playerAddress);
+ return new MapPlayerObj(_memory, (IntPtr)playerAddress);
+ }
+ }
+
+ /// <summary>
+ /// Gets the player id.
+ /// </summary>
+ public int PlayerId
+ {
+ get
+ {
+ _memory.SafeRead(Address + 0x24, out int playerId);
+ return playerId;
+ }
+ }
+
+ /// <inheritdoc />
+ public override MapBaseObj Entity => Player;
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalBinding/Structs/SceneManager.cs +160 -0
@@ 1,160 @@
+//
+// SceneManager.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalBinding.Errors;
+using NosSmooth.LocalBinding.Extensions;
+using NosSmooth.LocalBinding.Options;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+
+namespace NosSmooth.LocalBinding.Structs;
+
+/// <summary>
+/// Represents nostale scene manager struct.
+/// </summary>
+public class SceneManager
+{
+ /// <summary>
+ /// Create <see cref="PlayerManager"/> instance.
+ /// </summary>
+ /// <param name="nosBrowserManager">The NosTale process browser.</param>
+ /// <param name="options">The options.</param>
+ /// <returns>The player manager or an error.</returns>
+ public static Result<SceneManager> Create(NosBrowserManager nosBrowserManager, SceneManagerOptions options)
+ {
+ var characterObjectAddress = nosBrowserManager.Scanner.CompiledFindPattern(options.SceneManagerObjectPattern);
+ if (!characterObjectAddress.Found)
+ {
+ return new BindingNotFoundError(options.SceneManagerObjectPattern, "SceneManager");
+ }
+
+ if (nosBrowserManager.Process.MainModule is null)
+ {
+ return new NotFoundError("Cannot find the main module of the target process.");
+ }
+
+ int staticManagerAddress = (int)nosBrowserManager.Process.MainModule.BaseAddress + characterObjectAddress.Offset;
+ return new SceneManager(nosBrowserManager.Memory, staticManagerAddress, options.SceneManagerOffsets);
+ }
+
+ private readonly int[] _sceneManagerOffsets;
+ private readonly IMemory _memory;
+ private readonly int _staticSceneManagerAddress;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SceneManager"/> class.
+ /// </summary>
+ /// <param name="memory">The memory.</param>
+ /// <param name="staticSceneManagerAddress">The pointer to the scene manager.</param>
+ /// <param name="sceneManagerOffsets">The offsets from the static scene manager address.</param>
+ public SceneManager(IMemory memory, int staticSceneManagerAddress, int[] sceneManagerOffsets)
+ {
+ _memory = memory;
+ _staticSceneManagerAddress = staticSceneManagerAddress;
+ _sceneManagerOffsets = sceneManagerOffsets;
+ }
+
+ /// <summary>
+ /// Gets the address of the scene manager.
+ /// </summary>
+ public IntPtr Address => _memory.FollowStaticAddressOffsets(_staticSceneManagerAddress, _sceneManagerOffsets);
+
+ /// <summary>
+ /// Gets the player list.
+ /// </summary>
+ public NostaleList<MapBaseObj> PlayerList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0xC));
+
+ /// <summary>
+ /// Gets the monster list.
+ /// </summary>
+ public NostaleList<MapBaseObj> MonsterList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0x10));
+
+ /// <summary>
+ /// Gets the npc list.
+ /// </summary>
+ public NostaleList<MapBaseObj> NpcList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0x14));
+
+ /// <summary>
+ /// Gets the item list.
+ /// </summary>
+ public NostaleList<MapBaseObj> ItemList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0x18));
+
+ /// <summary>
+ /// Gets the entity that is currently being followed by the player.
+ /// </summary>
+ public MapBaseObj? FollowEntity
+ {
+ get
+ {
+ var ptr = ReadPtr(Address + 0x48);
+ if (ptr == IntPtr.Zero)
+ {
+ return null;
+ }
+
+ return new MapBaseObj(_memory, ptr);
+ }
+ }
+
+ /// <summary>
+ /// Gets the lock on target marked address.
+ /// </summary>
+ public IntPtr LockOnTargetMarkedAddress
+ {
+ get
+ {
+ var ptr = ReadPtr(Address + 0x1C);
+ ptr = ReadPtr(ptr + 0x04);
+ ptr = ReadPtr(ptr + 0x00);
+ return ptr;
+ }
+ }
+
+ private IntPtr ReadPtr(IntPtr ptr)
+ {
+ _memory.Read(ptr, out int read);
+ return (IntPtr)read;
+ }
+
+ /// <summary>
+ /// Find the given entity address.
+ /// </summary>
+ /// <param name="id">The id of the entity.</param>
+ /// <returns>The pointer to the entity or an error.</returns>
+ public Result<MapBaseObj?> FindEntity(int id)
+ {
+ if (id == 0)
+ {
+ return Result<MapBaseObj?>.FromSuccess(null);
+ }
+
+ var item = ItemList.FirstOrDefault(x => x.Id == id);
+ if (item is not null)
+ {
+ return item;
+ }
+
+ var monster = MonsterList.FirstOrDefault(x => x.Id == id);
+ if (monster is not null)
+ {
+ return monster;
+ }
+
+ var npc = NpcList.FirstOrDefault(x => x.Id == id);
+ if (npc is not null)
+ {
+ return npc;
+ }
+
+ var player = PlayerList.FirstOrDefault(x => x.Id == id);
+ if (player is not null)
+ {
+ return player;
+ }
+
+ return new NotFoundError($"Could not find entity with id {id}");
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/ControlCommandWalkHandler.cs +145 -0
@@ 1,145 @@
+//
+// ControlCommandWalkHandler.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands.Control;
+using NosSmooth.Core.Commands.Walking;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding.Structs;
+using NosSmooth.LocalClient.CommandHandlers.Walk.Errors;
+using Remora.Results;
+
+namespace NosSmooth.LocalClient.CommandHandlers.Walk;
+
+/// <summary>
+/// Handler for control manager walk command.
+/// </summary>
+internal class ControlCommandWalkHandler
+{
+ private readonly INostaleClient _nostaleClient;
+ private readonly Func<ushort, ushort, Result<bool>> _walkFunction;
+ private readonly ControlManager _controlManager;
+ private readonly WalkCommandHandlerOptions _options;
+
+ private ushort _x;
+ private ushort _y;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ControlCommandWalkHandler"/> class.
+ /// </summary>
+ /// <param name="nostaleClient">The nostale client.</param>
+ /// <param name="walkFunction">The walk function.</param>
+ /// <param name="controlManager">The control manager.</param>
+ /// <param name="options">The options.</param>
+ public ControlCommandWalkHandler
+ (
+ INostaleClient nostaleClient,
+ Func<ushort, ushort, Result<bool>> walkFunction,
+ ControlManager controlManager,
+ WalkCommandHandlerOptions options
+ )
+ {
+ _nostaleClient = nostaleClient;
+ _walkFunction = walkFunction;
+ _controlManager = controlManager;
+ _options = options;
+ }
+
+ /// <summary>
+ /// Handle walk take control command.
+ /// </summary>
+ /// <param name="x">The x coordinate.</param>
+ /// <param name="y">The y coordinate.</param>
+ /// <param name="command">The take control command.</param>
+ /// <param name="groupName">The name of the take control group.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A <see cref="Task{TResult}"/> representing the result of the asynchronous operation.</returns>
+ public async Task<Result> HandleCommand(ushort x, ushort y, ITakeControlCommand command, string groupName, CancellationToken ct = default)
+ {
+ _x = x;
+ _y = y;
+
+ using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
+ WalkUnfinishedReason? reason = null;
+ var takeControlCommand = command.CreateTakeControl
+ (
+ groupName,
+ WalkGrantedCallback,
+ (r) =>
+ {
+ reason = r switch
+ {
+ ControlCancelReason.AnotherTask => WalkUnfinishedReason.AnotherTask,
+ ControlCancelReason.UserAction => WalkUnfinishedReason.UserAction,
+ _ => WalkUnfinishedReason.Unknown
+ };
+ return Task.FromResult(Result.FromSuccess());
+ }
+ );
+
+ var commandResult = await _nostaleClient.SendCommandAsync(takeControlCommand, ct);
+ if (!commandResult.IsSuccess)
+ {
+ return commandResult;
+ }
+
+ if (reason is null && !IsAt(x, y))
+ {
+ reason = WalkUnfinishedReason.PathNotFound;
+ }
+
+ if (reason is null)
+ {
+ return Result.FromSuccess();
+ }
+
+ return new WalkNotFinishedError
+ (
+ _controlManager.X,
+ _controlManager.Y,
+ (WalkUnfinishedReason)reason
+ );
+ }
+
+ private bool IsAtTarget()
+ {
+ return _controlManager.TargetX == _controlManager.Entity.X
+ && _controlManager.TargetY == _controlManager.Entity.Y;
+ }
+
+ private bool IsAt(ushort x, ushort y)
+ {
+ return _controlManager.Entity.X == x && _controlManager.Entity.Y == y;
+ }
+
+ private async Task<Result> WalkGrantedCallback(CancellationToken ct)
+ {
+ var result = _walkFunction(_x, _y);
+ if (!result.IsSuccess)
+ {
+ return Result.FromError(result);
+ }
+
+ while (!ct.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(_options.CheckDelay, ct);
+ }
+ catch
+ {
+ // ignored
+ }
+
+ if (IsAtTarget() || IsAt(_x, _y))
+ {
+ return Result.FromSuccess();
+ }
+ }
+
+ return Result.FromSuccess(); // cancellation is handled in cancellation callback.
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs +18 -0
@@ 1,18 @@
+//
+// WalkNotFinishedError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.LocalClient.CommandHandlers.Walk.Errors;
+
+/// <summary>
+/// Represents an error that can be returned from walk command handler.
+/// </summary>
+/// <param name="X">The x coordinate where the player is. (if known)</param>
+/// <param name="Y">The y coordinate where the player is. (if known)</param>
+/// <param name="Reason"></param>
+public record WalkNotFinishedError(int? X, int? Y, WalkUnfinishedReason Reason)
+ : ResultError($"Could not finish the walk to {X} {Y}, because {Reason}");<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PetWalkCommandHandler.cs +75 -0
@@ 1,75 @@
+//
+// PetWalkCommandHandler.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Options;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Commands.Control;
+using NosSmooth.Core.Commands.Walking;
+using NosSmooth.LocalBinding.Objects;
+using Remora.Results;
+
+namespace NosSmooth.LocalClient.CommandHandlers.Walk;
+
+/// <summary>
+/// Handles <see cref="PetWalkCommand"/>.
+/// </summary>
+public class PetWalkCommandHandler : ICommandHandler<PetWalkCommand>
+{
+ /// <summary>
+ /// Group that is used for <see cref="TakeControlCommand"/>.
+ /// </summary>
+ public const string PetWalkControlGroup = "PetWalk";
+
+ private readonly PetManagerBinding _petManagerBinding;
+ private readonly INostaleClient _nostaleClient;
+ private readonly WalkCommandHandlerOptions _options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PetWalkCommandHandler"/> class.
+ /// </summary>
+ /// <param name="petManagerBinding">The character object binding.</param>
+ /// <param name="nostaleClient">The nostale client.</param>
+ /// <param name="options">The options.</param>
+ public PetWalkCommandHandler
+ (
+ PetManagerBinding petManagerBinding,
+ INostaleClient nostaleClient,
+ IOptions<WalkCommandHandlerOptions> options
+ )
+ {
+ _options = options.Value;
+ _petManagerBinding = petManagerBinding;
+ _nostaleClient = nostaleClient;
+ }
+
+ /// <inheritdoc/>
+ public async Task<Result> HandleCommand(PetWalkCommand command, CancellationToken ct = default)
+ {
+ if (_petManagerBinding.PetManagerList.Length < command.PetSelector + 1)
+ {
+ return new NotFoundError("Could not find the pet using the given selector.");
+ }
+ var petManager = _petManagerBinding.PetManagerList[command.PetSelector];
+
+ var handler = new ControlCommandWalkHandler
+ (
+ _nostaleClient,
+ (x, y) => _petManagerBinding.PetWalk(command.PetSelector, x, y),
+ petManager,
+ _options
+ );
+
+ return await handler.HandleCommand
+ (
+ command.TargetX,
+ command.TargetY,
+ command,
+ PetWalkControlGroup + "_" + command.PetSelector,
+ ct
+ );
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PlayerWalkCommandHandler.cs +71 -0
@@ 1,71 @@
+//
+// PlayerWalkCommandHandler.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Options;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Commands.Control;
+using NosSmooth.Core.Commands.Walking;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding.Objects;
+using NosSmooth.LocalClient.CommandHandlers.Walk.Errors;
+using Remora.Results;
+
+namespace NosSmooth.LocalClient.CommandHandlers.Walk;
+
+/// <summary>
+/// Handles <see cref="PlayerWalkCommand"/>.
+/// </summary>
+public class PlayerWalkCommandHandler : ICommandHandler<PlayerWalkCommand>
+{
+ /// <summary>
+ /// Group that is used for <see cref="TakeControlCommand"/>.
+ /// </summary>
+ public const string PlayerWalkControlGroup = "PlayerWalk";
+
+ private readonly PlayerManagerBinding _playerManagerBinding;
+ private readonly INostaleClient _nostaleClient;
+ private readonly WalkCommandHandlerOptions _options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PlayerWalkCommandHandler"/> class.
+ /// </summary>
+ /// <param name="playerManagerBinding">The character object binding.</param>
+ /// <param name="nostaleClient">The nostale client.</param>
+ /// <param name="options">The options.</param>
+ public PlayerWalkCommandHandler
+ (
+ PlayerManagerBinding playerManagerBinding,
+ INostaleClient nostaleClient,
+ IOptions<WalkCommandHandlerOptions> options
+ )
+ {
+ _options = options.Value;
+ _playerManagerBinding = playerManagerBinding;
+ _nostaleClient = nostaleClient;
+ }
+
+ /// <inheritdoc/>
+ public async Task<Result> HandleCommand(PlayerWalkCommand command, CancellationToken ct = default)
+ {
+ var handler = new ControlCommandWalkHandler
+ (
+ _nostaleClient,
+ (x, y) => _playerManagerBinding.Walk(x, y),
+ _playerManagerBinding.PlayerManager,
+ _options
+ );
+
+ return await handler.HandleCommand
+ (
+ command.TargetX,
+ command.TargetY,
+ command,
+ PlayerWalkControlGroup,
+ ct
+ );
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs +23 -0
@@ 1,23 @@
+//
+// WalkCommandHandlerOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.LocalClient.CommandHandlers.Walk
+{
+ /// <summary>
+ /// Options for <see cref="PlayerWalkCommandHandler"/>.
+ /// </summary>
+ public class WalkCommandHandlerOptions
+ {
+ /// <summary>
+ /// The command handler sleeps for this duration, then checks for new info. Unit is milliseconds.
+ /// </summary>
+ /// <remarks>
+ /// The operation is done with a cancellation token, if there is an outer event, then it can be faster.
+ /// Walk function is called again as well after this delay.
+ /// </remarks>
+ public int CheckDelay { get; set; } = 50;
+ }
+}
A => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/WalkUnfinishedReason.cs +36 -0
@@ 1,36 @@
+//
+// WalkUnfinishedReason.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.LocalClient.CommandHandlers.Walk;
+
+/// <summary>
+/// Reason for not finishing a walk.
+/// </summary>
+public enum WalkUnfinishedReason
+{
+ /// <summary>
+ /// There was an unknown unfinished reason.
+ /// </summary>
+ Unknown,
+
+ /// <summary>
+ /// The client could not find path to the given location.
+ /// </summary>
+ /// <remarks>
+ /// The user walked just some part of the path.
+ /// </remarks>
+ PathNotFound,
+
+ /// <summary>
+ /// The user has took an action that has cancelled the walk.
+ /// </summary>
+ UserAction,
+
+ /// <summary>
+ /// There was another walk action that cancelled this one.
+ /// </summary>
+ AnotherTask
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs +51 -0
@@ 1,51 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding.Extensions;
+using NosSmooth.LocalClient.CommandHandlers.Walk;
+
+namespace NosSmooth.LocalClient.Extensions;
+
+/// <summary>
+/// Contains extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds <see cref="NostaleLocalClient"/> along with all core dependencies.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddLocalClient(this IServiceCollection serviceCollection)
+ {
+ serviceCollection.AddNostaleCore();
+ serviceCollection.AddNostaleBindings();
+ serviceCollection
+ .AddTakeControlCommand()
+ .AddCommandHandler<PlayerWalkCommandHandler>()
+ .AddCommandHandler<PetWalkCommandHandler>();
+ serviceCollection.TryAddSingleton<NostaleLocalClient>();
+ serviceCollection.TryAddSingleton<INostaleClient>(p => p.GetRequiredService<NostaleLocalClient>());
+
+ return serviceCollection;
+ }
+
+ /// <summary>
+ /// Adds packet interceptor.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <typeparam name="TInterceptor">The type of the interceptor.</typeparam>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddPacketInterceptor<TInterceptor>(this IServiceCollection serviceCollection)
+ where TInterceptor : class, IPacketInterceptor
+ {
+ return serviceCollection.AddSingleton<IPacketInterceptor, TInterceptor>();
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/IPacketInterceptor.cs +28 -0
@@ 1,28 @@
+//
+// IPacketInterceptor.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.LocalClient;
+
+/// <summary>
+/// Used for intercepting packet communication,
+/// changing the contents of the packets and/or cancelling them altogether.
+/// </summary>
+public interface IPacketInterceptor
+{
+ /// <summary>
+ /// Intercept the given packet.
+ /// </summary>
+ /// <param name="packet">The packet itself, if it is changed, the modified packet will be sent.</param>
+ /// <returns>Whether to send the packet to the server.</returns>
+ public bool InterceptSend(ref string packet);
+
+ /// <summary>
+ /// Intercept the given packet.
+ /// </summary>
+ /// <param name="packet">The packet itself, if it is changed, the modified packet will be received.</param>
+ /// <returns>Whether to receive the packet by the client.</returns>
+ public bool InterceptReceive(ref string packet);
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/IsExternalInit.cs +15 -0
@@ 1,15 @@
+//
+// IsExternalInit.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace System.Runtime.CompilerServices
+{
+ /// <summary>
+ /// Dummy.
+ /// </summary>
+ public class IsExternalInit
+ {
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/LocalClientOptions.cs +18 -0
@@ 1,18 @@
+//
+// LocalClientOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.LocalClient;
+
+/// <summary>
+/// Options for <see cref="NostaleLocalClient"/>.
+/// </summary>
+public class LocalClientOptions
+{
+ /// <summary>
+ /// Gets or sets whether the interception of packets should be allowed.
+ /// </summary>
+ public bool AllowIntercept { get; set; }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj +21 -0
@@ 1,21 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <LangVersion>10</LangVersion>
+ <TargetFrameworks>net6.0;netstandard2.1</TargetFrameworks>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\NosSmooth.LocalBinding\NosSmooth.LocalBinding.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
+ <PackageReference Include="NosSmooth.Core" Version="1.0.0" />
+ <PackageReference Include="Reloaded.Hooks" Version="3.5.0" />
+ <PackageReference Include="Reloaded.Memory.Sigscan" Version="1.2.1" />
+ </ItemGroup>
+
+</Project>
A => src/Core/NosSmooth.LocalClient/NostaleLocalClient.cs +216 -0
@@ 1,216 @@
+//
+// NostaleLocalClient.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Commands.Control;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.LocalBinding.Objects;
+using NosSmooth.LocalBinding.Structs;
+using NosSmooth.Packets;
+using NosSmooth.Packets.Errors;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using Remora.Results;
+
+namespace NosSmooth.LocalClient;
+
+/// <summary>
+/// The local nostale client.
+/// </summary>
+/// <remarks>
+/// Client used for living in the same process as NostaleClientX.exe.
+/// It hooks the send and receive packet methods.
+/// </remarks>
+public class NostaleLocalClient : BaseNostaleClient
+{
+ private readonly NetworkBinding _networkBinding;
+ private readonly PlayerManagerBinding _playerManagerBinding;
+ private readonly ControlCommands _controlCommands;
+ private readonly IPacketSerializer _packetSerializer;
+ private readonly IPacketHandler _packetHandler;
+ private readonly ILogger _logger;
+ private readonly IServiceProvider _provider;
+ private readonly LocalClientOptions _options;
+ private CancellationToken? _stopRequested;
+ private IPacketInterceptor? _interceptor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleLocalClient"/> class.
+ /// </summary>
+ /// <param name="networkBinding">The network binding.</param>
+ /// <param name="playerManagerBinding">The player manager binding.</param>
+ /// <param name="controlCommands">The control commands.</param>
+ /// <param name="commandProcessor">The command processor.</param>
+ /// <param name="packetSerializer">The packet serializer.</param>
+ /// <param name="packetHandler">The packet handler.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="options">The options for the client.</param>
+ /// <param name="provider">The dependency injection provider.</param>
+ public NostaleLocalClient
+ (
+ NetworkBinding networkBinding,
+ PlayerManagerBinding playerManagerBinding,
+ ControlCommands controlCommands,
+ CommandProcessor commandProcessor,
+ IPacketSerializer packetSerializer,
+ IPacketHandler packetHandler,
+ ILogger<NostaleLocalClient> logger,
+ IOptions<LocalClientOptions> options,
+ IServiceProvider provider
+ )
+ : base(commandProcessor, packetSerializer)
+ {
+ _options = options.Value;
+ _networkBinding = networkBinding;
+ _playerManagerBinding = playerManagerBinding;
+ _controlCommands = controlCommands;
+ _packetSerializer = packetSerializer;
+ _packetHandler = packetHandler;
+ _logger = logger;
+ _provider = provider;
+ }
+
+ /// <inheritdoc />
+ public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
+ {
+ _stopRequested = stopRequested;
+ _logger.LogInformation("Starting local client");
+ _networkBinding.PacketSend += SendCallback;
+ _networkBinding.PacketReceive += ReceiveCallback;
+
+ _playerManagerBinding.FollowEntityCall += FollowEntity;
+ _playerManagerBinding.WalkCall += Walk;
+
+ try
+ {
+ await Task.Delay(-1, stopRequested);
+ }
+ catch
+ {
+ // ignored
+ }
+
+ _networkBinding.PacketSend -= SendCallback;
+ _networkBinding.PacketReceive -= ReceiveCallback;
+ _playerManagerBinding.FollowEntityCall -= FollowEntity;
+ _playerManagerBinding.WalkCall -= Walk;
+
+ return Result.FromSuccess();
+ }
+
+ /// <inheritdoc />
+ public override Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
+ {
+ ReceivePacket(packetString);
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public override Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
+ {
+ SendPacket(packetString);
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ private bool ReceiveCallback(string packet)
+ {
+ bool accepted = true;
+ if (_options.AllowIntercept)
+ {
+ if (_interceptor is null)
+ {
+ _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
+ }
+
+ accepted = _interceptor.InterceptReceive(ref packet);
+ }
+
+ Task.Run(async () => await ProcessPacketAsync(PacketSource.Server, packet));
+
+ return accepted;
+ }
+
+ private bool SendCallback(string packet)
+ {
+ bool accepted = true;
+ if (_options.AllowIntercept)
+ {
+ if (_interceptor is null)
+ {
+ _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
+ }
+
+ accepted = _interceptor.InterceptSend(ref packet);
+ }
+
+ Task.Run(async () => await ProcessPacketAsync(PacketSource.Client, packet));
+
+ return accepted;
+ }
+
+ private void SendPacket(string packetString)
+ {
+ _networkBinding.SendPacket(packetString);
+ _logger.LogDebug($"Sending client packet {packetString}");
+ }
+
+ private void ReceivePacket(string packetString)
+ {
+ _networkBinding.ReceivePacket(packetString);
+ _logger.LogDebug($"Receiving client packet {packetString}");
+ }
+
+ private async Task ProcessPacketAsync(PacketSource type, string packetString)
+ {
+ var packet = _packetSerializer.Deserialize(packetString, type);
+ if (!packet.IsSuccess)
+ {
+ if (packet.Error is not PacketConverterNotFoundError)
+ {
+ _logger.LogWarning("Could not parse {Packet}. Reason:", packetString);
+ _logger.LogResultError(packet);
+ }
+
+ packet = new ParsingFailedPacket(packet, packetString);
+ }
+
+ Result result;
+ if (type == PacketSource.Server)
+ {
+ result = await _packetHandler.HandleReceivedPacketAsync
+ (packet.Entity, packetString, _stopRequested ?? default);
+ }
+ else
+ {
+ result = await _packetHandler.HandleSentPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
+ }
+
+ if (!result.IsSuccess)
+ {
+ _logger.LogError("There was an error whilst handling packet");
+ _logger.LogResultError(result);
+ }
+ }
+
+ private bool FollowEntity(MapBaseObj? obj)
+ {
+ Task.Run
+ (
+ async () => await _controlCommands.CancelAsync
+ (ControlCommandsFilter.UserCancellable, false, (CancellationToken)_stopRequested!)
+ );
+ return true;
+ }
+
+ private bool Walk(ushort x, ushort y)
+ {
+ return _controlCommands.AllowUserActions;
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/NostaleWindow.cs +58 -0
@@ 1,58 @@
+//
+// NostaleWindow.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.LocalClient.Utils;
+
+namespace NosSmooth.LocalClient;
+
+/// <summary>
+/// Represents window of nostale client.
+/// </summary>
+public class NostaleWindow
+{
+ private const int WM_KEYDOWN = 0x0100;
+ private const int WM_KEYUP = 0x0101;
+ private const int WM_CHAR = 0x0102;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleWindow"/> class.
+ /// </summary>
+ /// <param name="handle">The handle of the window.</param>
+ public NostaleWindow(IntPtr handle) => Handle = handle;
+
+ /// <summary>
+ /// Gets the window handle.
+ /// </summary>
+ public IntPtr Handle { get; }
+
+ /// <summary>
+ /// Changes the title of the window.
+ /// </summary>
+ /// <param name="name">The new name of the window.</param>
+ public void Rename(string name)
+ {
+ User32.SetWindowText(Handle, name);
+ }
+
+ /// <summary>
+ /// Bring the window to front.
+ /// </summary>
+ public void BringToFront()
+ {
+ User32.SetForegroundWindow(Handle);
+ }
+
+ /// <summary>
+ /// Send the given key to the window.
+ /// </summary>
+ /// <param name="key">The id of the key.</param>
+ public void SendKey(uint key)
+ {
+ User32.PostMessage(Handle, WM_KEYDOWN, key, 0);
+ User32.PostMessage(Handle, WM_CHAR, key, 0);
+ User32.PostMessage(Handle, WM_KEYUP, key, 0);
+ }
+}<
\ No newline at end of file
A => src/Core/NosSmooth.LocalClient/Utils/User32.cs +88 -0
@@ 1,88 @@
+//
+// User32.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Text;
+#pragma warning disable CS1591
+
+namespace NosSmooth.LocalClient.Utils;
+
+/// <summary>
+/// Represents class with extern calls to user32.dll.
+/// </summary>
+[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600", MessageId = "Elements should be documented", Justification = "user32.dll methods do not need documentation, it can be found on msdn.")]
+public class User32
+{
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
+
+ [DllImport("user32.dll")]
+ public static extern int PostMessage(IntPtr hWnd, int uMsg, uint wParam, uint lParam);
+
+ [DllImport("user32.dll")]
+ public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
+
+ [DllImport("user32.dll")]
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
+
+ [DllImport("user32.dll")]
+ public static extern bool SetWindowText(IntPtr hWnd, string text);
+
+ [DllImport("user32.dll")]
+ public static extern int EnumWindows(EnumWindowsProc callback, IntPtr lParam);
+
+ [DllImport("user32.dll")]
+ public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ public static extern int GetWindowTextLength(IntPtr hWnd);
+
+ [DllImport("user32.dll")]
+ public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
+
+ /// <summary>
+ /// Finds all windows with the given title matching.
+ /// </summary>
+ /// <param name="title">The title to match.</param>
+ /// <returns>The matched windows.</returns>
+ public static IEnumerable<IntPtr> FindWindowsWithTitle(string title)
+ {
+ var windows = new List<IntPtr>();
+ EnumWindows(
+ (hWnd, lParam) =>
+ {
+ string windowTitle = GetWindowTitle(hWnd);
+ if (windowTitle.Equals(title))
+ {
+ windows.Add(hWnd);
+ }
+
+ return true;
+ },
+ IntPtr.Zero
+ );
+
+ return windows;
+ }
+
+ /// <summary>
+ /// Returns the title of a window.
+ /// </summary>
+ /// <param name="hWnd">The handle of the window.</param>
+ /// <returns>The title of the window.</returns>
+ public static string GetWindowTitle(IntPtr hWnd)
+ {
+ int size = GetWindowTextLength(hWnd);
+ if (size == 0)
+ {
+ return string.Empty;
+ }
+
+ var sb = new StringBuilder(size + 1);
+ GetWindowText(hWnd, sb, sb.Capacity);
+ return sb.ToString();
+ }
+}<
\ No newline at end of file
A => src/Extensions/NosSmooth.ChatCommands/ChatCommandInterceptor.cs +92 -0
@@ 1,92 @@
+//
+// ChatCommandInterceptor.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reflection.Emit;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalClient;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+using Remora.Commands.Services;
+
+namespace NosSmooth.ChatCommands;
+
+/// <summary>
+/// Handles commands in the chat.
+/// </summary>
+public class ChatCommandInterceptor : IPacketInterceptor
+{
+ private readonly CommandService _commandService;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly FeedbackService _feedbackService;
+ private readonly ILogger<ChatCommandInterceptor> _logger;
+ private readonly ChatCommandsOptions _options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ChatCommandInterceptor"/> class.
+ /// </summary>
+ /// <param name="options">The options.</param>
+ /// <param name="commandService">The command service.</param>
+ /// <param name="serviceProvider">The services.</param>
+ /// <param name="feedbackService">The feedback service.</param>
+ /// <param name="logger">The logger.</param>
+ public ChatCommandInterceptor
+ (
+ IOptions<ChatCommandsOptions> options,
+ CommandService commandService,
+ IServiceProvider serviceProvider,
+ FeedbackService feedbackService,
+ ILogger<ChatCommandInterceptor> logger
+ )
+ {
+ _commandService = commandService;
+ _serviceProvider = serviceProvider;
+ _feedbackService = feedbackService;
+ _logger = logger;
+ _options = options.Value;
+ }
+
+ /// <inheritdoc />
+ public bool InterceptSend(ref string packet)
+ {
+ ReadOnlySpan<char> span = packet;
+ if (span.StartsWith("say ") && span.Slice(4).StartsWith(_options.Prefix))
+ {
+ var command = span.Slice(4 + _options.Prefix.Length).ToString();
+ Task.Run(async () => await ExecuteCommand(command));
+ return false;
+ }
+
+ return true;
+ }
+
+ /// <inheritdoc />
+ public bool InterceptReceive(ref string packet)
+ {
+ return true;
+ }
+
+ private async Task ExecuteCommand(string command)
+ {
+ var preparedResult = await _commandService.TryPrepareCommandAsync(command, _serviceProvider);
+ if (!preparedResult.IsSuccess)
+ {
+ _logger.LogError($"Could not prepare \"{command}\"");
+ _logger.LogResultError(preparedResult);
+ await _feedbackService.SendErrorMessageAsync($"Could not prepare the given command. {preparedResult.Error.Message}");
+ }
+
+ var executeResult = await _commandService.TryExecuteAsync(preparedResult.Entity, _serviceProvider);
+ if (!executeResult.IsSuccess)
+ {
+ _logger.LogError($"Could not execute \"{command}\"");
+ _logger.LogResultError(executeResult);
+ await _feedbackService.SendErrorMessageAsync($"Could not execute the given command. {executeResult.Error.Message}");
+ }
+ }
+}<
\ No newline at end of file
A => src/Extensions/NosSmooth.ChatCommands/ChatCommandsOptions.cs +18 -0
@@ 1,18 @@
+//
+// ChatCommandsOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.ChatCommands;
+
+/// <summary>
+/// Options for <see cref="ChatCommandInterceptor"/>.
+/// </summary>
+public class ChatCommandsOptions
+{
+ /// <summary>
+ /// Gets or sets the command prefix.
+ /// </summary>
+ public string Prefix { get; set; } = "#";
+}<
\ No newline at end of file
A => src/Extensions/NosSmooth.ChatCommands/FeedbackService.cs +68 -0
@@ 1,68 @@
+//
+// FeedbackService.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+using Remora.Results;
+
+namespace NosSmooth.ChatCommands;
+
+/// <summary>
+/// Feedback for chat commands.
+/// </summary>
+public class FeedbackService
+{
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FeedbackService"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ public FeedbackService(INostaleClient client)
+ {
+ _client = client;
+
+ }
+
+ /// <summary>
+ /// Send message error.
+ /// </summary>
+ /// <param name="message">The message to send.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendErrorMessageAsync(string message, CancellationToken ct = default)
+ => SendMessageAsync(message, SayColor.Red, ct);
+
+ /// <summary>
+ /// Send message success.
+ /// </summary>
+ /// <param name="message">The message to send.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendSuccessMessageAsync(string message, CancellationToken ct = default)
+ => SendMessageAsync(message, SayColor.Green, ct);
+
+ /// <summary>
+ /// Send message info.
+ /// </summary>
+ /// <param name="message">The message to send.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendInfoMessageAsync(string message, CancellationToken ct = default)
+ => SendMessageAsync(message, SayColor.Default, ct);
+
+ /// <summary>
+ /// Send message with the given color.
+ /// </summary>
+ /// <param name="message">The message to send.</param>
+ /// <param name="color">The color.</param>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> SendMessageAsync(string message, SayColor color, CancellationToken ct = default)
+ => _client.ReceivePacketAsync(new SayPacket(EntityType.Map, 0, color, message), ct);
+}<
\ No newline at end of file
A => src/Extensions/NosSmooth.ChatCommands/NosSmooth.ChatCommands.csproj +18 -0
@@ 1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Remora.Commands" Version="9.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
+ </ItemGroup>
+
+</Project>
A => src/Extensions/NosSmooth.ChatCommands/ServiceCollectionExtensions.cs +36 -0
@@ 1,36 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.LocalClient;
+using NosSmooth.LocalClient.Extensions;
+using Remora.Commands.Extensions;
+
+namespace NosSmooth.ChatCommands;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds NosTale commands and the interceptor to execute commands with.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <param name="prefix">The prefix for the commands.</param>
+ /// <returns>The collection.</returns>
+ public static IServiceCollection AddNostaleChatCommands(this IServiceCollection serviceCollection, string prefix = "#")
+ {
+ serviceCollection
+ .Configure<ChatCommandsOptions>((o) => o.Prefix = prefix);
+
+ return serviceCollection
+ .AddCommands()
+ .Configure<LocalClientOptions>(o => o.AllowIntercept = true)
+ .AddSingleton<FeedbackService>()
+ .AddPacketInterceptor<ChatCommandInterceptor>();
+ }
+}<
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/NosSmooth.Inject.vcxproj +174 -0
@@ 1,174 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup Label="ProjectConfigurations">
+ <ProjectConfiguration Include="Debug|Win32">
+ <Configuration>Debug</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|Win32">
+ <Configuration>Release</Configuration>
+ <Platform>Win32</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Debug|x64">
+ <Configuration>Debug</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ <ProjectConfiguration Include="Release|x64">
+ <Configuration>Release</Configuration>
+ <Platform>x64</Platform>
+ </ProjectConfiguration>
+ </ItemGroup>
+ <PropertyGroup Label="Globals">
+ <VCProjectVersion>16.0</VCProjectVersion>
+ <Keyword>Win32Proj</Keyword>
+ <ProjectGuid>{ca2873d8-bd0b-4583-818d-b94a3c2abba3}</ProjectGuid>
+ <RootNamespace>NosSmoothInject</RootNamespace>
+ <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>true</UseDebugLibraries>
+ <PlatformToolset>v143</PlatformToolset>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>false</UseDebugLibraries>
+ <PlatformToolset>v143</PlatformToolset>
+ <WholeProgramOptimization>true</WholeProgramOptimization>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>true</UseDebugLibraries>
+ <PlatformToolset>v143</PlatformToolset>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+ <ConfigurationType>DynamicLibrary</ConfigurationType>
+ <UseDebugLibraries>false</UseDebugLibraries>
+ <PlatformToolset>v143</PlatformToolset>
+ <WholeProgramOptimization>true</WholeProgramOptimization>
+ <CharacterSet>Unicode</CharacterSet>
+ </PropertyGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+ <ImportGroup Label="ExtensionSettings">
+ </ImportGroup>
+ <ImportGroup Label="Shared">
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+ <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+ </ImportGroup>
+ <PropertyGroup Label="UserMacros" />
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+ <LinkIncremental>true</LinkIncremental>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <LinkIncremental>false</LinkIncremental>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+ <LinkIncremental>true</LinkIncremental>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+ <LinkIncremental>false</LinkIncremental>
+ </PropertyGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+ <ClCompile>
+ <WarningLevel>Level3</WarningLevel>
+ <SDLCheck>true</SDLCheck>
+ <PreprocessorDefinitions>WIN32;_DEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <PrecompiledHeader>NotUsing</PrecompiledHeader>
+ <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <EnableUAC>false</EnableUAC>
+ <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+ <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+ <ClCompile>
+ <WarningLevel>Level3</WarningLevel>
+ <FunctionLevelLinking>true</FunctionLevelLinking>
+ <IntrinsicFunctions>true</IntrinsicFunctions>
+ <SDLCheck>true</SDLCheck>
+ <PreprocessorDefinitions>WIN32;NDEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <PrecompiledHeader>NotUsing</PrecompiledHeader>
+ <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <EnableCOMDATFolding>true</EnableCOMDATFolding>
+ <OptimizeReferences>true</OptimizeReferences>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <EnableUAC>false</EnableUAC>
+ <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+ <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+ <ClCompile>
+ <WarningLevel>Level3</WarningLevel>
+ <SDLCheck>true</SDLCheck>
+ <PreprocessorDefinitions>_DEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <PrecompiledHeader>NotUsing</PrecompiledHeader>
+ <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <EnableUAC>false</EnableUAC>
+ <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+ <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+ <ClCompile>
+ <WarningLevel>Level3</WarningLevel>
+ <FunctionLevelLinking>true</FunctionLevelLinking>
+ <IntrinsicFunctions>true</IntrinsicFunctions>
+ <SDLCheck>true</SDLCheck>
+ <PreprocessorDefinitions>NDEBUG;NOSSMOOTHINJECT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+ <ConformanceMode>true</ConformanceMode>
+ <PrecompiledHeader>NotUsing</PrecompiledHeader>
+ <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
+ </ClCompile>
+ <Link>
+ <SubSystem>Windows</SubSystem>
+ <EnableCOMDATFolding>true</EnableCOMDATFolding>
+ <OptimizeReferences>true</OptimizeReferences>
+ <GenerateDebugInformation>true</GenerateDebugInformation>
+ <EnableUAC>false</EnableUAC>
+ <AdditionalLibraryDirectories>%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
+ <AdditionalDependencies>nethost.lib;%(AdditionalDependencies)</AdditionalDependencies>
+ </Link>
+ </ItemDefinitionGroup>
+ <ItemGroup>
+ <ClCompile Include="dllmain.cpp" />
+ <ClCompile Include="nossmooth.cpp" />
+ </ItemGroup>
+ <ItemGroup>
+ <ClInclude Include="coreclr_delegates.h" />
+ <ClInclude Include="nethost.h" />
+ <ClInclude Include="hostfxr.h" />
+ <ClInclude Include="nossmooth.h" />
+ </ItemGroup>
+ <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+ <ImportGroup Label="ExtensionTargets">
+ </ImportGroup>
+</Project><
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/NosSmooth.Inject.vcxproj.filters +39 -0
@@ 1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+ <ItemGroup>
+ <Filter Include="Source Files">
+ <UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
+ <Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
+ </Filter>
+ <Filter Include="Header Files">
+ <UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
+ <Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
+ </Filter>
+ <Filter Include="Resource Files">
+ <UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
+ <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
+ </Filter>
+ </ItemGroup>
+ <ItemGroup>
+ <ClCompile Include="dllmain.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ <ClCompile Include="nossmooth.cpp">
+ <Filter>Source Files</Filter>
+ </ClCompile>
+ </ItemGroup>
+ <ItemGroup>
+ <ClInclude Include="nossmooth.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="hostfxr.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="nethost.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ <ClInclude Include="coreclr_delegates.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
+ </ItemGroup>
+</Project><
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/coreclr_delegates.h +47 -0
@@ 1,47 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __CORECLR_DELEGATES_H__
+#define __CORECLR_DELEGATES_H__
+
+#include <stdint.h>
+
+#if defined(_WIN32)
+#define CORECLR_DELEGATE_CALLTYPE __stdcall
+#ifdef _WCHAR_T_DEFINED
+typedef wchar_t char_t;
+#else
+typedef unsigned short char_t;
+#endif
+#else
+#define CORECLR_DELEGATE_CALLTYPE
+typedef char char_t;
+#endif
+
+#define UNMANAGEDCALLERSONLY_METHOD ((const char_t*)-1)
+
+// Signature of delegate returned by coreclr_delegate_type::load_assembly_and_get_function_pointer
+typedef int (CORECLR_DELEGATE_CALLTYPE* load_assembly_and_get_function_pointer_fn)(
+ const char_t* assembly_path /* Fully qualified path to assembly */,
+ const char_t* type_name /* Assembly qualified type name */,
+ const char_t* method_name /* Public static method name compatible with delegateType */,
+ const char_t* delegate_type_name /* Assembly qualified delegate type name or null
+ or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
+ the UnmanagedCallersOnlyAttribute. */,
+ void* reserved /* Extensibility parameter (currently unused and must be 0) */,
+ /*out*/ void** delegate /* Pointer where to store the function pointer result */);
+
+// Signature of delegate returned by load_assembly_and_get_function_pointer_fn when delegate_type_name == null (default)
+typedef int (CORECLR_DELEGATE_CALLTYPE* component_entry_point_fn)(void* arg, int32_t arg_size_in_bytes);
+
+typedef int (CORECLR_DELEGATE_CALLTYPE* get_function_pointer_fn)(
+ const char_t* type_name /* Assembly qualified type name */,
+ const char_t* method_name /* Public static method name compatible with delegateType */,
+ const char_t* delegate_type_name /* Assembly qualified delegate type name or null,
+ or UNMANAGEDCALLERSONLY_METHOD if the method is marked with
+ the UnmanagedCallersOnlyAttribute. */,
+ void* load_context /* Extensibility parameter (currently unused and must be 0) */,
+ void* reserved /* Extensibility parameter (currently unused and must be 0) */,
+ /*out*/ void** delegate /* Pointer where to store the function pointer result */);
+
+#endif // __CORECLR_DELEGATES_H__<
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/dllmain.cpp +19 -0
@@ 1,19 @@
+// dllmain.cpp : Defines the entry point for the DLL application.
+#include <windows.h>
+
+BOOL APIENTRY DllMain( HMODULE hModule,
+ DWORD ul_reason_for_call,
+ LPVOID lpReserved
+ )
+{
+ switch (ul_reason_for_call)
+ {
+ case DLL_PROCESS_ATTACH:
+ case DLL_THREAD_ATTACH:
+ case DLL_THREAD_DETACH:
+ case DLL_PROCESS_DETACH:
+ break;
+ }
+ return TRUE;
+}
+
A => src/Inject/NosSmooth.Inject/framework.h +5 -0
@@ 1,5 @@
+#pragma once
+
+#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
+// Windows Header Files
+#include <windows.h>
A => src/Inject/NosSmooth.Inject/hostfxr.h +323 -0
@@ 1,323 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#ifndef __HOSTFXR_H__
+#define __HOSTFXR_H__
+
+#include <stddef.h>
+#include <stdint.h>
+
+#if defined(_WIN32)
+#define HOSTFXR_CALLTYPE __cdecl
+#ifdef _WCHAR_T_DEFINED
+typedef wchar_t char_t;
+#else
+typedef unsigned short char_t;
+#endif
+#else
+#define HOSTFXR_CALLTYPE
+typedef char char_t;
+#endif
+
+enum hostfxr_delegate_type
+{
+ hdt_com_activation,
+ hdt_load_in_memory_assembly,
+ hdt_winrt_activation,
+ hdt_com_register,
+ hdt_com_unregister,
+ hdt_load_assembly_and_get_function_pointer,
+ hdt_get_function_pointer,
+};
+
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_fn)(const int argc, const char_t** argv);
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_startupinfo_fn)(
+ const int argc,
+ const char_t** argv,
+ const char_t* host_path,
+ const char_t* dotnet_root,
+ const char_t* app_path);
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_main_bundle_startupinfo_fn)(
+ const int argc,
+ const char_t** argv,
+ const char_t* host_path,
+ const char_t* dotnet_root,
+ const char_t* app_path,
+ int64_t bundle_header_offset);
+
+typedef void(HOSTFXR_CALLTYPE* hostfxr_error_writer_fn)(const char_t* message);
+
+//
+// Sets a callback which is to be used to write errors to.
+//
+// Parameters:
+// error_writer
+// A callback function which will be invoked every time an error is to be reported.
+// Or nullptr to unregister previously registered callback and return to the default behavior.
+// Return value:
+// The previously registered callback (which is now unregistered), or nullptr if no previous callback
+// was registered
+//
+// The error writer is registered per-thread, so the registration is thread-local. On each thread
+// only one callback can be registered. Subsequent registrations overwrite the previous ones.
+//
+// By default no callback is registered in which case the errors are written to stderr.
+//
+// Each call to the error writer is sort of like writing a single line (the EOL character is omitted).
+// Multiple calls to the error writer may occure for one failure.
+//
+// If the hostfxr invokes functions in hostpolicy as part of its operation, the error writer
+// will be propagated to hostpolicy for the duration of the call. This means that errors from
+// both hostfxr and hostpolicy will be reporter through the same error writer.
+//
+typedef hostfxr_error_writer_fn(HOSTFXR_CALLTYPE* hostfxr_set_error_writer_fn)(hostfxr_error_writer_fn error_writer);
+
+typedef void* hostfxr_handle;
+struct hostfxr_initialize_parameters
+{
+ size_t size;
+ const char_t* host_path;
+ const char_t* dotnet_root;
+};
+
+//
+// Initializes the hosting components for a dotnet command line running an application
+//
+// Parameters:
+// argc
+// Number of argv arguments
+// argv
+// Command-line arguments for running an application (as if through the dotnet executable).
+// Only command-line arguments which are accepted by runtime installation are supported, SDK/CLI commands are not supported.
+// For example 'app.dll app_argument_1 app_argument_2`.
+// parameters
+// Optional. Additional parameters for initialization
+// host_context_handle
+// On success, this will be populated with an opaque value representing the initialized host context
+//
+// Return value:
+// Success - Hosting components were successfully initialized
+// HostInvalidState - Hosting components are already initialized
+//
+// This function parses the specified command-line arguments to determine the application to run. It will
+// then find the corresponding .runtimeconfig.json and .deps.json with which to resolve frameworks and
+// dependencies and prepare everything needed to load the runtime.
+//
+// This function only supports arguments for running an application. It does not support SDK commands.
+//
+// This function does not load the runtime.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_dotnet_command_line_fn)(
+ int argc,
+ const char_t** argv,
+ const struct hostfxr_initialize_parameters* parameters,
+ /*out*/ hostfxr_handle* host_context_handle);
+
+//
+// Initializes the hosting components using a .runtimeconfig.json file
+//
+// Parameters:
+// runtime_config_path
+// Path to the .runtimeconfig.json file
+// parameters
+// Optional. Additional parameters for initialization
+// host_context_handle
+// On success, this will be populated with an opaque value representing the initialized host context
+//
+// Return value:
+// Success - Hosting components were successfully initialized
+// Success_HostAlreadyInitialized - Config is compatible with already initialized hosting components
+// Success_DifferentRuntimeProperties - Config has runtime properties that differ from already initialized hosting components
+// CoreHostIncompatibleConfig - Config is incompatible with already initialized hosting components
+//
+// This function will process the .runtimeconfig.json to resolve frameworks and prepare everything needed
+// to load the runtime. It will only process the .deps.json from frameworks (not any app/component that
+// may be next to the .runtimeconfig.json).
+//
+// This function does not load the runtime.
+//
+// If called when the runtime has already been loaded, this function will check if the specified runtime
+// config is compatible with the existing runtime.
+//
+// Both Success_HostAlreadyInitialized and Success_DifferentRuntimeProperties codes are considered successful
+// initializations. In the case of Success_DifferentRuntimeProperties, it is left to the consumer to verify that
+// the difference in properties is acceptable.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_initialize_for_runtime_config_fn)(
+ const char_t* runtime_config_path,
+ const struct hostfxr_initialize_parameters* parameters,
+ /*out*/ hostfxr_handle* host_context_handle);
+
+//
+// Gets the runtime property value for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// name
+// Runtime property name
+// value
+// Out parameter. Pointer to a buffer with the property value.
+//
+// Return value:
+// The error code result.
+//
+// The buffer pointed to by value is owned by the host context. The lifetime of the buffer is only
+// guaranteed until any of the below occur:
+// - a 'run' method is called for the host context
+// - properties are changed via hostfxr_set_runtime_property_value
+// - the host context is closed via 'hostfxr_close'
+//
+// If host_context_handle is nullptr and an active host context exists, this function will get the
+// property value for the active host context.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_property_value_fn)(
+ const hostfxr_handle host_context_handle,
+ const char_t* name,
+ /*out*/ const char_t** value);
+
+//
+// Sets the value of a runtime property for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// name
+// Runtime property name
+// value
+// Value to set
+//
+// Return value:
+// The error code result.
+//
+// Setting properties is only supported for the first host context, before the runtime has been loaded.
+//
+// If the property already exists in the host context, it will be overwritten. If value is nullptr, the
+// property will be removed.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_set_runtime_property_value_fn)(
+ const hostfxr_handle host_context_handle,
+ const char_t* name,
+ const char_t* value);
+
+//
+// Gets all the runtime properties for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// count
+// [in] Size of the keys and values buffers
+// [out] Number of properties returned (size of keys/values buffers used). If the input value is too
+// small or keys/values is nullptr, this is populated with the number of available properties
+// keys
+// Array of pointers to buffers with runtime property keys
+// values
+// Array of pointers to buffers with runtime property values
+//
+// Return value:
+// The error code result.
+//
+// The buffers pointed to by keys and values are owned by the host context. The lifetime of the buffers is only
+// guaranteed until any of the below occur:
+// - a 'run' method is called for the host context
+// - properties are changed via hostfxr_set_runtime_property_value
+// - the host context is closed via 'hostfxr_close'
+//
+// If host_context_handle is nullptr and an active host context exists, this function will get the
+// properties for the active host context.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_properties_fn)(
+ const hostfxr_handle host_context_handle,
+ /*inout*/ size_t* count,
+ /*out*/ const char_t** keys,
+ /*out*/ const char_t** values);
+
+//
+// Load CoreCLR and run the application for an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+//
+// Return value:
+// If the app was successfully run, the exit code of the application. Otherwise, the error code result.
+//
+// The host_context_handle must have been initialized using hostfxr_initialize_for_dotnet_command_line.
+//
+// This function will not return until the managed application exits.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_run_app_fn)(const hostfxr_handle host_context_handle);
+
+//
+// Gets a typed delegate from the currently loaded CoreCLR or from a newly created one.
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+// type
+// Type of runtime delegate requested
+// delegate
+// An out parameter that will be assigned the delegate.
+//
+// Return value:
+// The error code result.
+//
+// If the host_context_handle was initialized using hostfxr_initialize_for_runtime_config,
+// then all delegate types are supported.
+// If the host_context_handle was initialized using hostfxr_initialize_for_dotnet_command_line,
+// then only the following delegate types are currently supported:
+// hdt_load_assembly_and_get_function_pointer
+// hdt_get_function_pointer
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_get_runtime_delegate_fn)(
+ const hostfxr_handle host_context_handle,
+ enum hostfxr_delegate_type type,
+ /*out*/ void** delegate);
+
+//
+// Closes an initialized host context
+//
+// Parameters:
+// host_context_handle
+// Handle to the initialized host context
+//
+// Return value:
+// The error code result.
+//
+typedef int32_t(HOSTFXR_CALLTYPE* hostfxr_close_fn)(const hostfxr_handle host_context_handle);
+
+struct hostfxr_dotnet_environment_sdk_info
+{
+ size_t size;
+ const char_t* version;
+ const char_t* path;
+};
+
+typedef void(HOSTFXR_CALLTYPE* hostfxr_get_dotnet_environment_info_result_fn)(
+ const struct hostfxr_dotnet_environment_info* info,
+ void* result_context);
+
+struct hostfxr_dotnet_environment_framework_info
+{
+ size_t size;
+ const char_t* name;
+ const char_t* version;
+ const char_t* path;
+};
+
+struct hostfxr_dotnet_environment_info
+{
+ size_t size;
+
+ const char_t* hostfxr_version;
+ const char_t* hostfxr_commit_hash;
+
+ size_t sdk_count;
+ const hostfxr_dotnet_environment_sdk_info* sdks;
+
+ size_t framework_count;
+ const hostfxr_dotnet_environment_framework_info* frameworks;
+};
+
+#endif //__HOSTFXR_H__<
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/nethost.h +94 -0
@@ 1,94 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+#ifndef __NETHOST_H__
+#define __NETHOST_H__
+
+#include <stddef.h>
+
+#if defined(_WIN32)
+#ifdef NETHOST_EXPORT
+#define NETHOST_API __declspec(dllexport)
+#else
+#define NETHOST_API __declspec(dllimport)
+#endif
+
+#define NETHOST_CALLTYPE __stdcall
+#ifdef _WCHAR_T_DEFINED
+typedef wchar_t char_t;
+#else
+typedef unsigned short char_t;
+#endif
+#else
+#ifdef NETHOST_EXPORT
+#define NETHOST_API __attribute__((__visibility__("default")))
+#else
+#define NETHOST_API
+#endif
+
+#define NETHOST_CALLTYPE
+typedef char char_t;
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ // Parameters for get_hostfxr_path
+ //
+ // Fields:
+ // size
+ // Size of the struct. This is used for versioning.
+ //
+ // assembly_path
+ // Path to the compenent's assembly.
+ // If specified, hostfxr is located as if the assembly_path is the apphost
+ //
+ // dotnet_root
+ // Path to directory containing the dotnet executable.
+ // If specified, hostfxr is located as if an application is started using
+ // 'dotnet app.dll', which means it will be searched for under the dotnet_root
+ // path and the assembly_path is ignored.
+ //
+ struct get_hostfxr_parameters {
+ size_t size;
+ const char_t* assembly_path;
+ const char_t* dotnet_root;
+ };
+
+ //
+ // Get the path to the hostfxr library
+ //
+ // Parameters:
+ // buffer
+ // Buffer that will be populated with the hostfxr path, including a null terminator.
+ //
+ // buffer_size
+ // [in] Size of buffer in char_t units.
+ // [out] Size of buffer used in char_t units. If the input value is too small
+ // or buffer is nullptr, this is populated with the minimum required size
+ // in char_t units for a buffer to hold the hostfxr path
+ //
+ // get_hostfxr_parameters
+ // Optional. Parameters that modify the behaviour for locating the hostfxr library.
+ // If nullptr, hostfxr is located using the enviroment variable or global registration
+ //
+ // Return value:
+ // 0 on success, otherwise failure
+ // 0x80008098 - buffer is too small (HostApiBufferTooSmall)
+ //
+ // Remarks:
+ // The full search for the hostfxr library is done on every call. To minimize the need
+ // to call this function multiple times, pass a large buffer (e.g. PATH_MAX).
+ //
+ NETHOST_API int NETHOST_CALLTYPE get_hostfxr_path(
+ char_t* buffer,
+ size_t* buffer_size,
+ const struct get_hostfxr_parameters* parameters);
+
+#ifdef __cplusplus
+} // extern "C"
+#endif
+
+#endif // __NETHOST_H__
A => src/Inject/NosSmooth.Inject/nossmooth.cpp +125 -0
@@ 1,125 @@
+#include "nossmooth.h"
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// Standard headers
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <iostream>
+#include "nethost.h"
+#include "coreclr_delegates.h"
+#include "hostfxr.h"
+
+#include <cassert>
+
+#define STR(s) L ## s
+#define CH(c) L ## c
+#define DIR_SEPARATOR L'\\'
+
+using string_t = std::basic_string<char_t>;
+
+// Globals to hold hostfxr exports
+hostfxr_initialize_for_runtime_config_fn init_fptr;
+hostfxr_get_runtime_delegate_fn get_delegate_fptr;
+hostfxr_close_fn close_fptr;
+
+// Forward declarations
+bool load_hostfxr();
+load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* assembly);
+
+/********************************************************************************************
+ * Function used to load and activate .NET Core
+ ********************************************************************************************/
+
+ // Forward declarations
+void* load_library(const char_t*);
+void* get_export(void*, const char*);
+
+void* load_library(const char_t* path)
+{
+ HMODULE h = ::LoadLibraryW(path);
+ assert(h != nullptr);
+ return (void*)h;
+}
+void* get_export(void* h, const char* name)
+{
+ void* f = ::GetProcAddress((HMODULE)h, name);
+ assert(f != nullptr);
+ return f;
+}
+
+// Using the nethost library, discover the location of hostfxr and get exports
+bool load_hostfxr()
+{
+ // Pre-allocate a large buffer for the path to hostfxr
+ char_t buffer[MAX_PATH];
+ size_t buffer_size = sizeof(buffer) / sizeof(char_t);
+ int rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
+ if (rc != 0)
+ return false;
+
+ // Load hostfxr and get desired exports
+ void* lib = load_library(buffer);
+ init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
+ get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
+ close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");
+
+ return (init_fptr && get_delegate_fptr && close_fptr);
+}
+
+// Load and initialize .NET Core and get desired function pointer for scenario
+load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t* config_path)
+{
+ // Load .NET Core
+ void* load_assembly_and_get_function_pointer = nullptr;
+ hostfxr_handle cxt = nullptr;
+ int rc = init_fptr(config_path, nullptr, &cxt);
+ if (rc != 0 || cxt == nullptr)
+ {
+ std::cerr << "Init failed: " << std::hex << std::showbase << rc << std::endl;
+ close_fptr(cxt);
+ return nullptr;
+ }
+
+ // Get the load assembly function pointer
+ rc = get_delegate_fptr(
+ cxt,
+ hdt_load_assembly_and_get_function_pointer,
+ &load_assembly_and_get_function_pointer);
+ if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
+ std::cerr << "Get delegate failed: " << std::hex << std::showbase << rc << std::endl;
+
+ close_fptr(cxt);
+ return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
+}
+
+bool LoadAndCallMethod(LoadParams* params)
+{
+ if (!load_hostfxr())
+ {
+ assert(false && "Failure: load_hostfxr()");
+ return false;
+ }
+
+ load_assembly_and_get_function_pointer_fn load_assembly_and_get_function_pointer = nullptr;
+ load_assembly_and_get_function_pointer = get_dotnet_load_assembly(params->runtimeConfigPath);
+ assert(load_assembly_and_get_function_pointer != nullptr && "Failure: get_dotnet_load_assembly()");
+
+ typedef void (CORECLR_DELEGATE_CALLTYPE* main_entry_point_fn)();
+ main_entry_point_fn main = nullptr;
+ int rc = load_assembly_and_get_function_pointer(
+ params->libraryPath,
+ params->typePath,
+ params->methodName,
+ UNMANAGEDCALLERSONLY_METHOD,
+ nullptr,
+ (void**)&main);
+ assert(rc == 0 && main != nullptr && "Failure: load_assembly_and_get_function_pointer()");
+ main();
+ return true;
+}<
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/nossmooth.h +15 -0
@@ 1,15 @@
+#pragma once
+#include <Windows.h>
+
+#pragma pack(push, 1)
+struct LoadParams
+{
+ wchar_t *libraryPath;
+ wchar_t *runtimeConfigPath;
+ wchar_t *typePath;
+ wchar_t *methodName;
+};
+#pragma pack(pop)
+#define DllExport extern "C" __declspec( dllexport )
+
+DllExport bool LoadAndCallMethod(LoadParams* params);<
\ No newline at end of file
A => src/Inject/NosSmooth.Inject/pch.cpp +5 -0
@@ 1,5 @@
+// pch.cpp: source file corresponding to the pre-compiled header
+
+#include "pch.h"
+
+// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
A => src/Inject/NosSmooth.Inject/pch.h +13 -0
@@ 1,13 @@
+// pch.h: This is a precompiled header file.
+// Files listed below are compiled only once, improving build performance for future builds.
+// This also affects IntelliSense performance, including code completion and many code browsing features.
+// However, files listed here are ALL re-compiled if any one of them is updated between builds.
+// Do not add files here that you will be updating frequently as this negates the performance advantage.
+
+#ifndef PCH_H
+#define PCH_H
+
+// add headers that you want to pre-compile here
+#include "framework.h"
+
+#endif //PCH_H
A => src/Inject/NosSmooth.Injector.CLI/Commands/InjectCommand.cs +68 -0
@@ 1,68 @@
+//
+// InjectCommand.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.ComponentModel;
+using System.Diagnostics;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Results;
+
+namespace NosSmooth.Injector.CLI.Commands
+{
+ /// <summary>
+ /// Injection command for injecting .NET 5+ libraries with UnmanagedCallersOnly method.
+ /// </summary>
+ internal class InjectCommand : CommandGroup
+ {
+ private readonly NosInjector _injector;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="InjectCommand"/> class.
+ /// </summary>
+ /// <param name="injector">The nos smooth injector.</param>
+ public InjectCommand(NosInjector injector)
+ {
+ _injector = injector;
+ }
+
+ /// <summary>
+ /// The command to inject.
+ /// </summary>
+ /// <param name="process">The id of the process or part of its name.</param>
+ /// <param name="dllPath">The path to the dll to inject.</param>
+ /// <param name="typeName">The full type specifier. Default is LibraryName.DllMain, LibraryName.</param>
+ /// <param name="methodName">The name of the UnmanagedCallersOnly method. Default is Main.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ [Command("inject")]
+ public Task<Result> Inject
+ (
+ [Description("The id of the process to inject into.")]
+ string process,
+ [Description("The path to the dll to inject.")]
+ string dllPath,
+ [Option('t', "type"), Description("The full type specifier. Default is LibraryName.DllMain, LibraryName")]
+ string? typeName = null,
+ [Option('m', "method"), Description("The name of the UnmanagedCallersOnly method. Default is Main")]
+ string? methodName = null
+ )
+ {
+ if (!int.TryParse(process, out var processId))
+ {
+ var foundProcess = Process.GetProcesses().FirstOrDefault(x => x.ProcessName.Contains(process, StringComparison.OrdinalIgnoreCase));
+ if (foundProcess is null)
+ {
+ return Task.FromResult(Result.FromError(new NotFoundError("Could not find the given process.")));
+ }
+
+ processId = foundProcess.Id;
+ }
+
+ var dllName = Path.GetFileNameWithoutExtension(dllPath);
+ return Task.FromResult
+ (_injector.Inject(processId, dllPath, $"{dllName}.DllMain, {dllName}", methodName ?? "Main"));
+ }
+ }
+}<
\ No newline at end of file
A => src/Inject/NosSmooth.Injector.CLI/Commands/ListProcessesCommand.cs +41 -0
@@ 1,41 @@
+//
+// ListProcessesCommand.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Results;
+
+namespace NosSmooth.Injector.CLI.Commands
+{
+ /// <summary>
+ /// Command for listing processes to find id of NosTale process.
+ /// </summary>
+ internal class ListProcessesCommand : CommandGroup
+ {
+ /// <summary>
+ /// Lists processes by the given criteria.
+ /// </summary>
+ /// <param name="nameContains">What should the name of the process contain.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ [Command("list")]
+ public Task<Result> List(string nameContains = "Nostale")
+ {
+ var processes = Process.GetProcesses();
+ foreach (var process in processes.Where(x => x.ProcessName.Contains(nameContains, StringComparison.OrdinalIgnoreCase)))
+ {
+ Console.WriteLine(ProcessToString(process));
+ }
+
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ private string ProcessToString(Process process)
+ {
+ return $"{process.ProcessName} - {process.Id}";
+ }
+ }
+}
A => src/Inject/NosSmooth.Injector.CLI/NosSmooth.Injector.CLI.csproj +28 -0
@@ 1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <ApplicationManifest>app.manifest</ApplicationManifest>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
+ <PackageReference Include="Remora.Commands" Version="8.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <NativeLibs Remove="Program.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <NativeLibs Remove="app.manifest" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\NosSmooth.Injector\NosSmooth.Injector.csproj" />
+ </ItemGroup>
+
+</Project>
A => src/Inject/NosSmooth.Injector.CLI/Program.cs +75 -0
@@ 1,75 @@
+//
+// Program.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Injector.CLI.Commands;
+using Remora.Commands.Extensions;
+using Remora.Commands.Services;
+using Remora.Results;
+
+namespace NosSmooth.Injector.CLI
+{
+ /// <summary>
+ /// The entrypoint class.
+ /// </summary>
+ public static class Program
+ {
+ /// <summary>
+ /// The entrypoint method.
+ /// </summary>
+ /// <param name="argv">The command line arguments.</param>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ public static async Task Main(string[] argv)
+ {
+ var services = CreateServices();
+ var commandService = services.GetRequiredService<CommandService>();
+ var preparedCommandResult = await commandService.TryPrepareCommandAsync(string.Join(' ', argv), services);
+ if (!preparedCommandResult.IsSuccess)
+ {
+ Console.Error.WriteLine($"There was an error, {preparedCommandResult.Error.Message}");
+ return;
+ }
+
+ if (preparedCommandResult.Entity is null)
+ {
+ Console.Error.WriteLine("You must enter a command such ast list or inject.");
+ return;
+ }
+
+ var executionResult = await commandService.TryExecuteAsync(preparedCommandResult.Entity, services);
+ if (!executionResult.Entity.IsSuccess)
+ {
+ switch (executionResult.Entity.Error)
+ {
+ case ExceptionError exc:
+ Console.Error.WriteLine($"There was an exception, {exc.Exception.Message}");
+ break;
+ default:
+ Console.Error.WriteLine($"There was an error, {executionResult.Entity.Error!.Message}");
+ break;
+ }
+
+ return;
+ }
+ }
+
+ private static IServiceProvider CreateServices()
+ {
+ var collection = new ServiceCollection();
+ collection
+ .AddSingleton<NosInjector>()
+ .AddOptions<NosInjectorOptions>();
+
+ collection
+ .AddCommands()
+ .AddCommandTree()
+ .WithCommandGroup<InjectCommand>()
+ .WithCommandGroup<ListProcessesCommand>();
+
+ return collection.BuildServiceProvider();
+ }
+ }
+}
A => src/Inject/NosSmooth.Injector.CLI/app.manifest +11 -0
@@ 1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
+ <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
+ <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
+ <security>
+ <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
+ <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
+ </requestedPrivileges>
+ </security>
+ </trustInfo>
+</assembly><
\ No newline at end of file
A => src/Inject/NosSmooth.Injector/Errors/InjectionFailedError.cs +15 -0
@@ 1,15 @@
+//
+// InjectionFailedError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.Injector.Errors;
+
+/// <summary>
+/// The injection could not be finished successfully.
+/// </summary>
+/// <param name="DllPath">The path to the dll.</param>
+public record InjectionFailedError(string DllPath) : ResultError($"Could not inject {DllPath} dll into the process.");<
\ No newline at end of file
A => src/Inject/NosSmooth.Injector/Errors/InsufficientPermissionsError.cs +20 -0
@@ 1,20 @@
+//
+// InsufficientPermissionsError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.Injector.Errors;
+
+/// <summary>
+/// The current user has insufficient permissions to inject into the given process.
+/// </summary>
+/// <param name="ProcessId">The id of the process.</param>
+/// <param name="ProcessName">The name of the process.</param>
+public record InsufficientPermissionsError(long ProcessId, string ProcessName)
+ : ResultError
+ (
+ $"Insufficient permissions to open process {ProcessId} ({ProcessName}). Try running the injector as administrator."
+ );<
\ No newline at end of file
A => src/Inject/NosSmooth.Injector/Errors/ProcessNotFoundError.cs +16 -0
@@ 1,16 @@
+//
+// ProcessNotFoundError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.Injector.Errors;
+
+/// <summary>
+/// The given process was not found.
+/// </summary>
+/// <param name="ProcessId">The id of the process.</param>
+public record ProcessNotFoundError(string ProcessId)
+ : NotFoundError($"Could not find process with the given id {ProcessId}.");<
\ No newline at end of file
A => src/Inject/NosSmooth.Injector/LoadParams.cs +44 -0
@@ 1,44 @@
+//
+// LoadParams.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace NosSmooth.Injector
+{
+ /// <summary>
+ /// The parameters passed to the inject module.
+ /// </summary>
+ internal struct LoadParams
+ {
+ /// <summary>
+ /// The full path of the library.
+ /// </summary>
+ public int LibraryPath;
+
+ /// <summary>
+ /// The full path of the library.
+ /// </summary>
+ public int RuntimeConfigPath;
+
+ /// <summary>
+ /// The full path to the type with the method marked as UnsafeCallersOnly.
+ /// </summary>
+ /// <remarks>
+ /// Can be for example "LibraryNamespace.Type, LibraryNamespace".
+ /// </remarks>
+ public int TypePath;
+
+ /// <summary>
+ /// The name of the method to execute.
+ /// </summary>
+ public int MethodName;
+ }
+}
A => src/Inject/NosSmooth.Injector/ManagedMemoryAllocation.cs +47 -0
@@ 1,47 @@
+//
+// ManagedMemoryAllocation.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Net.NetworkInformation;
+using Reloaded.Memory.Sources;
+
+namespace NosSmooth.Injector;
+
+/// <summary>
+/// Represents freeable memory allocation.
+/// </summary>
+internal class ManagedMemoryAllocation : IDisposable
+{
+ private readonly IMemory _memory;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ManagedMemoryAllocation"/> class.
+ /// </summary>
+ /// <param name="memory">The memory with allocation.</param>
+ /// <param name="pointer">The pointer to allocated memory.</param>
+ public ManagedMemoryAllocation(IMemory memory, IntPtr pointer)
+ {
+ Pointer = pointer;
+ _memory = memory;
+
+ }
+
+ /// <summary>
+ /// The allocated pointer number.
+ /// </summary>
+ public IntPtr Pointer { get; private set; }
+
+ /// <summary>
+ /// Whether the memory is currently allocated.
+ /// </summary>
+ public bool Allocated => Pointer != IntPtr.Zero;
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ _memory.Free(Pointer);
+ Pointer = IntPtr.Zero;
+ }
+}<
\ No newline at end of file
A => src/Inject/NosSmooth.Injector/NosInjector.cs +155 -0
@@ 1,155 @@
+//
+// NosInjector.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Security;
+using System.Text;
+using Microsoft.Extensions.Options;
+using NosSmooth.Injector.Errors;
+using Reloaded.Memory.Sources;
+using Remora.Results;
+
+namespace NosSmooth.Injector;
+
+/// <summary>
+/// Nos smooth injector for .NET 5+ projects.
+/// </summary>
+/// <remarks>
+/// If you want to inject your project into NosTale that
+/// uses NosSmooth.LocalClient, use this injector.
+/// You must expose static UnmanagedCallersOnly method.
+/// </remarks>
+public class NosInjector
+{
+ private readonly NosInjectorOptions _options;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NosInjector"/> class.
+ /// </summary>
+ /// <param name="options">The injector options.</param>
+ public NosInjector(IOptions<NosInjectorOptions> options)
+ {
+ _options = options.Value;
+ }
+
+ /// <summary>
+ /// Inject the given .NET 5+ dll into NosTale process.
+ /// </summary>
+ /// <remarks>
+ /// The dll must also have .runtimeconfig.json present next to the dll.
+ /// </remarks>
+ /// <param name="processId">The id of the process to inject to.</param>
+ /// <param name="dllPath">The absolute path to the dll to inject.</param>
+ /// <param name="classPath">The full path to the class. Such as "MyLibrary.DllMain, MyLibrary".</param>
+ /// <param name="methodName">The name of the method to execute. The method should return void and have no parameters.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result Inject
+ (
+ int processId,
+ string dllPath,
+ string classPath,
+ string methodName = "Main"
+ )
+ {
+ using var process = Process.GetProcessById(processId);
+ return Inject(process, dllPath, classPath, methodName);
+ }
+
+ /// <summary>
+ /// Inject the given .NET 5+ dll into NosTale process.
+ /// </summary>
+ /// <remarks>
+ /// The dll must also have .runtimeconfig.json present next to the dll.
+ /// </remarks>
+ /// <param name="process">The process to inject to.</param>
+ /// <param name="dllPath">The absolute path to the dll to inject.</param>
+ /// <param name="classPath">The full path to the class. Such as "MyLibrary.DllMain, MyLibrary".</param>
+ /// <param name="methodName">The name of the method to execute. The method should return void and have no parameters.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Result Inject
+ (
+ Process process,
+ string dllPath,
+ string classPath,
+ string methodName = "Main"
+ )
+ {
+ try
+ {
+ using var injector = new Reloaded.Injector.Injector(process);
+ var memory = new ExternalMemory(process);
+
+ var directoryName = Path.GetDirectoryName(dllPath);
+ if (directoryName is null)
+ {
+ return new GenericError("There was an error obtaining directory name of the dll path.");
+ }
+
+ var runtimePath = Path.Combine
+ (directoryName, Path.GetFileNameWithoutExtension(dllPath)) + ".runtimeconfig.json";
+
+ using var dllPathMemory = AllocateString(memory, dllPath);
+ using var classPathMemory = AllocateString(memory, classPath);
+ using var methodNameMemory = AllocateString(memory, methodName);
+ using var runtimePathMemory = AllocateString(memory, runtimePath);
+
+ if (!dllPathMemory.Allocated || !classPathMemory.Allocated || !methodNameMemory.Allocated
+ || !runtimePathMemory.Allocated)
+ {
+ return new GenericError("Could not allocate memory in the external process.");
+ }
+
+ var loadParams = new LoadParams()
+ {
+ LibraryPath = (int)dllPathMemory.Pointer,
+ MethodName = (int)methodNameMemory.Pointer,
+ RuntimeConfigPath = (int)runtimePathMemory.Pointer,
+ TypePath = (int)classPathMemory.Pointer
+ };
+
+ var nosSmoothInjectPath = Path.GetFullPath(_options.NosSmoothInjectPath);
+ var injected = injector.Inject(nosSmoothInjectPath);
+ if (injected == 0)
+ {
+ return new InjectionFailedError(nosSmoothInjectPath);
+ }
+
+ var functionResult = injector.CallFunction(nosSmoothInjectPath, "LoadAndCallMethod", loadParams);
+ if (functionResult != 0)
+ {
+ return new InjectionFailedError(dllPath);
+ }
+
+ return Result.FromSuccess();
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return new InsufficientPermissionsError(process.Id, process.ProcessName);
+ }
+ catch (SecurityException)
+ {
+ return new InsufficientPermissionsError(process.Id, process.ProcessName);
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ }
+
+ private ManagedMemoryAllocation AllocateString(IMemory memory, string str)
+ {
+ var bytes = Encoding.Unicode.GetBytes(str);
+ var allocated = memory.Allocate(bytes.Length + 1);
+ if (allocated == IntPtr.Zero)
+ {
+ return new ManagedMemoryAllocation(memory, allocated);
+ }
+
+ memory.SafeWriteRaw(allocated + bytes.Length, new byte[] { 0 });
+ memory.SafeWriteRaw(allocated, bytes);
+ return new ManagedMemoryAllocation(memory, allocated);
+ }
+}<
\ No newline at end of file
A => src/Inject/NosSmooth.Injector/NosInjectorOptions.cs +32 -0
@@ 1,32 @@
+//
+// NosInjectorOptions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+
+namespace NosSmooth.Injector
+{
+ /// <summary>
+ /// Options for NosInjector.
+ /// </summary>
+ public class NosInjectorOptions : IOptions<NosInjectorOptions>
+ {
+ /// <summary>
+ /// Gets or sets the path to the nos smooth inject dll.
+ /// </summary>
+ /// <remarks>
+ /// If not absolute path, then relative path from the current executing process is assumed.
+ /// </remarks>
+ public string NosSmoothInjectPath { get; set; } = "NosSmooth.Inject.dll";
+
+ /// <inheritdoc/>
+ public NosInjectorOptions Value => this;
+ }
+}
A => src/Inject/NosSmooth.Injector/NosSmooth.Injector.csproj +15 -0
@@ 1,15 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" />
+ <PackageReference Include="Reloaded.Injector" Version="1.2.4" />
+ <PackageReference Include="Remora.Results" Version="7.1.0" />
+ </ItemGroup>
+
+</Project>
A => src/Samples/External/ExternalBrowser/ExternalBrowser.csproj +18 -0
@@ 1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\Core\NosSmooth.LocalBinding\NosSmooth.LocalBinding.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="NosSmooth.Core" Version="1.0.0" />
+ </ItemGroup>
+
+</Project>
A => src/Samples/External/ExternalBrowser/Program.cs +75 -0
@@ 1,75 @@
+//
+// Program.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalBinding.Options;
+
+namespace ExternalBrowser;
+
+/// <summary>
+/// The entrypoint class for ExternalBrowser.
+/// </summary>
+public class Program
+{
+ /// <summary>
+ /// The entrypoint method for ExternalBrowser.
+ /// </summary>
+ /// <param name="arguments">The arguments.</param>
+ public static void Main(string[] arguments)
+ {
+ var playerManagerOptions = new PlayerManagerOptions();
+ var sceneManagerOptions = new SceneManagerOptions();
+ var petManagerOptions = new PetManagerOptions();
+
+ foreach (var argument in arguments)
+ {
+ Process[] processes;
+
+ if (int.TryParse(argument, out var processId))
+ {
+ processes = new[] { Process.GetProcessById(processId) };
+ }
+ else
+ {
+ processes = Process
+ .GetProcesses()
+ .Where(x => x.ProcessName.Contains(argument))
+ .ToArray();
+ }
+
+ foreach (var process in processes)
+ {
+ var externalBrowser = new NosBrowserManager
+ (process, playerManagerOptions, sceneManagerOptions, petManagerOptions);
+
+ if (!externalBrowser.IsNostaleProcess)
+ {
+ Console.Error.WriteLine($"Process {process.Id} is not a nostale process.");
+ continue;
+ }
+
+ var initializationResult = externalBrowser.Initialize();
+ if (!initializationResult.IsSuccess)
+ {
+ Console.Error.WriteLine(initializationResult.ToFullString());
+ }
+
+ var length = externalBrowser.PetManagerList.Length;
+ Console.WriteLine(length);
+
+ if (!externalBrowser.IsInGame)
+ {
+ Console.Error.WriteLine("The player is not in game, cannot get the name of the player.");
+ continue;
+ }
+
+ Console.WriteLine($"Player in process {process.Id} is named {externalBrowser.PlayerManager.Player.Name}");
+ }
+ }
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/InterceptNameChanger/DllMain.cs +45 -0
@@ 1,45 @@
+//
+// DllMain.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace InterceptNameChanger
+{
+ /// <summary>
+ /// The main entrypoint class of the dll.
+ /// </summary>
+ public class DllMain
+ {
+ [DllImport("kernel32")]
+#pragma warning disable SA1600
+ public static extern bool AllocConsole();
+#pragma warning restore SA1600
+
+ /// <summary>
+ /// The main entrypoint method of the dll.
+ /// </summary>
+ [UnmanagedCallersOnly(EntryPoint = "Main")]
+ public static void Main()
+ {
+ AllocConsole();
+ Console.WriteLine("Hello from InterceptNameChanger DllMain entry point.");
+
+ new Thread(() =>
+ {
+ try
+ {
+ new NameChanger().RunAsync().GetAwaiter().GetResult();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ }
+ }).Start();
+ }
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/InterceptNameChanger/FodyWeavers.xml +3 -0
@@ 1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+ <Costura />
+</Weavers><
\ No newline at end of file
A => src/Samples/LowLevel/InterceptNameChanger/InterceptNameChanger.csproj +25 -0
@@ 1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <LangVersion>10</LangVersion>
+ <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Costura.Fody" Version="5.7.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Fody" Version="6.6.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
+ <PackageReference Include="NosSmooth.Core" Version="1.0.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\Core\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
+ </ItemGroup>
+</Project><
\ No newline at end of file
A => src/Samples/LowLevel/InterceptNameChanger/NameChangeInterceptor.cs +81 -0
@@ 1,81 @@
+//
+// NameChangeInterceptor.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.LocalClient;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+
+namespace InterceptNameChanger
+{
+ /// <summary>
+ /// Intercepts the packets so name in c_info may be replaced.
+ /// </summary>
+ public class NameChangeInterceptor : IPacketInterceptor
+ {
+ private readonly INostaleClient _client;
+ private readonly ILogger<NameChangeInterceptor> _logger;
+ private string _name = "Intercept";
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NameChangeInterceptor"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ /// <param name="logger">The logger.</param>
+ public NameChangeInterceptor(INostaleClient client, ILogger<NameChangeInterceptor> logger)
+ {
+ _client = client;
+ _logger = logger;
+ }
+
+ /// <inheritdoc/>
+ public bool InterceptSend(ref string packet)
+ {
+ if (packet.StartsWith("say #"))
+ {
+ _name = packet.Substring(5).Replace(" ", "⠀"); // Mind the symbols!
+ _logger.LogInformation("Name changed to {Name}", _name);
+ _client.ReceivePacketAsync
+ (
+ new SayPacket
+ (
+ EntityType.Map,
+ 1,
+ SayColor.Red,
+ $"Name changed to {_name}, change map for it to take effect."
+ )
+ )
+ .GetAwaiter()
+ .GetResult();
+ return false;
+ }
+
+ return true; // Accept the packet
+ }
+
+ /// <inheritdoc/>
+ public bool InterceptReceive(ref string packet)
+ {
+ if (packet.StartsWith("c_info"))
+ {
+ var oldPart = packet.Substring(packet.IndexOf(' ', 7));
+ var result = _client.ReceivePacketAsync($"c_info {_name} " + oldPart)
+ .GetAwaiter().GetResult(); // Change the name
+
+ if (!result.IsSuccess)
+ {
+ _logger.LogError("Could not send the c_info packet: {Reason}", result.Error.Message);
+ return true; // Accept the packet so client is not confused
+ }
+ return false; // Reject the packet
+ }
+
+ return true; // Accept the packet
+ }
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/InterceptNameChanger/NameChanger.cs +86 -0
@@ 1,86 @@
+//
+// NameChanger.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalClient;
+using NosSmooth.LocalClient.Extensions;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Extensions;
+using NosSmooth.Packets.Packets;
+using NosSmooth.Packets.Server.Chat;
+
+namespace InterceptNameChanger
+{
+ /// <summary>
+ /// Main class of name changer.
+ /// </summary>
+ public class NameChanger
+ {
+ /// <summary>
+ /// Run the name changer.
+ /// </summary>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ public async Task RunAsync()
+ {
+ var provider = new ServiceCollection()
+ .AddLocalClient()
+
+ // .AddPacketResponder<SayResponder>()
+ .AddLogging
+ (
+ b =>
+ {
+ b.ClearProviders();
+ b.AddConsole();
+ b.SetMinimumLevel(LogLevel.Debug);
+ }
+ )
+ .Configure<LocalClientOptions>(o => o.AllowIntercept = true)
+ .AddSingleton<IPacketInterceptor, NameChangeInterceptor>()
+ .BuildServiceProvider();
+
+ var logger = provider.GetRequiredService<ILogger<NameChanger>>();
+ logger.LogInformation("Hello world from NameChanger!");
+
+ var bindingManager = provider.GetRequiredService<NosBindingManager>();
+ var initializeResult = bindingManager.Initialize();
+ if (!initializeResult.IsSuccess)
+ {
+ logger.LogError($"Could not initialize NosBindingManager.");
+ logger.LogResultError(initializeResult);
+ }
+
+ var packetTypesRepository = provider.GetRequiredService<IPacketTypesRepository>();
+ var packetAddResult = packetTypesRepository.AddDefaultPackets();
+ if (!packetAddResult.IsSuccess)
+ {
+ logger.LogError("Could not initialize default packet serializers correctly");
+ logger.LogResultError(packetAddResult);
+ }
+
+ var client = provider.GetRequiredService<INostaleClient>();
+
+ var sayResult = await client.ReceivePacketAsync
+ (
+ new SayPacket
+ (EntityType.Map, 1, SayColor.Red, "The name may be changed by typing #{NewName} into the chat.")
+ );
+
+ if (!sayResult.IsSuccess)
+ {
+ logger.LogError("Could not send say packet");
+ }
+
+ await client.RunAsync();
+ }
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/InterceptNameChanger/Properties/AssemblyInfo.cs +41 -0
@@ 1,41 @@
+//
+// AssemblyInfo.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("InterceptNameChanger")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("InterceptNameChanger")]
+[assembly: AssemblyCopyright("Copyright © 2021")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("F96F3AA0-131E-4B6B-AB21-BBE2DEBCEF3A")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]<
\ No newline at end of file
A => src/Samples/LowLevel/SimpleChat/DllMain.cs +32 -0
@@ 1,32 @@
+//
+// DllMain.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+namespace SimpleChat;
+
+/// <summary>
+/// The main entrypoint class of the dll.
+/// </summary>
+public class DllMain
+{
+ [DllImport("kernel32")]
+#pragma warning disable SA1600
+ public static extern bool AllocConsole();
+#pragma warning restore SA1600
+
+ /// <summary>
+ /// The main entrypoint method of the dll.
+ /// </summary>
+ [UnmanagedCallersOnly(EntryPoint = "Main")]
+ public static void Main()
+ {
+ AllocConsole();
+ Console.WriteLine("Hello from SimpleChat DllMain entry point.");
+
+ new Thread(() => new SimpleChat().RunAsync().GetAwaiter().GetResult()).Start();
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/SimpleChat/FodyWeavers.xml +4 -0
@@ 1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+ <Costura />
+</Weavers><
\ No newline at end of file
A => src/Samples/LowLevel/SimpleChat/SayResponder.cs +51 -0
@@ 1,51 @@
+//
+// SayResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+using Remora.Results;
+
+namespace SimpleChat;
+
+/// <summary>
+/// Responds to <see cref="SayPacket"/>.
+/// </summary>
+public class SayResponder : IPacketResponder<SayPacket>, IPacketResponder<MsgPacket>
+{
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="SayResponder"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ public SayResponder(INostaleClient client)
+ {
+ _client = client;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<SayPacket> packet, CancellationToken ct = default)
+ {
+ return _client.ReceivePacketAsync
+ (
+ new SayPacket(EntityType.Map, 1, SayColor.Red, "Hello world from NosSmooth!"),
+ ct
+ );
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketEventArgs<MsgPacket> packet, CancellationToken ct = default)
+ {
+ return _client.ReceivePacketAsync
+ (
+ new SayPacket(EntityType.Map, 1, SayColor.Red, "Hello world from NosSmooth!"),
+ ct
+ );
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/SimpleChat/SimpleChat.cs +74 -0
@@ 1,74 @@
+//
+// SimpleChat.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalClient.Extensions;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Extensions;
+using NosSmooth.Packets.Packets;
+using NosSmooth.Packets.Server.Chat;
+
+namespace SimpleChat;
+
+/// <summary>
+/// The main simple chat class.
+/// </summary>
+public class SimpleChat
+{
+ /// <summary>
+ /// Run the client.
+ /// </summary>
+ /// <returns>The task that may or may not have succeeded.</returns>
+ public async Task RunAsync()
+ {
+ var provider = new ServiceCollection()
+ .AddLocalClient()
+ .AddPacketResponder<SayResponder>()
+ .AddLogging
+ (
+ b =>
+ {
+ b.ClearProviders();
+ b.AddConsole();
+ b.SetMinimumLevel(LogLevel.Debug);
+ }
+ )
+ .BuildServiceProvider();
+
+ var logger = provider.GetRequiredService<ILogger<SimpleChat>>();
+ logger.LogInformation("Hello world from SimpleChat!");
+
+ var bindingManager = provider.GetRequiredService<NosBindingManager>();
+ var initializeResult = bindingManager.Initialize();
+ if (!initializeResult.IsSuccess)
+ {
+ logger.LogError($"Could not initialize NosBindingManager.");
+ logger.LogResultError(initializeResult);
+ }
+
+ var packetTypesRepository = provider.GetRequiredService<IPacketTypesRepository>();
+ var packetAddResult = packetTypesRepository.AddDefaultPackets();
+ if (!packetAddResult.IsSuccess)
+ {
+ logger.LogError("Could not initialize default packet serializers correctly");
+ logger.LogResultError(packetAddResult);
+ }
+
+ var client = provider.GetRequiredService<INostaleClient>();
+
+ await client.ReceivePacketAsync
+ (
+ new SayPacket(EntityType.Map, 1, SayColor.Red, "Hello world from NosSmooth!")
+ );
+
+ await client.RunAsync();
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/SimpleChat/SimpleChat.csproj +26 -0
@@ 1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <LangVersion>10</LangVersion>
+ <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Costura.Fody" Version="5.7.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Fody" Version="6.6.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
+ <PackageReference Include="NosSmooth.Core" Version="1.0.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\Core\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
+ </ItemGroup>
+</Project><
\ No newline at end of file
A => src/Samples/LowLevel/WalkCommands/Commands/CombatCommands.cs +92 -0
@@ 1,92 @@
+//
+// CombatCommands.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.ChatCommands;
+using NosSmooth.Core.Client;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalBinding.Objects;
+using NosSmooth.LocalBinding.Structs;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Results;
+
+namespace WalkCommands.Commands;
+
+/// <summary>
+/// Represents command group for combat commands.
+/// </summary>
+public class CombatCommands : CommandGroup
+{
+ private readonly UnitManagerBinding _unitManagerBinding;
+ private readonly SceneManager _sceneManager;
+ private readonly PlayerManagerBinding _playerManagerBinding;
+ private readonly FeedbackService _feedbackService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CombatCommands"/> class.
+ /// </summary>
+ /// <param name="unitManagerBinding">The scene manager binding.</param>
+ /// <param name="sceneManager">The scene manager.</param>
+ /// <param name="playerManagerBinding">The character binding.</param>
+ /// <param name="feedbackService">The feedback service.</param>
+ public CombatCommands
+ (
+ UnitManagerBinding unitManagerBinding,
+ SceneManager sceneManager,
+ PlayerManagerBinding playerManagerBinding,
+ FeedbackService feedbackService
+ )
+ {
+ _unitManagerBinding = unitManagerBinding;
+ _sceneManager = sceneManager;
+ _playerManagerBinding = playerManagerBinding;
+ _feedbackService = feedbackService;
+ }
+
+ /// <summary>
+ /// Focus the given entity.
+ /// </summary>
+ /// <param name="entityId">The entity id to focus.</param>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ [Command("focus")]
+ public Task<Result> HandleFocusAsync(int entityId)
+ {
+ var entityResult = _sceneManager.FindEntity(entityId);
+ if (!entityResult.IsSuccess)
+ {
+ return Task.FromResult(Result.FromError(entityResult));
+ }
+
+ return Task.FromResult(_unitManagerBinding.FocusEntity(entityResult.Entity));
+ }
+
+ /// <summary>
+ /// Follow the given entity.
+ /// </summary>
+ /// <param name="entityId">The entity id to follow.</param>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ [Command("follow")]
+ public Task<Result> HandleFollowAsync(int entityId)
+ {
+ var entityResult = _sceneManager.FindEntity(entityId);
+ if (!entityResult.IsSuccess)
+ {
+ return Task.FromResult(Result.FromError(entityResult));
+ }
+
+ return Task.FromResult(_playerManagerBinding.FollowEntity(entityResult.Entity));
+ }
+
+ /// <summary>
+ /// Stop following an entity.
+ /// </summary>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ [Command("unfollow")]
+ public Task<Result> HandleUnfollowAsync()
+ {
+ return Task.FromResult(_playerManagerBinding.UnfollowEntity());
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/WalkCommands/Commands/DetachCommand.cs +52 -0
@@ 1,52 @@
+//
+// DetachCommand.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.ChatCommands;
+using NosSmooth.Core.Client;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+using Remora.Commands.Groups;
+using Remora.Results;
+
+namespace WalkCommands.Commands;
+
+/// <summary>
+/// Group for detaching command that detaches the dll.
+/// </summary>
+public class DetachCommand : CommandGroup
+{
+ private readonly CancellationTokenSource _dllStop;
+ private readonly FeedbackService _feedbackService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DetachCommand"/> class.
+ /// </summary>
+ /// <param name="dllStop">The cancellation token source to stop the client.</param>
+ /// <param name="feedbackService">The feedback service.</param>
+ public DetachCommand(CancellationTokenSource dllStop, FeedbackService feedbackService)
+ {
+ _dllStop = dllStop;
+ _feedbackService = feedbackService;
+ }
+
+ /// <summary>
+ /// Detach the dll.
+ /// </summary>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public async Task<Result> HandleDetach()
+ {
+ var receiveResult = await _feedbackService.SendInfoMessageAsync("Going to detach!", CancellationToken);
+
+ if (!receiveResult.IsSuccess)
+ {
+ return receiveResult;
+ }
+
+ _dllStop.Cancel();
+ return Result.FromSuccess();
+ }
+}
A => src/Samples/LowLevel/WalkCommands/Commands/WalkCommands.cs +87 -0
@@ 1,87 @@
+//
+// WalkCommands.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.ChatCommands;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Commands;
+using NosSmooth.Core.Commands.Walking;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Packets.Enums;
+using NosSmooth.Packets.Enums.Chat;
+using NosSmooth.Packets.Server.Chat;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Results;
+
+namespace WalkCommands.Commands;
+
+/// <summary>
+/// Represents command group for walking.
+/// </summary>
+public class WalkCommands : CommandGroup
+{
+ private readonly INostaleClient _client;
+ private readonly FeedbackService _feedbackService;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="WalkCommands"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ /// <param name="feedbackService">The feedback service.</param>
+ public WalkCommands(INostaleClient client, FeedbackService feedbackService)
+ {
+ _client = client;
+ _feedbackService = feedbackService;
+ }
+
+ /// <summary>
+ /// Attempts to walk the character to the specified lcoation.
+ /// </summary>
+ /// <param name="x">The x coordinate.</param>
+ /// <param name="y">The y coordinate.</param>
+ /// <param name="isCancellable">Whether the user can cancel the operation.</param>
+ /// <param name="petSelectors">The pet selectors indices.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ [Command("walk")]
+ public async Task<Result> HandleWalkToAsync
+ (
+ ushort x,
+ ushort y,
+ bool isCancellable = true,
+ [Option('p', "pet")] params int[] petSelectors
+ )
+ {
+ var receiveResult = await _client.ReceivePacketAsync
+ (
+ new SayPacket(EntityType.Map, 1, SayColor.Red, $"Going to walk to {x} {y}."),
+ CancellationToken
+ );
+
+ if (!receiveResult.IsSuccess)
+ {
+ return receiveResult;
+ }
+
+ var command = new WalkCommand(x, y, petSelectors, AllowUserCancel: isCancellable);
+ var walkResult = await _client.SendCommandAsync(command, CancellationToken);
+ if (!walkResult.IsSuccess)
+ {
+ await _feedbackService.SendErrorMessageAsync($"Could not finish walking. {walkResult.ToFullString()}", CancellationToken);
+ await _client.ReceivePacketAsync
+ (
+ new SayPacket(EntityType.Map, 1, SayColor.Red, "Could not finish walking."),
+ CancellationToken
+ );
+ return walkResult;
+ }
+
+ return await _client.ReceivePacketAsync
+ (
+ new SayPacket(EntityType.Map, 1, SayColor.Red, "Walk has finished successfully."),
+ CancellationToken
+ );
+ }
+}
A => src/Samples/LowLevel/WalkCommands/DllMain.cs +40 -0
@@ 1,40 @@
+//
+// DllMain.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Runtime.InteropServices;
+
+namespace WalkCommands;
+
+/// <summary>
+/// Represents the dll entrypoint class.
+/// </summary>
+public class DllMain
+{
+ [DllImport("kernel32")]
+#pragma warning disable SA1600
+ public static extern bool AllocConsole();
+#pragma warning restore SA1600
+
+ /// <summary>
+ /// Represents the dll entrypoint method.
+ /// </summary>
+ [UnmanagedCallersOnly(EntryPoint = "Main")]
+ public static void Main()
+ {
+ AllocConsole();
+ new Thread(() =>
+ {
+ try
+ {
+ new Startup().RunAsync().GetAwaiter().GetResult();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ }
+ }).Start();
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/WalkCommands/FodyWeavers.xml +3 -0
@@ 1,3 @@
+<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
+ <Costura />
+</Weavers><
\ No newline at end of file
A => src/Samples/LowLevel/WalkCommands/Startup.cs +82 -0
@@ 1,82 @@
+//
+// Startup.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NosSmooth.ChatCommands;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalClient;
+using NosSmooth.LocalClient.Extensions;
+using NosSmooth.Packets.Extensions;
+using NosSmooth.Packets.Packets;
+using Remora.Commands.Extensions;
+using WalkCommands.Commands;
+
+namespace WalkCommands;
+
+/// <summary>
+/// Startup class of WalkCommands.
+/// </summary>
+public class Startup
+{
+ private IServiceProvider BuildServices()
+ {
+ var collection = new ServiceCollection()
+ .AddLocalClient()
+ .AddScoped<Commands.WalkCommands>()
+ .AddScoped<DetachCommand>()
+ .AddSingleton<CancellationTokenSource>()
+ .Configure<LocalClientOptions>(o => o.AllowIntercept = true)
+ .AddNostaleChatCommands()
+ .AddLogging
+ (
+ b =>
+ {
+ b.ClearProviders();
+ b.AddConsole();
+ b.SetMinimumLevel(LogLevel.Debug);
+ }
+ );
+
+ collection.AddCommandTree()
+ .WithCommandGroup<DetachCommand>()
+ .WithCommandGroup<CombatCommands>()
+ .WithCommandGroup<Commands.WalkCommands>();
+ return collection.BuildServiceProvider();
+ }
+
+ /// <summary>
+ /// Run the MoveToMiniland.
+ /// </summary>
+ /// <returns>A task that may or may not have succeeded.</returns>
+ public async Task RunAsync()
+ {
+ var provider = BuildServices();
+ var bindingManager = provider.GetRequiredService<NosBindingManager>();
+ var logger = provider.GetRequiredService<ILogger<Startup>>();
+ var initializeResult = bindingManager.Initialize();
+ if (!initializeResult.IsSuccess)
+ {
+ logger.LogError($"Could not initialize NosBindingManager.");
+ logger.LogResultError(initializeResult);
+ }
+
+ var packetTypesRepository = provider.GetRequiredService<IPacketTypesRepository>();
+ var packetAddResult = packetTypesRepository.AddDefaultPackets();
+ if (!packetAddResult.IsSuccess)
+ {
+ logger.LogError("Could not initialize default packet serializers correctly");
+ logger.LogResultError(packetAddResult);
+ }
+
+ var mainCancellation = provider.GetRequiredService<CancellationTokenSource>();
+
+ var client = provider.GetRequiredService<INostaleClient>();
+ await client.RunAsync(mainCancellation.Token);
+ }
+}<
\ No newline at end of file
A => src/Samples/LowLevel/WalkCommands/WalkCommands.csproj +42 -0
@@ 1,42 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <AssemblyName>WalkCommands</AssemblyName>
+ <RootNamespace>WalkCommands</RootNamespace>
+ <LangVersion>10</LangVersion>
+ <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <RestoreProjectStyle>PackageReference</RestoreProjectStyle>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Costura.Fody">
+ <Version>5.7.0</Version>
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Fody">
+ <Version>6.6.0</Version>
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection">
+ <Version>6.0.0</Version>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
+ <Version>6.0.0</Version>
+ </PackageReference>
+ <PackageReference Include="Microsoft.Extensions.Logging.Console">
+ <Version>6.0.0</Version>
+ </PackageReference>
+ <PackageReference Include="Remora.Results">
+ <Version>7.1.0</Version>
+ </PackageReference>
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\Core\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
+ <ProjectReference Include="..\..\..\Extensions\NosSmooth.ChatCommands\NosSmooth.ChatCommands.csproj" />
+ </ItemGroup>
+</Project><
\ No newline at end of file
A => stylecop.json +61 -0
@@ 1,61 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "indentation": {
+ "indentationSize": 4,
+ "tabSize": 4,
+ "useTabs": false
+ },
+ "spacingRules": {
+ },
+ "readabilityRules": {
+ },
+ "orderingRules": {
+ "elementOrder": [
+ "kind",
+ "constant",
+ "accessibility",
+ "static",
+ "readonly"
+ ],
+ "systemUsingDirectivesFirst": true,
+ "usingDirectivesPlacement": "outsideNamespace",
+ "blankLinesBetweenUsingGroups": "allow"
+ },
+ "namingRules": {
+ "allowCommonHungarianPrefixes": true,
+ "allowedHungarianPrefixes": [
+ "gl",
+ "f",
+ "db"
+ ]
+ },
+ "maintainabilityRules": {
+ "topLevelTypes": [
+ "class",
+ "interface",
+ "struct",
+ "enum"
+ ]
+ },
+ "layoutRules": {
+ "allowConsecutiveUsings": false
+ },
+ "documentationRules": {
+ "companyName": "František Boháček",
+ "copyrightText": "\n {fileName}\n\n Copyright (c) {companyName}. All rights reserved.\n Licensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.",
+ "variables": {
+ "licenseName": "MIT",
+ "licenseFile": "LICENSE"
+ },
+ "xmlHeader": false,
+ "documentInterfaces": true,
+ "documentExposedElements": true,
+ "documentInternalElements": true,
+ "documentPrivateElements": false,
+ "documentPrivateFields": false,
+ "documentationCulture": "en-US",
+ "fileNamingConvention": "stylecop"
+ }
+ }
+}
A => stylecop.ruleset +202 -0
@@ 1,202 @@
+<RuleSet Name="Christofel rules" ToolsVersion="14.0">
+ <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
+
+ <!-- Special rules -->
+ <Rule Id="SA0001" Action="Error"/> <!-- XML comment analysis disabled -->
+ <Rule Id="SA0002" Action="Error"/> <!-- Invalid settings file -->
+
+ <!-- Spacing rules -->
+ <Rule Id="SA1000" Action="Error"/> <!-- Keywords must be spaced correctly -->
+ <Rule Id="SA1001" Action="Error"/> <!-- Commas must be spaced correctly -->
+ <Rule Id="SA1002" Action="Error"/> <!-- Semicolons must be spaced correctly -->
+ <Rule Id="SA1003" Action="Error"/> <!-- Symbols must be spaced correctly -->
+ <Rule Id="SA1004" Action="Error"/> <!-- Documentation lines must begin with single space -->
+ <Rule Id="SA1005" Action="Error"/> <!-- Single line comments must begin with single space -->
+ <Rule Id="SA1006" Action="Error"/> <!-- Preprocessor keywords must not be preceded by space -->
+ <Rule Id="SA1007" Action="Error"/> <!-- Operator keyword must be followed by space -->
+ <Rule Id="SA1008" Action="Error"/> <!-- Opening parenthesis must be spaced correctly -->
+ <Rule Id="SA1009" Action="None"/> <!-- Closing parenthesis must be spaced correctly -->
+ <Rule Id="SA1010" Action="Error"/> <!-- Opening square brackets must be spaced correctly -->
+ <Rule Id="SA1011" Action="Error"/> <!-- Closing square brackets must be spaced correctly -->
+ <Rule Id="SA1012" Action="Error"/> <!-- Opening braces must be spaced correctly -->
+ <Rule Id="SA1013" Action="Error"/> <!-- Closing braces must be spaced correctly -->
+ <Rule Id="SA1014" Action="Error"/> <!-- Opening generic brackets must be spaced correctly -->
+ <Rule Id="SA1015" Action="Error"/> <!-- Closing generic brackets must be spaced correctly -->
+ <Rule Id="SA1016" Action="Error"/> <!-- Opening attribute brackets must be spaced correctly -->
+ <Rule Id="SA1017" Action="Error"/> <!-- Closing attribute brackets must be spaced correctly -->
+ <Rule Id="SA1018" Action="Error"/> <!-- Nullable type symbols must be spaced correctly -->
+ <Rule Id="SA1019" Action="Error"/> <!-- Member access symbols must be spaced correctly -->
+ <Rule Id="SA1020" Action="Error"/> <!-- Increment decrement symbols must be spaced correctly -->
+ <Rule Id="SA1021" Action="Error"/> <!-- Negative signs must be spaced correctly -->
+ <Rule Id="SA1022" Action="Error"/> <!-- Positive signs must be spaced correctly -->
+ <Rule Id="SA1023" Action="Error"/> <!-- Dereference and access of symbols must be spaced correctly -->
+ <Rule Id="SA1024" Action="Error"/> <!-- Colons must be spaced correctly -->
+ <Rule Id="SA1025" Action="Error"/> <!-- Code must not contain multiple whitespace in a row -->
+ <Rule Id="SA1026"
+ Action="Error"/> <!-- Code must not contain space after new keyword in implicitly typed array allocation -->
+ <Rule Id="SA1027" Action="Error"/> <!-- Use tabs correctly -->
+ <Rule Id="SA1028" Action="Error"/> <!-- Code must not contain trailing whitespace -->
+
+ <!-- Readability rules -->
+ <Rule Id="SA1100" Action="Error"/> <!-- Do not prefix calls with base unless local implementation exists -->
+ <Rule Id="SA1101" Action="None"/> <!-- Prefix local calls with this -->
+ <Rule Id="SX1101" Action="None"/> <!-- Do not prefix local calls with this. -->
+ <Rule Id="SA1102" Action="Error"/> <!-- Query clause must follow previous clause -->
+ <Rule Id="SA1103" Action="Error"/> <!-- Query clauses must be on separate lines or all on one line -->
+ <Rule Id="SA1104"
+ Action="Error"/> <!-- Query clause must begin on new line when previous clause spans multiple lines -->
+ <Rule Id="SA1105" Action="Error"/> <!-- Query clauses spanning multiple lines must begin on own line -->
+ <Rule Id="SA1106" Action="Error"/> <!-- Code must not contain empty statements -->
+ <Rule Id="SA1107" Action="Error"/> <!-- Code must not contain multiple statements on one line -->
+ <Rule Id="SA1108" Action="Error"/> <!-- Block statements must not contain embedded comments -->
+ <Rule Id="SA1110" Action="None"/> <!-- Opening parenthesis or bracket must be on declaration line -->
+ <Rule Id="SA1111" Action="None"/> <!-- Closing parenthesis must be on line of last parameter -->
+ <Rule Id="SA1112" Action="Error"/> <!-- Closing parenthesis must be on line of opening parenthesis -->
+ <Rule Id="SA1113" Action="Error"/> <!-- Comma must be on the same line as previous parameter -->
+ <Rule Id="SA1114" Action="Error"/> <!-- Parameter list must follow declaration -->
+ <Rule Id="SA1115" Action="Error"/> <!-- Parameter must follow comma -->
+ <Rule Id="SA1116" Action="Error"/> <!-- Split parameters must start on line after declaration -->
+ <Rule Id="SA1117" Action="Error"/> <!-- Parameters must be on same line or separate lines -->
+ <Rule Id="SA1118" Action="Error"/> <!-- Parameter must not span multiple lines -->
+ <Rule Id="SA1119" Action="Error"/> <!-- Statement must not use unnecessary parenthesis -->
+ <Rule Id="SA1120" Action="None"/> <!-- Comments must contain text -->
+ <Rule Id="SA1121" Action="Error"/> <!-- Use built-in type alias -->
+ <Rule Id="SA1122" Action="Error"/> <!-- Use string.Empty for empty strings -->
+ <Rule Id="SA1123" Action="Error"/> <!-- Do not place regions within elements -->
+ <Rule Id="SA1124" Action="Error"/> <!-- Do not use regions -->
+ <Rule Id="SA1125" Action="Error"/> <!-- Use shorthand for nullable types -->
+ <Rule Id="SA1127" Action="None"/> <!-- Generic type constraints must be on their own line -->
+ <Rule Id="SA1128" Action="Error"/> <!-- Put constructor initializers on their own line -->
+ <Rule Id="SA1129" Action="Error"/> <!-- Do not use default value type constructor -->
+ <Rule Id="SA1130" Action="Error"/> <!-- Use lambda syntax -->
+ <Rule Id="SA1131" Action="Error"/> <!-- Use readable conditions -->
+ <Rule Id="SA1132" Action="Error"/> <!-- Do not combine fields -->
+ <Rule Id="SA1133" Action="None"/> <!-- Do not combine attributes -->
+ <Rule Id="SA1134" Action="Error"/> <!-- Attributes must not share line -->
+ <Rule Id="SA1136" Action="Error"/> <!-- Enum values should be on separate lines -->
+ <Rule Id="SA1137" Action="Error"/> <!-- Elements should have the same indentation -->
+ <Rule Id="SA1139" Action="Error"/> <!-- Use literals suffix notation instead of casting -->
+
+ <!-- Ordering rules -->
+ <Rule Id="SA1200" Action="None"/> <!-- Using directives must be placed correctly -->
+ <Rule Id="SA1201" Action="None"/> <!-- Elements must appear in the correct order -->
+ <Rule Id="SA1202" Action="None"/> <!-- Elements must be ordered by access -->
+ <Rule Id="SA1203" Action="Error"/> <!-- Constants must appear before fields -->
+ <Rule Id="SA1204" Action="None"/> <!-- Static elements must appear before instance elements -->
+ <Rule Id="SA1205" Action="Error"/> <!-- Partial elements must declare access -->
+ <Rule Id="SA1206" Action="Error"/> <!-- Declaration keywords must follow order -->
+ <Rule Id="SA1207" Action="Error"/> <!-- Protected must come before internal -->
+ <Rule Id="SA1208" Action="Error"/> <!-- System using directives must be placed before other using directives -->
+ <Rule Id="SA1209" Action="Error"/> <!-- Using alias directives must be placed after other using directives -->
+ <Rule Id="SA1210" Action="Error"/> <!-- Using directives must be ordered alphabetically by namespace -->
+ <Rule Id="SA1210" Action="Error"/> <!-- Using directives must be ordered alphabetically by namespace -->
+ <Rule Id="SA1211" Action="Error"/> <!-- Using alias directives must be ordered alphabetically by alias name -->
+ <Rule Id="SA1212" Action="Error"/> <!-- Property accessors must follow order -->
+ <Rule Id="SA1213" Action="Error"/> <!-- Event accessors must follow order -->
+ <Rule Id="SA1214" Action="Error"/> <!-- Readonly fields must appear before non-readonly fields -->
+ <Rule Id="SA1216" Action="Error"/> <!-- Using static directives must be placed at the correct location. -->
+ <Rule Id="SA1217" Action="Error"/> <!-- Using static directives must be ordered alphabetically -->
+
+ <!-- Naming rules -->
+ <Rule Id="SA1300" Action="None"/> <!-- Element must begin with upper-case letter -->
+ <Rule Id="SA1302" Action="Error"/> <!-- Interface names must begin with I -->
+ <Rule Id="SA1303" Action="Error"/> <!-- Const field names must begin with upper-case letter -->
+ <Rule Id="SA1304" Action="Error"/> <!-- Non-private readonly fields must begin with upper-case letter -->
+ <Rule Id="SA1305" Action="None"/> <!-- Field names must not use Hungarian notation -->
+ <Rule Id="SA1306" Action="None"/> <!-- Field names must begin with lower-case letter -->
+ <Rule Id="SA1307" Action="Error"/> <!-- Accessible fields must begin with upper-case letter -->
+ <Rule Id="SA1308" Action="Error"/> <!-- Variable names must not be prefixed -->
+ <Rule Id="SA1309" Action="None"/> <!-- Field names must not begin with underscore -->
+ <Rule Id="SX1309" Action="Error"/> <!-- Field names must begin with underscore -->
+ <Rule Id="SX1309S" Action="None"/> <!-- Static field names must begin with underscore -->
+ <Rule Id="SA1310" Action="None"/> <!-- Field names must not contain underscore -->
+ <Rule Id="SA1311" Action="Error"/> <!-- Static readonly fields must begin with upper-case letter -->
+ <Rule Id="SA1312" Action="Error"/> <!-- Variable names must begin with lower-case letter -->
+ <Rule Id="SA1313" Action="Error"/> <!-- Parameter names must begin with lower-case letter -->
+ <Rule Id="SA1314" Action="Error"/> <!-- Type name parameters must begin with T -->
+
+ <!-- Maintainability rules -->
+ <Rule Id="SA1400" Action="Error"/> <!-- Access modifier must be declared -->
+ <Rule Id="SA1401" Action="Error"/> <!-- Fields must be private -->
+ <Rule Id="SA1402" Action="Error"/> <!-- File may only contain a single class -->
+ <Rule Id="SA1403" Action="Error"/> <!-- File may only contain a single namespace -->
+ <Rule Id="SA1404" Action="Error"/> <!-- Code analysis suppression must have justification -->
+ <Rule Id="SA1405" Action="Error"/> <!-- Debug.Assert must provide message text -->
+ <Rule Id="SA1406" Action="Error"/> <!-- Debug.Fail must provide message text -->
+ <Rule Id="SA1407" Action="Error"/> <!-- Arithmetic expressions must declare precedence -->
+ <Rule Id="SA1408" Action="Error"/> <!-- Conditional expressions must declare precedence -->
+ <Rule Id="SA1410" Action="Error"/> <!-- Remove delegate parenthesis when possible -->
+ <Rule Id="SA1411" Action="Error"/> <!-- Attribute constructor must not use unnecessary parenthesis -->
+ <Rule Id="SA1412" Action="None"/> <!-- Store files as UTF-8 with byte order mark -->
+ <Rule Id="SA1413" Action="None"/> <!-- Use trailing comma in multi-line initializers -->
+
+ <!-- Layout rules -->
+ <Rule Id="SA1500" Action="Error"/> <!-- Braces for multi-line statements must not share line -->
+ <Rule Id="SA1501" Action="Error"/> <!-- Statement must not be on a single line -->
+ <Rule Id="SA1502" Action="Error"/> <!-- Element must not be on a single line -->
+ <Rule Id="SA1503" Action="Error"/> <!-- Braces must not be omitted -->
+ <Rule Id="SA1504" Action="Error"/> <!-- All accessors must be single-line or multi-line -->
+ <Rule Id="SA1505" Action="Error"/> <!-- Opening braces must not be followed by blank line -->
+ <Rule Id="SA1506" Action="Error"/> <!-- Element documentation headers must not be followed by blank line -->
+ <Rule Id="SA1507" Action="Error"/> <!-- Code must not contain multiple blank lines in a row -->
+ <Rule Id="SA1508" Action="None"/> <!-- Closing braces must not be preceded by blank line -->
+ <Rule Id="SA1509" Action="Error"/> <!-- Opening braces must not be preceded by blank line -->
+ <Rule Id="SA1510" Action="Error"/> <!-- Chained statement blocks must not be preceded by blank line -->
+ <Rule Id="SA1511" Action="Error"/> <!-- While-do footer must not be preceded by blank line -->
+ <Rule Id="SA1512" Action="Warning"/> <!-- Single-line comments must not be followed by blank line -->
+ <Rule Id="SA1513" Action="None"/> <!-- Closing brace must be followed by blank line -->
+ <Rule Id="SA1514" Action="Error"/> <!-- Element documentation header must be preceded by blank line -->
+ <Rule Id="SA1515" Action="Error"/> <!-- Single-line comment must be preceded by blank line -->
+ <Rule Id="SA1516" Action="Error"/> <!-- Elements must be separated by blank line -->
+ <Rule Id="SA1517" Action="Error"/> <!-- Code must not contain blank lines at start of file -->
+ <Rule Id="SA1518" Action="Error"/> <!-- Use line endings correctly at end of file -->
+ <Rule Id="SA1519" Action="Error"/> <!-- Braces must not be omitted from multi-line child statement -->
+ <Rule Id="SA1520" Action="Error"/> <!-- Use braces consistently -->
+
+ <!-- Documentation rules -->
+ <Rule Id="SA1600" Action="Error"/> <!-- Elements must be documented -->
+ <Rule Id="SA1601" Action="None"/> <!-- Partial elements must be documented -->
+ <Rule Id="SA1602" Action="Error"/> <!-- Enumeration items must be documented -->
+ <Rule Id="SA1604" Action="Error"/> <!-- Element documentation must have summary -->
+ <Rule Id="SA1605" Action="Error"/> <!-- Partial element documentation must have summary -->
+ <Rule Id="SA1606" Action="Error"/> <!-- Element documentation must have summary text -->
+ <Rule Id="SA1607" Action="Error"/> <!-- Partial element documentation must have summary text -->
+ <Rule Id="SA1608" Action="Error"/> <!-- Element documentation must not have default summary -->
+ <Rule Id="SA1609" Action="None"/> <!-- Property documentation must have value -->
+ <Rule Id="SA1610" Action="None"/> <!-- Property documentation must have value text -->
+ <Rule Id="SA1611" Action="Error"/> <!-- Element parameters must be documented -->
+ <Rule Id="SA1612" Action="Error"/> <!-- Element parameter documentation must match element parameters -->
+ <Rule Id="SA1613" Action="Error"/> <!-- Element parameter documentation must declare parameter name -->
+ <Rule Id="SA1614" Action="Error"/> <!-- Element parameter documentation must have text -->
+ <Rule Id="SA1615" Action="Error"/> <!-- Element return value must be documented -->
+ <Rule Id="SA1616" Action="Error"/> <!-- Element return value documentation must have text -->
+ <Rule Id="SA1617" Action="Error"/> <!-- Void return value must not be documented -->
+ <Rule Id="SA1618" Action="Error"/> <!-- Generic type parameters must be documented -->
+ <Rule Id="SA1619" Action="Error"/> <!-- Generic type parameters must be documented partial class -->
+ <Rule Id="SA1620" Action="Error"/> <!-- Generic type parameter documentation must match type parameters -->
+ <Rule Id="SA1621" Action="Error"/> <!-- Generic type parameter documentation must declare parameter name -->
+ <Rule Id="SA1622" Action="Error"/> <!-- Generic type parameter documentation must have text -->
+ <Rule Id="SA1623" Action="None"/> <!-- Property summary documentation must match accessors -->
+ <Rule Id="SA1624"
+ Action="None"/> <!-- Property summary documentation must omit accessor with restricted access -->
+ <Rule Id="SA1625" Action="Error"/> <!-- Element documentation must not be copied and pasted -->
+ <Rule Id="SA1626" Action="Error"/> <!-- Single-line comments must not use documentation style slashes -->
+ <Rule Id="SA1627" Action="Error"/> <!-- Documentation text must not be empty -->
+ <Rule Id="SA1629" Action="Error"/> <!-- Documentation text must end with a period -->
+ <Rule Id="SA1633" Action="Error"/> <!-- File must have header -->
+ <Rule Id="SA1634" Action="Error"/> <!-- File header must show copyright -->
+ <Rule Id="SA1635" Action="Error"/> <!-- File header must have copyright text -->
+ <Rule Id="SA1636" Action="Error"/> <!-- File header copyright text must match -->
+ <Rule Id="SA1637" Action="Error"/> <!-- File header must contain file name -->
+ <Rule Id="SA1638" Action="Error"/> <!-- File header file name documentation must match file name -->
+ <Rule Id="SA1639" Action="Error"/> <!-- File header must have summary -->
+ <Rule Id="SA1640" Action="Error"/> <!-- File header must have valid company text -->
+ <Rule Id="SA1641" Action="Error"/> <!-- File header company name text must match -->
+ <Rule Id="SA1642" Action="Error"/> <!-- Constructor summary documentation must begin with standard text -->
+ <Rule Id="SA1643" Action="Error"/> <!-- Destructor summary documentation must begin with standard text -->
+ <Rule Id="SA1644" Action="Error"/> <!-- Documentation header must not contain blank lines -->
+ <Rule Id="SA1648" Action="Error"/> <!-- inheritdoc must be used with inheriting class -->
+ <Rule Id="SA1649" Action="Error"/> <!-- File name must match first type name -->
+ <Rule Id="SA1651" Action="Error"/> <!-- Do not use placeholder elements -->
+ </Rules>
+</RuleSet>