~ruther/NosSmooth.Local

abdcf979af451c162dd74dc1622aebb23a0357c5 — Rutherther 3 years ago
feat: add initial migrated files from NosSmooth repository
100 files changed, 7255 insertions(+), 0 deletions(-)

A .gitignore
A Directory.Build.props
A LICENSE
A NosSmooth.Local.sln
A src/Core/NosSmooth.LocalBinding/Errors/BindingNotFoundError.cs
A src/Core/NosSmooth.LocalBinding/Errors/CouldNotInitializeModuleError.cs
A src/Core/NosSmooth.LocalBinding/Errors/NotNostaleProcessError.cs
A src/Core/NosSmooth.LocalBinding/Extensions/MemoryExtensions.cs
A src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs
A src/Core/NosSmooth.LocalBinding/IsExternalInit.cs
A src/Core/NosSmooth.LocalBinding/NosBindingManager.cs
A src/Core/NosSmooth.LocalBinding/NosBrowserManager.cs
A src/Core/NosSmooth.LocalBinding/NosSmooth.LocalBinding.csproj
A src/Core/NosSmooth.LocalBinding/Objects/NetworkBinding.cs
A src/Core/NosSmooth.LocalBinding/Objects/NostaleStringA.cs
A src/Core/NosSmooth.LocalBinding/Objects/PetManagerBinding.cs
A src/Core/NosSmooth.LocalBinding/Objects/PlayerManagerBinding.cs
A src/Core/NosSmooth.LocalBinding/Objects/UnitManagerBinding.cs
A src/Core/NosSmooth.LocalBinding/Options/CharacterBindingOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/NetworkBindingOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/PetManagerBindingOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/PetManagerOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/PlayerManagerOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/SceneManagerOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/UnitManagerBindingOptions.cs
A src/Core/NosSmooth.LocalBinding/Structs/ControlManager.cs
A src/Core/NosSmooth.LocalBinding/Structs/MapBaseObj.cs
A src/Core/NosSmooth.LocalBinding/Structs/MapNpcObj.cs
A src/Core/NosSmooth.LocalBinding/Structs/MapPlayerObj.cs
A src/Core/NosSmooth.LocalBinding/Structs/NostaleList.cs
A src/Core/NosSmooth.LocalBinding/Structs/NostaleObject.cs
A src/Core/NosSmooth.LocalBinding/Structs/PetManager.cs
A src/Core/NosSmooth.LocalBinding/Structs/PetManagerList.cs
A src/Core/NosSmooth.LocalBinding/Structs/PlayerManager.cs
A src/Core/NosSmooth.LocalBinding/Structs/SceneManager.cs
A src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/ControlCommandWalkHandler.cs
A src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs
A src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PetWalkCommandHandler.cs
A src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PlayerWalkCommandHandler.cs
A src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs
A src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/WalkUnfinishedReason.cs
A src/Core/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs
A src/Core/NosSmooth.LocalClient/IPacketInterceptor.cs
A src/Core/NosSmooth.LocalClient/IsExternalInit.cs
A src/Core/NosSmooth.LocalClient/LocalClientOptions.cs
A src/Core/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj
A src/Core/NosSmooth.LocalClient/NostaleLocalClient.cs
A src/Core/NosSmooth.LocalClient/NostaleWindow.cs
A src/Core/NosSmooth.LocalClient/Utils/User32.cs
A src/Extensions/NosSmooth.ChatCommands/ChatCommandInterceptor.cs
A src/Extensions/NosSmooth.ChatCommands/ChatCommandsOptions.cs
A src/Extensions/NosSmooth.ChatCommands/FeedbackService.cs
A src/Extensions/NosSmooth.ChatCommands/NosSmooth.ChatCommands.csproj
A src/Extensions/NosSmooth.ChatCommands/ServiceCollectionExtensions.cs
A src/Inject/NosSmooth.Inject/NosSmooth.Inject.vcxproj
A src/Inject/NosSmooth.Inject/NosSmooth.Inject.vcxproj.filters
A src/Inject/NosSmooth.Inject/coreclr_delegates.h
A src/Inject/NosSmooth.Inject/dllmain.cpp
A src/Inject/NosSmooth.Inject/framework.h
A src/Inject/NosSmooth.Inject/hostfxr.h
A src/Inject/NosSmooth.Inject/nethost.h
A src/Inject/NosSmooth.Inject/nossmooth.cpp
A src/Inject/NosSmooth.Inject/nossmooth.h
A src/Inject/NosSmooth.Inject/pch.cpp
A src/Inject/NosSmooth.Inject/pch.h
A src/Inject/NosSmooth.Injector.CLI/Commands/InjectCommand.cs
A src/Inject/NosSmooth.Injector.CLI/Commands/ListProcessesCommand.cs
A src/Inject/NosSmooth.Injector.CLI/NosSmooth.Injector.CLI.csproj
A src/Inject/NosSmooth.Injector.CLI/Program.cs
A src/Inject/NosSmooth.Injector.CLI/app.manifest
A src/Inject/NosSmooth.Injector/Errors/InjectionFailedError.cs
A src/Inject/NosSmooth.Injector/Errors/InsufficientPermissionsError.cs
A src/Inject/NosSmooth.Injector/Errors/ProcessNotFoundError.cs
A src/Inject/NosSmooth.Injector/LoadParams.cs
A src/Inject/NosSmooth.Injector/ManagedMemoryAllocation.cs
A src/Inject/NosSmooth.Injector/NosInjector.cs
A src/Inject/NosSmooth.Injector/NosInjectorOptions.cs
A src/Inject/NosSmooth.Injector/NosSmooth.Injector.csproj
A src/Samples/External/ExternalBrowser/ExternalBrowser.csproj
A src/Samples/External/ExternalBrowser/Program.cs
A src/Samples/LowLevel/InterceptNameChanger/DllMain.cs
A src/Samples/LowLevel/InterceptNameChanger/FodyWeavers.xml
A src/Samples/LowLevel/InterceptNameChanger/InterceptNameChanger.csproj
A src/Samples/LowLevel/InterceptNameChanger/NameChangeInterceptor.cs
A src/Samples/LowLevel/InterceptNameChanger/NameChanger.cs
A src/Samples/LowLevel/InterceptNameChanger/Properties/AssemblyInfo.cs
A src/Samples/LowLevel/SimpleChat/DllMain.cs
A src/Samples/LowLevel/SimpleChat/FodyWeavers.xml
A src/Samples/LowLevel/SimpleChat/SayResponder.cs
A src/Samples/LowLevel/SimpleChat/SimpleChat.cs
A src/Samples/LowLevel/SimpleChat/SimpleChat.csproj
A src/Samples/LowLevel/WalkCommands/Commands/CombatCommands.cs
A src/Samples/LowLevel/WalkCommands/Commands/DetachCommand.cs
A src/Samples/LowLevel/WalkCommands/Commands/WalkCommands.cs
A src/Samples/LowLevel/WalkCommands/DllMain.cs
A src/Samples/LowLevel/WalkCommands/FodyWeavers.xml
A src/Samples/LowLevel/WalkCommands/Startup.cs
A src/Samples/LowLevel/WalkCommands/WalkCommands.csproj
A stylecop.json
A stylecop.ruleset
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>

Do not follow this link