commit 0113a368a44c26f2e739cd1af5c21227de5f4c61 Author: Ereshkigal Date: Fri Nov 5 02:45:04 2021 +0100 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6576fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,364 @@ +## 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/ +[Oo]ut/ +[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 +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# 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 + +# 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 + diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..c8d1305 --- /dev/null +++ b/README.MD @@ -0,0 +1,25 @@ +# Bot monitor + +![botmon](monitor.png) + +Debug monitor that shows active bots on the map, the zone in which they are located and the distance to them. + +Checked on EFT version 0.12.11.1.13487, 0..12.11.2.13615, 0.12.11.13725 / AKI 2.0.0-A8 BLEEDINGEDGE + +# Install + + * Copy folder 'astealz-BotMonitor' to 'user/mods' + +# Usage + +After load on map, open in-game console and type 'botmon N', where N is number between 0 and 3. + + * 0 - disable monitor, + * 1 - show simple bot counter, + * 2 - show zone counter, + * 3 - show zone and distance to each bot. + +# Version history + + * 1.0.0 - Initial release + * 1.0.1 - Added bot diffulty to mode 3 \ No newline at end of file diff --git a/astealz-BotMonitor/module.dll b/astealz-BotMonitor/module.dll new file mode 100644 index 0000000..b5eaae8 Binary files /dev/null and b/astealz-BotMonitor/module.dll differ diff --git a/astealz-BotMonitor/package.js b/astealz-BotMonitor/package.js new file mode 100644 index 0000000..f1ab55c --- /dev/null +++ b/astealz-BotMonitor/package.js @@ -0,0 +1,12 @@ +"use strict"; + +class ModMain { + constructor() { + const mod = require("./package.json"); + this.modName = `${mod.author.toLowerCase()}-${mod.name.toLowerCase()}`; + + Logger.info(`Loading: ${this.modName} : ${mod.version}`); + } +} + +module.exports = new ModMain(); \ No newline at end of file diff --git a/astealz-BotMonitor/package.json b/astealz-BotMonitor/package.json new file mode 100644 index 0000000..efb996b --- /dev/null +++ b/astealz-BotMonitor/package.json @@ -0,0 +1,8 @@ +{ + "name": "botmonitor", + "author": "astealz", + "version": "1.0.1", + "license": "NCSA Open Source", + "main": "package.js" +} + diff --git a/monitor.png b/monitor.png new file mode 100644 index 0000000..00abd7d Binary files /dev/null and b/monitor.png differ diff --git a/src/BotMonitor.cs b/src/BotMonitor.cs new file mode 100644 index 0000000..e7dc0c8 --- /dev/null +++ b/src/BotMonitor.cs @@ -0,0 +1,222 @@ +using EFT; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; +using astealz.BotMonitor.Utils; + +namespace astealz.BotMonitor +{ + class BotMonitor : MonoBehaviour + { + private const int maxMode = 3; + private const int zonePoolSize = 20; + private const int playersPoolSize = 40; + private float timer = 0f; + private float updateRate = 2f; + private int botsCount = 0; + + private List players = new List(playersPoolSize); + private Stack ZoneDataPool { get; } = new Stack(zonePoolSize); + private Stack DistancePool { get; } = new Stack(playersPoolSize); + private SortedDictionary botByZoneDictionary = new SortedDictionary(Comparer.Default); + private StringBuilder stringBuilder = new StringBuilder(200); + private GUIContent guiContent = null; + private Player localPlayer = null; + private int mode = 0; + private GUIStyle textStyle; + + public BotMonitor() + { + for (int i = 0; i < zonePoolSize; i++) + ZoneDataPool.Push(new ZoneData()); + for (int i = 0; i < playersPoolSize; i++) + DistancePool.Push(new DistanceData()); + } + + void Awake() + { + if (localPlayer == null) + localPlayer = Globals.LocalPlayer; + } + + public void SetMode(int mode) + { + if (mode < 1) + this.mode = 1; + else if (mode > maxMode) + this.mode = maxMode; + else + this.mode = mode; + } + + public void Update() + { + if (!Application.isFocused) return; + + timer += Time.deltaTime; + + if (timer < updateRate) + return; + + timer = 0; + + if (localPlayer == null) + return; + + botsCount = 0; + foreach (var keyValue in botByZoneDictionary) + { + var zoneData = keyValue.Value; + foreach (var dist in zoneData.Distance) + { + dist.Reset(); + DistancePool.Push(dist); + } + zoneData.Reset(); + ZoneDataPool.Push(zoneData); + } + botByZoneDictionary.Clear(); + + players.Clear(); + foreach (var player in Globals.GameWorld.RegisteredPlayers) + { + if (!player.isActiveAndEnabled || !player.IsAI) + continue; + players.Add(player); + } + + foreach (var player in players) + { + botsCount++; + + var botOwner = player.GetBotOwner(); + var botZone = botOwner.GetBotZone(); + + string zoneName = botZone.NameZone; + + ZoneData zoneData = null; + if (!botByZoneDictionary.ContainsKey(zoneName)) + { + if (ZoneDataPool.Count == 0) + { + Logger.Error("Not enough pool items!"); + return; + } + zoneData = ZoneDataPool.Pop(); + botByZoneDictionary.Add(zoneName, zoneData); + } + else + { + zoneData = botByZoneDictionary[zoneName]; + } + + zoneData.BotsCount += 1; + if (mode == 3) + { + var distance = DistancePool.Pop(); + distance.BotName = player.Profile.GetNickname(); + distance.Distance = botOwner.DistTo(localPlayer.Transform.position); + distance.BotType = botOwner.Profile.GetRole(); + distance.BotDifficulty = botOwner.Profile.GetDifficulty(); + zoneData.Distance.Add(distance); + } + } + + foreach (var zone in botByZoneDictionary) + { + zone.Value.Distance.Sort(); + } + + } + + public void OnGUI() + { + if (textStyle == null) + { + textStyle = new GUIStyle(GUI.skin.box); + textStyle.alignment = TextAnchor.MiddleLeft; + textStyle.fontSize = 16; + } + + if (guiContent == null) + guiContent = new GUIContent(); + + stringBuilder.Clear(); + + stringBuilder.AppendLine($" Bot monitor - {mode.ToString()}"); + + if (mode >= 2) + { + foreach (var keyValue in botByZoneDictionary) + { + stringBuilder.AppendLine($"{keyValue.Key} = {keyValue.Value.BotsCount.ToString()}"); + if (mode == 3) + { + foreach (var element in keyValue.Value.Distance) + { + stringBuilder.AppendLine($" > [{element.Distance.ToString("F1")}m] [{element.BotType.ToStr()}][{element.BotDifficulty.ToStr()}] '{element.BotName.TransliterateThis()}'"); + } + } + } + } + + stringBuilder.Append($"Total = {botsCount.ToString()}"); + + guiContent.text = stringBuilder.ToString(); + + var size = textStyle.CalcSize(guiContent); + + GUI.Label(new Rect(Screen.width - size.x - 5f, 60f, size.x, size.y), guiContent, textStyle); + } + + class ZoneData + { + public int BotsCount { get; set; } + public List Distance { get; } = new List(playersPoolSize); + + public ZoneData() + { + Reset(); + } + + internal void Reset() + { + BotsCount = 0; + Distance.Clear(); + } + } + + class DistanceData : IComparable + { + public string BotName { get; set; } + public WildSpawnType BotType { get; set; } + public BotDifficulty BotDifficulty { get; set; } + public float Distance { get; set; } + + public DistanceData() + { + Reset(); + } + + public void Reset() + { + BotName = string.Empty; + BotType = WildSpawnType.assault; + BotDifficulty = BotDifficulty.normal; + Distance = 0f; + } + + public int CompareTo(DistanceData other) + { + return Distance > other.Distance + ? 1 + : Distance < other.Distance + ? -1 + : 0; + } + } + } +} diff --git a/src/Extensions.cs b/src/Extensions.cs new file mode 100644 index 0000000..128455d --- /dev/null +++ b/src/Extensions.cs @@ -0,0 +1,194 @@ +using EFT; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using astealz.BotMonitor.Utils; +using UnityEngine; + +namespace astealz.BotMonitor +{ + static class Extensions + { + private static readonly PropertyGetter getAiData; + private static readonly PropertyGetter getBotOwner; + private static readonly PropertyGetter getLeaveData; + private static readonly PropertyGetter getWannaLeave; + private static readonly AccessTools.FieldRef getInfo; + private static readonly AccessTools.FieldRef getSettings; + private static readonly AccessTools.FieldRef getRole; + private static readonly AccessTools.FieldRef getDifficulty; + private static readonly PropertyGetter getBotsGroup; + private static readonly PropertyGetter getBotZone; + private static readonly AccessTools.FieldRef getBotGame; + private static readonly AccessTools.FieldRef getNickname; + private static MethodInvoker botUnspawn; + + static Extensions() + { + var aiDataProperty = AccessTools.Property(typeof(Player), nameof(Player.AIData)); + var botOwnerProperty = AccessTools.Property(aiDataProperty.PropertyType, nameof(Player.AIData.BotOwner)); + var leaveDataProperty = AccessTools.Property(typeof(BotOwner), nameof(BotOwner.LeaveData)); + var wannaLeaveProperty = AccessTools.Property(leaveDataProperty.PropertyType, nameof(BotOwner.LeaveData.WannaLeave)); + var infoField = AccessTools.Field(typeof(Profile), nameof(Profile.Info)); + var settingsField = AccessTools.Field(infoField.FieldType, nameof(Profile.Info.Settings)); + var roleField = AccessTools.Field(settingsField.FieldType, nameof(Profile.Info.Settings.Role)); + var botsGroupProperty = AccessTools.Property(typeof(BotOwner), nameof(BotOwner.BotsGroup)); + var botZoneProperty = AccessTools.Property(botsGroupProperty.PropertyType, nameof(BotOwner.BotsGroup.BotZone)); + var botGameField = AccessTools.Field(botsGroupProperty.PropertyType, nameof(BotOwner.BotsGroup.BotGame)); + var nickNameField = AccessTools.Field(infoField.FieldType, nameof(Profile.Info.Nickname)); + + getAiData = Emit.CreateDynamicPropertyGetter(aiDataProperty); + getBotOwner = Emit.CreateDynamicPropertyGetter(botOwnerProperty); + getLeaveData = Emit.CreateDynamicPropertyGetter(leaveDataProperty); + getWannaLeave = Emit.CreateDynamicPropertyGetter(wannaLeaveProperty); + getInfo = AccessTools.FieldRefAccess(typeof(Profile), nameof(Profile.Info)); + getSettings = AccessTools.FieldRefAccess(infoField.FieldType, nameof(Profile.Info.Settings)); + getRole = AccessTools.FieldRefAccess(settingsField.FieldType, nameof(Profile.Info.Settings.Role)); + getDifficulty = AccessTools.FieldRefAccess(settingsField.FieldType, nameof(Profile.Info.Settings.BotDifficulty)); + getBotsGroup = Emit.CreateDynamicPropertyGetter(botsGroupProperty); + getBotZone = Emit.CreateDynamicPropertyGetter(botZoneProperty); + getBotGame = AccessTools.FieldRefAccess(botsGroupProperty.PropertyType, nameof(BotOwner.BotsGroup.BotGame)); + getNickname = AccessTools.FieldRefAccess(infoField.FieldType, nameof(Profile.Info.Nickname)); + } + + public static T GetOrAddComponent(this GameObject gameObject) where T : Component + { + var c = gameObject.GetComponent(); + if (c == null) + c = gameObject.AddComponent(); + return c; + } + + public static string GetNickname(this Profile profile) + { + var info = getInfo(profile); + return getNickname(info); + } + + public static BotOwner GetBotOwner(this Player player) + { + var aiData = getAiData(player); + return getBotOwner(aiData); + } + + public static bool IsWannaLeave(this BotOwner botOwner) + { + var leaveData = getLeaveData(botOwner); + return getWannaLeave(leaveData); + } + + public static WildSpawnType GetRole(this Profile profile) + { + var info = getInfo(profile); + var settings = getSettings(info); + return getRole(settings); + } + + public static BotDifficulty GetDifficulty(this Profile profile) + { + var info = getInfo(profile); + var settings = getSettings(info); + return getDifficulty(settings); + } + + public static BotZone GetBotZone(this BotOwner botOwner) + { + var botsGroup = getBotsGroup(botOwner); + return getBotZone(botsGroup); + } + + public static void Unspawn(this BotOwner botOwner) + { + var botsGroup = getBotsGroup(botOwner); + var botGame = getBotGame(botsGroup); + + if (botUnspawn == null) + { + // bot game is an interface, need to get realization type to create the dynamic method + var botGameTrueType = botGame.GetType(); + var botUnspawnMethod = AccessTools.Method(botGameTrueType, nameof(BotOwner.BotsGroup.BotGame.BotUnspawn)); + botUnspawn = Emit.CreateDynamicMethodInvoker(botUnspawnMethod); + } + botUnspawn(botGame, botOwner); + } + + public static string ToStr(this BotDifficulty botDifficulty) + { + switch (botDifficulty) + { + case BotDifficulty.easy: + return "easy"; + case BotDifficulty.normal: + return "normal"; + case BotDifficulty.hard: + return "hard"; + case BotDifficulty.impossible: + return "impossible"; + } + return botDifficulty.ToString(); + } + + /// + /// Just trying to avoid boxing + /// + /// + /// string representation of wildSpawnType + public static string ToStr(this WildSpawnType wildSpawnType) + { + switch (wildSpawnType) + { + case WildSpawnType.assault: + return "assault"; + case WildSpawnType.cursedAssault: + return "cursedAssault"; + case WildSpawnType.assaultGroup: + return "assaultGroup"; + case WildSpawnType.marksman: + return "marksman"; + case WildSpawnType.pmcBot: + return "pmcBot"; + case WildSpawnType.sectantPriest: + return "sectantPriest"; + case WildSpawnType.sectantWarrior: + return "sectantWarrior"; + case WildSpawnType.bossBully: + return "bossBully"; + case WildSpawnType.bossGluhar: + return "bossGluhar"; + case WildSpawnType.bossKilla: + return "bossKilla"; + case WildSpawnType.bossKojaniy: + return "bossKojaniy"; + case WildSpawnType.bossSanitar: + return "bossSanitar"; + case WildSpawnType.bossTagilla: + return "bossTagilla"; + case WildSpawnType.bossTest: + return "bossTest"; + case WildSpawnType.followerBully: + return "followerBully"; + case WildSpawnType.followerGluharAssault: + return "followerGluharAssault"; + case WildSpawnType.followerGluharScout: + return "followerGluharScout"; + case WildSpawnType.followerGluharSecurity: + return "followerGluharSecurity"; + case WildSpawnType.followerGluharSnipe: + return "followerGluharSnipe"; + case WildSpawnType.followerKojaniy: + return "followerKojaniy"; + case WildSpawnType.followerSanitar: + return "followerSanitar"; + case WildSpawnType.followerTagilla: + return "followerTagilla"; + case WildSpawnType.followerTest: + return "followerTest"; + } + return wildSpawnType.ToString(); + } + } +} diff --git a/src/GameConsole.cs b/src/GameConsole.cs new file mode 100644 index 0000000..a643bcd --- /dev/null +++ b/src/GameConsole.cs @@ -0,0 +1,107 @@ +using EFT; +using EFT.UI; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using UnityEngine; + +namespace astealz.BotMonitor +{ + static class GameConsole + { + private static readonly Type consoleCommandType; + private static readonly ConstructorInfo consoleCommandConstructor; + private static readonly MethodInfo consoleCommandsAddMethod; + private static readonly FieldInfo commandsField; + + private static ConsoleScreen _console = null; + private static List _userCommands = new List(); + + public static bool IsInitialized => _console != null; + + static GameConsole() + { + consoleCommandType = typeof(GameWorld).Assembly.GetTypes() + .Single(t => t.GetProperty("Regex") != null + && t.GetMethod("TryExecute") != null); + consoleCommandConstructor = consoleCommandType.GetConstructor(new[] { typeof(string), typeof(Action) }); + consoleCommandsAddMethod = AccessTools.Field(typeof(ConsoleScreen), nameof(ConsoleScreen.Commands)) + .FieldType + .GetMethod("Add", new[] { consoleCommandType }); + commandsField = AccessTools.Field(typeof(ConsoleScreen), nameof(ConsoleScreen.Commands)); + } + + static void AddCommand(string regular, Action onExecute) + { + var commands = commandsField.GetValue(null); + var command = consoleCommandConstructor.Invoke(new object[] { regular, onExecute }); + consoleCommandsAddMethod.Invoke(commands, new[] { command }); + } + + public static void Initialize(ConsoleScreen consoleScreen) + { + _console = consoleScreen; + + AddLog($"{Module.Name}: Console initialized", Color.green); + + AddCommand($"{Module.Name.ToLower()}-help", ShowHelp); + + AddLog($"type '{Module.Name.ToLower()}-help' to see available commands", Color.green); + } + + private static void ShowHelp(Match obj) + { + foreach (var cmd in _userCommands) + { + AddLog(cmd, UnityEngine.Color.green); + } + } + + public static void AddLog(string message, Color? color = null) + { + if (_console == null) + return; + _console.AddLog(message, SetColor(ColorToString(color ?? Color.white))); + } + + static string ColorToString(UnityEngine.Color color) + { + return string.Concat(new string[] + { + "#", + FloatNormalizedToHex(color.r), + FloatNormalizedToHex(color.g), + FloatNormalizedToHex(color.b), + FloatNormalizedToHex(color.a) + }); + } + + static string FloatNormalizedToHex(float value) + { + return Mathf.RoundToInt(value * 255f).ToString("X2"); + } + + internal static string SetColor(string color) + { + return string.Concat(new string[] + { + "", + DateTime.Now.ToString("[HH:mm:ss]"), + ": " + }); + } + + public static void AddCommand(string regular, Action onExecute, string helpMessage) + { + _userCommands.Add(helpMessage); + AddCommand(regular, onExecute); + } + } +} diff --git a/src/GenericPatch`.cs b/src/GenericPatch`.cs new file mode 100644 index 0000000..a128d81 --- /dev/null +++ b/src/GenericPatch`.cs @@ -0,0 +1,100 @@ +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace astealz.BotMonitor +{ + public abstract class GenericPatch where T : GenericPatch + { + private Harmony _harmony; + private HarmonyMethod _prefix; + private HarmonyMethod _postfix; + private HarmonyMethod _transpiler; + private HarmonyMethod _finalizer; + private HarmonyMethod _ilmanipulator; + + public GenericPatch(string name = null, string prefix = null, string postfix = null, string transpiler = null, string finalizer = null, string ilmanipulator = null) + { + _harmony = new Harmony(name ?? typeof(T).Name); + _prefix = GetPatchMethod(prefix); + _postfix = GetPatchMethod(postfix); + _transpiler = GetPatchMethod(transpiler); + _finalizer = GetPatchMethod(finalizer); + _ilmanipulator = GetPatchMethod(ilmanipulator); + + if (_prefix == null && _postfix == null && _transpiler == null && _finalizer == null && _ilmanipulator == null) + { + throw new Exception($"{_harmony.Id}: At least one of the patch methods must be specified"); + } + } + + /// + /// Get original method + /// + /// Method + protected abstract MethodBase GetTargetMethod(); + + /// + /// Get MethodInfo from string + /// + /// Method name + /// Method + private HarmonyMethod GetPatchMethod(string methodName) + { + if (string.IsNullOrWhiteSpace(methodName)) + { + return null; + } + + return new HarmonyMethod(typeof(T).GetMethod(methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)); + } + + /// + /// Apply patch to target + /// + public void Apply() + { + var targetMethod = GetTargetMethod(); + + if (targetMethod == null) + { + throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null"); + } + + try + { + _harmony.Patch(targetMethod, _prefix, _postfix, _transpiler, _finalizer); + } + catch (Exception ex) + { + throw new Exception($"{_harmony.Id}:", ex); + } + } + + /// + /// Remove applied patch from target + /// + public void Remove() + { + var targetMethod = GetTargetMethod(); + + if (targetMethod == null) + { + throw new InvalidOperationException($"{_harmony.Id}: TargetMethod is null"); + } + + try + { + _harmony.Unpatch(targetMethod, HarmonyPatchType.All, _harmony.Id); + } + catch (Exception ex) + { + throw new Exception($"{_harmony.Id}:", ex); + } + } + } +} diff --git a/src/Globals.cs b/src/Globals.cs new file mode 100644 index 0000000..c2e796a --- /dev/null +++ b/src/Globals.cs @@ -0,0 +1,35 @@ +using Comfort.Common; +using EFT; +using EFT.UI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace astealz.BotMonitor +{ + class Globals + { + internal static Player LocalPlayer => GameWorldInitialized + ? GetLocalPlayer(GameWorld.RegisteredPlayers) + : null; + + internal static GameObject Game => GameObject.Find("GAME"); + + internal static bool GameWorldInitialized => Singleton.Instantiated; + internal static GameWorld GameWorld => Singleton.Instance; + internal static GameObject GameWorldObject => GameObject.Find("GameWorld"); + + static Player GetLocalPlayer(List players) + { + foreach (var player in players) + { + if (player.IsYourPlayer) + return player; + } + return null; + } + } +} diff --git a/src/Logger.cs b/src/Logger.cs new file mode 100644 index 0000000..9be450a --- /dev/null +++ b/src/Logger.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using UnityEngine; + +namespace astealz.BotMonitor +{ + static class Logger + { + public static void Info(string msg) + { + var s = $"{Module.Name}: {msg}"; + GameConsole.AddLog(s); + } + + public static void Error(string msg) + { + var s = $"{Module.Name}: {msg}"; + GameConsole.AddLog(s, Color.red); + } + } +} diff --git a/src/Module.cs b/src/Module.cs new file mode 100644 index 0000000..51969f0 --- /dev/null +++ b/src/Module.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using EFT; +using EFT.UI; +using UnityEngine; + +namespace astealz.BotMonitor +{ + class Module + { + internal const string Name = "BotMonitor"; + private static float timer = 0f; + + static void Main() + { + StaticManager.Instance.StaticUpdate += Instance_StaticUpdate; + } + + private static void Instance_StaticUpdate() + { + timer += Time.deltaTime; + if (timer < 3f) + return; + timer = 0; + + if (!GameConsole.IsInitialized) + { + var console = GameObject.Find("Console"); + if (console == null) + return; + + var cs = console.GetComponent(); + if (cs == null) + return; + + GameConsole.Initialize(cs); + + GameConsole.AddCommand(@"botmon\s+(\d)", ToggleBotMonitor, "\tbotmon [0..3] - toggle bot monitor in one of 3 modes"); + + StaticManager.Instance.StaticUpdate -= Instance_StaticUpdate; + } + } + + private static void ToggleBotMonitor(Match obj) + { + int value = 0; + if (!int.TryParse(obj.Groups[1].Value, out value)) + return; + ToggleBotMonitor(value); + } + + private static void ToggleBotMonitor(int value) + { + if (!Globals.GameWorldInitialized) + { + GameConsole.AddLog("Sorry, GameWorld isn't initialized yet."); + return; + } + + var botCounter = Extensions.GetOrAddComponent(Globals.GameWorldObject); + botCounter.SetMode(value); + botCounter.enabled = value != 0; + } + } +} diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..838afc2 --- /dev/null +++ b/src/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// Общие сведения об этой сборке предоставляются следующим набором +// набора атрибутов. Измените значения этих атрибутов для изменения сведений, +// связанные со сборкой. +[assembly: AssemblyTitle("astealz.BotMonitor")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("astealz.BotMonitor")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Установка значения False для параметра ComVisible делает типы в этой сборке невидимыми +// для компонентов COM. Если необходимо обратиться к типу в этой сборке через +// COM, задайте атрибуту ComVisible значение TRUE для этого типа. +[assembly: ComVisible(false)] + +// Следующий GUID служит для идентификации библиотеки типов, если этот проект будет видимым для COM +[assembly: Guid("C06FA716-ABA9-41BD-B5E0-F3F7420AADB5")] + +// Сведения о версии сборки состоят из указанных ниже четырех значений: +// +// Основной номер версии +// Дополнительный номер версии +// Номер сборки +// Редакция +// +// Можно задать все значения или принять номера сборки и редакции по умолчанию +// используя "*", как показано ниже: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/References/Assembly-CSharp.dll b/src/References/Assembly-CSharp.dll new file mode 100644 index 0000000..04d33be Binary files /dev/null and b/src/References/Assembly-CSharp.dll differ diff --git a/src/References/Comfort.dll b/src/References/Comfort.dll new file mode 100644 index 0000000..14ddadd Binary files /dev/null and b/src/References/Comfort.dll differ diff --git a/src/References/UnityEngine.CoreModule.dll b/src/References/UnityEngine.CoreModule.dll new file mode 100644 index 0000000..ffe4eb5 Binary files /dev/null and b/src/References/UnityEngine.CoreModule.dll differ diff --git a/src/References/UnityEngine.IMGUIModule.dll b/src/References/UnityEngine.IMGUIModule.dll new file mode 100644 index 0000000..469d9aa Binary files /dev/null and b/src/References/UnityEngine.IMGUIModule.dll differ diff --git a/src/References/UnityEngine.TextRenderingModule.dll b/src/References/UnityEngine.TextRenderingModule.dll new file mode 100644 index 0000000..cc1eb8f Binary files /dev/null and b/src/References/UnityEngine.TextRenderingModule.dll differ diff --git a/src/References/UnityEngine.UIModule.dll b/src/References/UnityEngine.UIModule.dll new file mode 100644 index 0000000..d76fd3a Binary files /dev/null and b/src/References/UnityEngine.UIModule.dll differ diff --git a/src/Utils/Emit.cs b/src/Utils/Emit.cs new file mode 100644 index 0000000..ba057e8 --- /dev/null +++ b/src/Utils/Emit.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Text; +using System.Threading.Tasks; + +namespace astealz.BotMonitor.Utils +{ + public delegate T PropertyGetter(object instance); + public delegate void PropertySetter(object instance, T value); + public delegate T MethodInvoker(object instance, T1 arg1); + public delegate void MethodInvoker(object instance, T value); + + // methods in this class creates a dynamic methods (and return a delegate) for quick access to a property or method which cannot be called by a direct call + // this approach is about 5x faster than reflection + static class Emit + { + // delegate for property getter + public static PropertyGetter CreateDynamicPropertyGetter( + PropertyInfo propertyInfo, + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + if (!typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArgumentException($"Property type '{propertyInfo.PropertyType}' does not match return type '{typeof(T)}'"); + + var getterMethodInfo = propertyInfo.GetGetMethod(true); + if (getterMethodInfo == null) + throw new InvalidOperationException($"Can't find getter for property '{propertyInfo.Name}' in '{propertyInfo.DeclaringType}'"); + + var dynMethod = new DynamicMethod($"__get_{typeof(T).Name}_prop_{propertyInfo.Name}", typeof(T), new[] { typeof(object) }, propertyInfo.DeclaringType, true); + var ilGen = dynMethod.GetILGenerator(); + if (getterMethodInfo.IsStatic) + { + ilGen.Emit(OpCodes.Call, getterMethodInfo); + } + else + { + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Castclass, propertyInfo.DeclaringType); + ilGen.Emit(OpCodes.Callvirt, getterMethodInfo); + } + ilGen.Emit(OpCodes.Ret); + return (PropertyGetter)dynMethod.CreateDelegate(typeof(PropertyGetter)); + } + + // delegate for property setter + public static PropertySetter CreateDynamicPropertySetter( + PropertyInfo propertyInfo, + BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + if (!typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) + throw new ArgumentException($"Property type '{propertyInfo.PropertyType}' does not match the type of value '{typeof(T)}'"); + + var setterMethodInfo = propertyInfo.GetSetMethod(true); + if (setterMethodInfo == null) + throw new InvalidOperationException($"Can't find setter for property '{propertyInfo.Name}' in '{propertyInfo.DeclaringType}'"); + + var dynMethod = new DynamicMethod($"__set_{typeof(T).Name}_prop_{propertyInfo.Name}", null, new[] { typeof(object), typeof(T) }, propertyInfo.DeclaringType); + var ilGen = dynMethod.GetILGenerator(); + if (setterMethodInfo.IsStatic) + { + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Call, setterMethodInfo); + ilGen.Emit(OpCodes.Ret); + } + else + { + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Castclass, propertyInfo.DeclaringType); + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Callvirt, setterMethodInfo); + } + ilGen.Emit(OpCodes.Ret); + return (PropertySetter)dynMethod.CreateDelegate(typeof(PropertySetter)); + } + + // delegate to invoke a method with one argument and return value + public static MethodInvoker CreateDynamicMethodInvoker(MethodInfo methodInfo) + { + if (methodInfo == null) + throw new ArgumentNullException(nameof(methodInfo)); + + if (!typeof(TR).IsAssignableFrom(methodInfo.ReturnType)) + throw new ArgumentException($"Method return type '{methodInfo.ReturnType}' does not match return type '{typeof(TR)}'"); + + var dynMethod = new DynamicMethod( + $"__invoke_{typeof(TR).Name}_method_{methodInfo.Name}", // method name is only for debugging + typeof(TR), + new[] { typeof(object), typeof(T1) }, // add method arguments here (T2, T3, T4...) + methodInfo.DeclaringType, + true); + + var ilGen = dynMethod.GetILGenerator(); + if (methodInfo.IsStatic) + { + // if target method is static push first argument on stack + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Call, methodInfo); + } + else + { + // otherwise push instance and cast it to type where method is declared + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Castclass, methodInfo.DeclaringType); + // then push method arguments + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Callvirt, methodInfo); + } + ilGen.Emit(OpCodes.Ret); + return (MethodInvoker)dynMethod.CreateDelegate(typeof(MethodInvoker)); + } + + public static MethodInvoker CreateDynamicMethodInvoker(MethodInfo methodInfo) + { + if (methodInfo == null) + throw new ArgumentNullException(nameof(methodInfo)); + + if (!typeof(void).Equals(methodInfo.ReturnType)) + throw new ArgumentException($"Method return type '{methodInfo.ReturnType}' does not match return type 'void'"); + + var dynMethod = new DynamicMethod( + $"__invoke_void_method_{methodInfo.Name}", // method name is only for debugging + typeof(void), + new[] { typeof(object), typeof(T) }, // add method arguments here (T2, T3, T4...) + methodInfo.DeclaringType, + true); + + var ilGen = dynMethod.GetILGenerator(); + if (methodInfo.IsStatic) + { + // if target method is static push first argument on stack + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Call, methodInfo); + } + else + { + // otherwise push instance and cast it to type where method is declared + ilGen.Emit(OpCodes.Ldarg_0); + ilGen.Emit(OpCodes.Castclass, methodInfo.DeclaringType); + // then push method arguments + ilGen.Emit(OpCodes.Ldarg_1); + ilGen.Emit(OpCodes.Callvirt, methodInfo); + } + ilGen.Emit(OpCodes.Ret); + return (MethodInvoker)dynMethod.CreateDelegate(typeof(MethodInvoker)); + } + } +} diff --git a/src/Utils/TextUtils.cs b/src/Utils/TextUtils.cs new file mode 100644 index 0000000..402e572 --- /dev/null +++ b/src/Utils/TextUtils.cs @@ -0,0 +1,53 @@ +using EFT; +using HarmonyLib; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace astealz.BotMonitor.Utils +{ + static class TextUtils + { + private static newTransliterateDelegate transliterateNew; + private static oldTransliterateDelegate transliterateOld; + + delegate string newTransliterateDelegate(string text); + delegate string oldTransliterateDelegate(string text, string locale); + + static TextUtils() + { + MethodInfo GetNewTransliterateMethod(Type t) + { + return t.GetMethod("Transliterate", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null); + } + MethodInfo GetOldTransliterateMethod(Type t) + { + return t.GetMethod("Transliterate", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string), typeof(string) }, null); + } + + + var textUtilsType = typeof(EFT.GameWorld).Assembly.GetTypes() + .Single(t => GetNewTransliterateMethod(t) != null || GetOldTransliterateMethod(t) != null); + var newTransliterateMethod = GetNewTransliterateMethod(textUtilsType); + var oldTransliterateMethod = GetOldTransliterateMethod(textUtilsType); + + if (newTransliterateMethod != null) + transliterateNew = AccessTools.MethodDelegate(newTransliterateMethod, null, false); + if (oldTransliterateMethod != null) + transliterateOld = AccessTools.MethodDelegate(oldTransliterateMethod, null, false); + + } + + public static string TransliterateThis(this string text) + { + if (transliterateNew != null) + return transliterateNew(text); + if (transliterateOld != null) + return transliterateOld(text, "en"); + return text; + } + } +} diff --git a/src/astealz.BotMonitor.csproj b/src/astealz.BotMonitor.csproj new file mode 100644 index 0000000..44f214f --- /dev/null +++ b/src/astealz.BotMonitor.csproj @@ -0,0 +1,104 @@ + + + + + Debug + AnyCPU + {95CFAE42-6D19-4BC7-AC41-EF9DAD478527} + Library + Properties + astealz.BotMonitor + astealz.BotMonitor + v4.7.2 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + + + key.snk + + + + packages\Lib.Harmony.2.1.1\lib\net472\0Harmony.dll + False + False + + + References\Assembly-CSharp.dll + False + + + References\Comfort.dll + False + + + + + + + References\UnityEngine.CoreModule.dll + False + + + References\UnityEngine.IMGUIModule.dll + False + + + False + ..\..\..\astealz.Modules\astealz.BotMonitor\References\UnityEngine.TextRenderingModule.dll + False + + + References\UnityEngine.UIModule.dll + False + + + + + + + + + + + + + + + + + + + + + + + + + + + + + copy /y $(TargetPath) $(SolutionDir)\..\astealz-BotMonitor\module.dll + + \ No newline at end of file diff --git a/src/astealz.BotMonitor.sln b/src/astealz.BotMonitor.sln new file mode 100644 index 0000000..f3da1ed --- /dev/null +++ b/src/astealz.BotMonitor.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31515.178 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "astealz.BotMonitor", "astealz.BotMonitor.csproj", "{95CFAE42-6D19-4BC7-AC41-EF9DAD478527}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {95CFAE42-6D19-4BC7-AC41-EF9DAD478527}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {95CFAE42-6D19-4BC7-AC41-EF9DAD478527}.Debug|Any CPU.Build.0 = Debug|Any CPU + {95CFAE42-6D19-4BC7-AC41-EF9DAD478527}.Release|Any CPU.ActiveCfg = Release|Any CPU + {95CFAE42-6D19-4BC7-AC41-EF9DAD478527}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {52FE0E75-ED1D-427C-AD98-46ADF1C74DE0} + EndGlobalSection +EndGlobal diff --git a/src/key.snk b/src/key.snk new file mode 100644 index 0000000..4b5f85e Binary files /dev/null and b/src/key.snk differ diff --git a/src/packages.config b/src/packages.config new file mode 100644 index 0000000..6fa11be --- /dev/null +++ b/src/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file